Add test infrastructure and example vault content
- Add docker-compose.test.yml for isolated testing - Add test vault with sample Markdown notes across categories - Add frontend audit script for module validation - Increment user failed_attempts in test data
This commit is contained in:
parent
8f3e602869
commit
e1a658cbcc
@ -13,7 +13,7 @@
|
|||||||
"active": true,
|
"active": true,
|
||||||
"created_at": "2026-03-23T19:38:00.742597+00:00",
|
"created_at": "2026-03-23T19:38:00.742597+00:00",
|
||||||
"last_login": null,
|
"last_login": null,
|
||||||
"failed_attempts": 0,
|
"failed_attempts": 2,
|
||||||
"locked_until": null
|
"locked_until": null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
docker-compose.test.yml
Normal file
20
docker-compose.test.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
obsigate:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
image: obsigate:latest
|
||||||
|
container_name: obsigate
|
||||||
|
user: "1000:1000"
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "2020:8080"
|
||||||
|
volumes:
|
||||||
|
- C:\dev\git\python\obsigate\test-vault:/vaults/TestVault
|
||||||
|
- C:\dev\git\python\obsigate\data:/app/data
|
||||||
|
environment:
|
||||||
|
- VAULT_1_NAME=TestVault
|
||||||
|
- VAULT_1_PATH=/vaults/TestVault
|
||||||
|
- OBSIGATE_AUTH_ENABLED=false
|
||||||
|
- OBSIGATE_ADMIN_USER=admin
|
||||||
|
- OBSIGATE_ADMIN_PASSWORD=test123
|
||||||
|
- OBSIGATE_WATCHER_ENABLED=false
|
||||||
22
test-vault/Accueil.md
Normal file
22
test-vault/Accueil.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
titre: Accueil
|
||||||
|
statut: actif
|
||||||
|
tags: [accueil, important]
|
||||||
|
catégorie: général
|
||||||
|
publish: true
|
||||||
|
date: 2025-01-15
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bienvenue dans le vault de test
|
||||||
|
|
||||||
|
Ceci est un document de test pour [[ObsiGate]].
|
||||||
|
|
||||||
|
## Sections
|
||||||
|
|
||||||
|
- [[Projets/Projet Alpha]] - Un projet en cours
|
||||||
|
- [[Notes/Configuration serveur]] - Documentation technique
|
||||||
|
- [[Recettes/Pâtes carbonara]] - Une recette
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
|
||||||
|
#accueil #important #test
|
||||||
29
test-vault/Recettes/Pâtes carbonara.md
Normal file
29
test-vault/Recettes/Pâtes carbonara.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
titre: Pâtes carbonara
|
||||||
|
tags: [recette, cuisine, italien, pâtes]
|
||||||
|
catégorie: cuisine
|
||||||
|
date: 2025-04-10
|
||||||
|
favoris: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Pâtes carbonara authentiques
|
||||||
|
|
||||||
|
## Ingrédients
|
||||||
|
|
||||||
|
- 400g de spaghetti
|
||||||
|
- 200g de guanciale
|
||||||
|
- 4 jaunes d'œufs
|
||||||
|
- 100g de pecorino romano
|
||||||
|
- Poivre noir
|
||||||
|
|
||||||
|
## Préparation
|
||||||
|
|
||||||
|
1. Cuire les pâtes al dente
|
||||||
|
2. Faire revenir le guanciale
|
||||||
|
3. Mélanger les jaunes avec le fromage
|
||||||
|
4. Assembler hors du feu
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Ne jamais utiliser de crème !
|
||||||
|
La vraie carbonara n'a pas de crème.
|
||||||
28
test-vault/Recettes/Tiramisu.md
Normal file
28
test-vault/Recettes/Tiramisu.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
titre: Tiramisu classique
|
||||||
|
tags: [recette, cuisine, italien, dessert]
|
||||||
|
catégorie: cuisine
|
||||||
|
date: 2025-04-15
|
||||||
|
publish: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tiramisu
|
||||||
|
|
||||||
|
## Ingrédients
|
||||||
|
|
||||||
|
- 500g de mascarpone
|
||||||
|
- 4 œufs
|
||||||
|
- 100g de sucre
|
||||||
|
- Biscuits à la cuillère
|
||||||
|
- Café fort
|
||||||
|
- Cacao en poudre
|
||||||
|
|
||||||
|
## Préparation
|
||||||
|
|
||||||
|
1. Séparer les blancs des jaunes
|
||||||
|
2. Mélanger jaunes + sucre + mascarpone
|
||||||
|
3. Monter les blancs en neige
|
||||||
|
4. Incorporer délicatement
|
||||||
|
5. Tremper les biscuits dans le café
|
||||||
|
6. Alterner couches
|
||||||
|
7. Réfrigérer 4h minimum
|
||||||
32
test-vault/notes/Configuration serveur.md
Normal file
32
test-vault/notes/Configuration serveur.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
titre: Configuration serveur
|
||||||
|
tags: [serveur, configuration, technique, linux]
|
||||||
|
catégorie: technique
|
||||||
|
date: 2025-02-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Configuration du serveur
|
||||||
|
|
||||||
|
Documentation pour configurer un serveur Linux.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- Ubuntu 22.04 LTS
|
||||||
|
- Docker installé
|
||||||
|
- 4 Go RAM minimum
|
||||||
|
|
||||||
|
## Étapes
|
||||||
|
|
||||||
|
1. Installation des dépendances
|
||||||
|
2. Configuration du firewall
|
||||||
|
3. Déploiement Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install docker.io
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Voir [[Notes/Sécurité]] pour la sécurisation.
|
||||||
|
Voir [[Notes/Performance]] pour l'optimisation.
|
||||||
22
test-vault/notes/Docker tips.md
Normal file
22
test-vault/notes/Docker tips.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
titre: Astuces Docker
|
||||||
|
tags: [docker, conteneur, déploiement, technique]
|
||||||
|
catégorie: technique
|
||||||
|
date: 2025-05-01
|
||||||
|
---
|
||||||
|
|
||||||
|
# Astuces Docker
|
||||||
|
|
||||||
|
## Nettoyage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker system prune -a
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Compose
|
||||||
|
|
||||||
|
Toujours utiliser des fichiers `.env` pour les secrets.
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
Préférer les volumes nommés aux bind mounts.
|
||||||
20
test-vault/notes/Performance.md
Normal file
20
test-vault/notes/Performance.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
titre: Optimisation des performances
|
||||||
|
tags: [performance, optimisation, serveur]
|
||||||
|
catégorie: technique
|
||||||
|
date: 2025-03-01
|
||||||
|
---
|
||||||
|
|
||||||
|
# Optimisation des performances
|
||||||
|
|
||||||
|
## Cache
|
||||||
|
|
||||||
|
Utiliser Redis pour le cache.
|
||||||
|
|
||||||
|
## Indexation
|
||||||
|
|
||||||
|
Configurer les index de base de données.
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
Mettre en place Prometheus et Grafana.
|
||||||
31
test-vault/notes/Sécurité.md
Normal file
31
test-vault/notes/Sécurité.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
titre: Sécurité
|
||||||
|
tags: [sécurité, serveur, firewall]
|
||||||
|
catégorie: technique
|
||||||
|
date: 2025-01-30
|
||||||
|
publish: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sécurité du serveur
|
||||||
|
|
||||||
|
## Firewall
|
||||||
|
|
||||||
|
Utiliser ufw pour le firewall :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ufw enable
|
||||||
|
sudo ufw allow 22/tcp
|
||||||
|
sudo ufw allow 80/tcp
|
||||||
|
sudo ufw allow 443/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Certificats SSL
|
||||||
|
|
||||||
|
Utiliser Let's Encrypt pour les certificats.
|
||||||
|
|
||||||
|
## Mots de passe
|
||||||
|
|
||||||
|
Toujours utiliser des mots de passe forts.
|
||||||
|
Minimum 16 caractères.
|
||||||
|
|
||||||
|
Voir aussi [[Notes/Configuration serveur]].
|
||||||
31
test-vault/projets/Projet Alpha.md
Normal file
31
test-vault/projets/Projet Alpha.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
titre: Projet Alpha
|
||||||
|
statut: en cours
|
||||||
|
tags: [projet, alpha, serveur]
|
||||||
|
catégorie: développement
|
||||||
|
publish: true
|
||||||
|
date: 2025-03-10
|
||||||
|
favoris: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Projet Alpha
|
||||||
|
|
||||||
|
Ce projet consiste à déployer un [[Notes/Configuration serveur|serveur]] pour l'application.
|
||||||
|
|
||||||
|
## Objectifs
|
||||||
|
|
||||||
|
- Mise en place du serveur
|
||||||
|
- Configuration de [[Notes/Sécurité]]
|
||||||
|
- Tests de performance
|
||||||
|
|
||||||
|
## Équipe
|
||||||
|
|
||||||
|
- Bruno (lead)
|
||||||
|
- Équipe DevOps
|
||||||
|
|
||||||
|
## Avancement
|
||||||
|
|
||||||
|
Le projet est **en cours**. La première phase est terminée.
|
||||||
|
La deuxième phase débute la semaine prochaine.
|
||||||
|
|
||||||
|
Voir aussi [[Projet Beta]] pour le projet connexe.
|
||||||
21
test-vault/projets/Projet Beta.md
Normal file
21
test-vault/projets/Projet Beta.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
titre: Projet Beta
|
||||||
|
statut: planifié
|
||||||
|
tags: [projet, beta, frontend]
|
||||||
|
catégorie: développement
|
||||||
|
date: 2025-05-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Projet Beta
|
||||||
|
|
||||||
|
Ce projet est le successeur du [[Projets/Projet Alpha]].
|
||||||
|
|
||||||
|
## Technos
|
||||||
|
|
||||||
|
- React
|
||||||
|
- TypeScript
|
||||||
|
- Tailwind CSS
|
||||||
|
|
||||||
|
## Planning
|
||||||
|
|
||||||
|
Début prévu : juin 2025
|
||||||
7
test-vault/subfolder/Document caché.md
Normal file
7
test-vault/subfolder/Document caché.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
tags: [caché, test]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Document dans un sous-dossier
|
||||||
|
|
||||||
|
Ce document teste l'arborescence.
|
||||||
418
tests/frontend/audit.mjs
Normal file
418
tests/frontend/audit.mjs
Normal file
@ -0,0 +1,418 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* ObsiGate — Comprehensive frontend validator.
|
||||||
|
* Scans ALL modules and catches:
|
||||||
|
* 1. Function calls without import or local definition
|
||||||
|
* 2. State variables used without state. prefix
|
||||||
|
* 3. Imported functions that don't exist in source module
|
||||||
|
* 4. Const reassignments
|
||||||
|
* Usage: node tests/frontend/audit.mjs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, readdirSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const JS_DIR = join(__dirname, '../../frontend/js');
|
||||||
|
|
||||||
|
let errors = [];
|
||||||
|
let warnings = [];
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Browser globals allowed without import
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
const BROWSER_GLOBALS = new Set([
|
||||||
|
'console','document','window','fetch','localStorage','sessionStorage','navigator','location','history',
|
||||||
|
'setTimeout','clearTimeout','setInterval','clearInterval','requestAnimationFrame','cancelAnimationFrame',
|
||||||
|
'addEventListener','removeEventListener','getElementById','querySelector','querySelectorAll',
|
||||||
|
'getComputedStyle','getBoundingClientRect','createElement','createElementNS','createTextNode',
|
||||||
|
'createDocumentFragment','appendChild','removeChild','insertBefore','replaceChild','cloneNode',
|
||||||
|
'setAttribute','getAttribute','removeAttribute','hasAttribute','classList','parentElement','parentNode',
|
||||||
|
'children','childNodes','firstChild','lastChild','nextSibling','previousSibling','textContent','innerHTML',
|
||||||
|
'style','className','value','checked','focus','blur','click','preventDefault','stopPropagation',
|
||||||
|
'JSON','Math','Date','Object','Array','Map','Set','WeakMap','Promise','Symbol','RegExp','Error',
|
||||||
|
'parseInt','parseFloat','isNaN','isFinite','encodeURIComponent','decodeURIComponent','atob','btoa',
|
||||||
|
'String','Number','Boolean','Intl','devicePixelRatio','URLSearchParams','FormData','Blob','FileReader',
|
||||||
|
'Image','EventSource','WebSocket','Worker','ServiceWorker','Notification','Headers','Request','Response',
|
||||||
|
'MutationObserver','IntersectionObserver','ResizeObserver','DOMParser','CustomEvent','Event',
|
||||||
|
'KeyboardEvent','MouseEvent','WheelEvent','FocusEvent','InputEvent','ClipboardEvent','TouchEvent',
|
||||||
|
'requestFullscreen','exitFullscreen','fullscreenElement','crypto','performance','alert','confirm',
|
||||||
|
'scrollTo','scrollBy','scrollIntoView','getContext','toDataURL','normalize','dispatchEvent',
|
||||||
|
'execCommand','closest','contains','toggle','replaceChildren','remove','before','after','TextDecoder',
|
||||||
|
'TextEncoder','Uint8Array','ArrayBuffer','File','FileList','DataTransfer','matchMedia',
|
||||||
|
'structuredClone','queueMicrotask','reportError',
|
||||||
|
// lucide icons
|
||||||
|
'lucide','createIcons',
|
||||||
|
// markdown rendering
|
||||||
|
'marked','hljs','frontmatter','mistune',
|
||||||
|
// Canvas drawing
|
||||||
|
'beginPath','closePath','moveTo','lineTo','arc','fill','stroke','fillStyle','strokeStyle',
|
||||||
|
'lineWidth','font','textAlign','fillText','clearRect','measureText','setLineDash','shadowColor',
|
||||||
|
'shadowBlur','setTransform','save','restore','translate','scale','rotate','roundRect',
|
||||||
|
'getPropertyValue',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Phase 1: Parse all modules — collect exports, imports, local definitions
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const modules = {};
|
||||||
|
const allExports = {};
|
||||||
|
|
||||||
|
function parseModule(fileName) {
|
||||||
|
const content = readFileSync(join(JS_DIR, fileName), 'utf-8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
imports: [], // [{ name, from }]
|
||||||
|
exports: new Set(),
|
||||||
|
localDefs: new Set(), // function names, const names defined in this file
|
||||||
|
localVars: new Set(), // let/var names
|
||||||
|
content,
|
||||||
|
lines,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inExportBlock = false;
|
||||||
|
let exportBlockText = '';
|
||||||
|
let inMultiLineImport = false;
|
||||||
|
let importBlockText = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
// ─ Multi-line export { ... } ─
|
||||||
|
if (/^export\s*\{/.test(trimmed) && !/\}/.test(trimmed)) {
|
||||||
|
inExportBlock = true;
|
||||||
|
exportBlockText = trimmed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inExportBlock) {
|
||||||
|
exportBlockText += ' ' + trimmed;
|
||||||
|
if (/\}/.test(trimmed)) {
|
||||||
|
inExportBlock = false;
|
||||||
|
const m = exportBlockText.match(/\{([^}]+)\}/);
|
||||||
|
if (m) {
|
||||||
|
for (const name of m[1].split(',')) {
|
||||||
|
const n = name.trim().replace(/\s+as\s+.*/, '').trim();
|
||||||
|
if (n) info.exports.add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─ Multi-line import { ... } ─
|
||||||
|
if (/^import\s*\{/.test(trimmed) && !/from/.test(trimmed)) {
|
||||||
|
inMultiLineImport = true;
|
||||||
|
importBlockText = trimmed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inMultiLineImport) {
|
||||||
|
importBlockText += ' ' + trimmed;
|
||||||
|
if (/from/.test(trimmed)) {
|
||||||
|
inMultiLineImport = false;
|
||||||
|
processImportLine(importBlockText, info, fileName);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─ export const/let/function/async function X ─
|
||||||
|
let m = trimmed.match(/^export\s+(?:const|let|function|async\s+function|class)\s+(\w+)/);
|
||||||
|
if (m) {
|
||||||
|
info.exports.add(m[1]);
|
||||||
|
info.localDefs.add(m[1]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─ Single-line export { a, b } ─
|
||||||
|
m = trimmed.match(/^export\s*\{([^}]+)\}/);
|
||||||
|
if (m) {
|
||||||
|
for (const name of m[1].split(',')) {
|
||||||
|
const n = name.trim().replace(/\s+as\s+.*/, '').trim();
|
||||||
|
if (n) info.exports.add(n);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─ export { a } from './x.js' ─
|
||||||
|
m = trimmed.match(/^export\s*\{([^}]+)\}\s*from\s*['"]\.\/(\w+\.js)['"]/);
|
||||||
|
if (m) {
|
||||||
|
for (const name of m[1].split(',')) {
|
||||||
|
const n = name.trim().replace(/\s+as\s+.*/, '').trim();
|
||||||
|
if (n) info.exports.add(n);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─ import { a, b } from './x.js' ─
|
||||||
|
m = trimmed.match(/^import\s*\{([^}]+)\}\s*from\s*['"]\.\/(\w+\.js)['"]/);
|
||||||
|
if (m) processImportLine(trimmed, info, fileName);
|
||||||
|
|
||||||
|
// ─ import * as NS from './x.js' ─
|
||||||
|
m = trimmed.match(/^import\s+\*\s+as\s+(\w+)\s+from\s*['"]\.\/(\w+\.js)['"]/);
|
||||||
|
if (m) {
|
||||||
|
info.imports.push({ ns: m[1], from: m[2] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─ function / async function X ─
|
||||||
|
m = trimmed.match(/^(?:async\s+)?function\s+(\w+)/);
|
||||||
|
if (m && !trimmed.startsWith('//') && !trimmed.startsWith('*')) {
|
||||||
|
info.localDefs.add(m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─ Object method definitions: methodName() { or methodName: function ─
|
||||||
|
m = trimmed.match(/^\s+(\w+)\s*\([^)]*\)\s*\{/);
|
||||||
|
if (m && !trimmed.startsWith('//') && !trimmed.startsWith('*')) {
|
||||||
|
info.localDefs.add(m[1]);
|
||||||
|
}
|
||||||
|
m = trimmed.match(/^\s+(\w+)\s*:\s*function/);
|
||||||
|
if (m && !trimmed.startsWith('//') && !trimmed.startsWith('*')) {
|
||||||
|
info.localDefs.add(m[1]);
|
||||||
|
}
|
||||||
|
m = trimmed.match(/^\s+(\w+)\s*:\s*async\s+function/);
|
||||||
|
if (m && !trimmed.startsWith('//') && !trimmed.startsWith('*')) {
|
||||||
|
info.localDefs.add(m[1]);
|
||||||
|
}
|
||||||
|
m = trimmed.match(/^\s+(\w+)\s*:\s*\([^)]*\)\s*=>/);
|
||||||
|
if (m && !trimmed.startsWith('//') && !trimmed.startsWith('*')) {
|
||||||
|
info.localDefs.add(m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─ const X = ─
|
||||||
|
m = trimmed.match(/^const\s+(\w+)\s*=/);
|
||||||
|
if (m) {
|
||||||
|
info.localDefs.add(m[1]);
|
||||||
|
info.localVars.add(m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─ let X = ─
|
||||||
|
m = trimmed.match(/^let\s+(\w+)\s*=/);
|
||||||
|
if (m) {
|
||||||
|
info.localVars.add(m[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allExports[fileName] = info.exports;
|
||||||
|
modules[fileName] = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processImportLine(line, info, fileName) {
|
||||||
|
const m = line.match(/^import\s*\{([^}]+)\}\s*from\s*['"]\.\/(\w+\.js)['"]/);
|
||||||
|
if (!m) return;
|
||||||
|
const source = m[2];
|
||||||
|
for (const name of m[1].split(',')) {
|
||||||
|
const parts = name.trim().split(/\s+as\s+/);
|
||||||
|
const importName = parts[0].trim();
|
||||||
|
const localName = parts.length > 1 ? parts[1].trim() : importName;
|
||||||
|
info.imports.push({ name: localName, from: source });
|
||||||
|
info.localDefs.add(localName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Phase 2: Validate imports exist in source
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function validateImports() {
|
||||||
|
for (const [modName, info] of Object.entries(modules)) {
|
||||||
|
for (const imp of info.imports) {
|
||||||
|
if (imp.ns) {
|
||||||
|
// Namespace import — check accessed properties
|
||||||
|
const nsRegex = new RegExp(`\\b${imp.ns}\\.(\\w+)`, 'g');
|
||||||
|
let m;
|
||||||
|
while ((m = nsRegex.exec(info.content)) !== null) {
|
||||||
|
const accessed = m[1];
|
||||||
|
if (!allExports[imp.from]?.has(accessed)) {
|
||||||
|
if (!BROWSER_GLOBALS.has(accessed) && !info.localDefs.has(accessed)) {
|
||||||
|
errors.push(`${modName}: uses ${imp.ns}.${accessed} but ${imp.from} doesn't export '${accessed}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (imp.name && imp.from) {
|
||||||
|
if (!allExports[imp.from]?.has(imp.name)) {
|
||||||
|
// Check if it's a re-export from another file
|
||||||
|
let found = false;
|
||||||
|
for (const [otherMod, otherExports] of Object.entries(allExports)) {
|
||||||
|
if (otherExports.has(imp.name)) found = true;
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
errors.push(`${modName}: imports '${imp.name}' from ${imp.from} but it's NOT exported there`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// JavaScript keywords that look like function calls but aren't
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
const JS_KEYWORDS = new Set([
|
||||||
|
'if','else','for','while','do','switch','case','default','break','continue',
|
||||||
|
'return','throw','try','catch','finally','async','await','function','var',
|
||||||
|
'let','const','class','extends','super','new','delete','typeof','instanceof',
|
||||||
|
'void','yield','import','export','in','of','this','debugger','with',
|
||||||
|
'true','false','null','undefined','NaN','Infinity',
|
||||||
|
// French/English words that appear in comments/strings
|
||||||
|
'lien','entrant','fichier','passe','vue','function','var','widgets','unsupported',
|
||||||
|
'management','directory','directories','dot','container','storage','options',
|
||||||
|
'connect','interface','header','js','forces','bg','border','accent','muted','text',
|
||||||
|
'all','add','get','set','has','keys','values','entries','some','apply','update',
|
||||||
|
'load','show','hide','render','open','close','init','destroy','toggle',
|
||||||
|
'then','catch','finally','parse','stringify','log','warn','error','debug',
|
||||||
|
'now','random','max','min','abs','round','floor','ceil','sqrt','pow',
|
||||||
|
'cos','sin','log','exp','toString','toFixed','toISOString','toLocaleString',
|
||||||
|
'toLocaleDateString','toLocaleTimeString','toUpperCase','toLowerCase',
|
||||||
|
'search','test','exec','match','replace','split','join','trim','startsWith',
|
||||||
|
'endsWith','includes','indexOf','charAt','substring','slice','sort','reverse',
|
||||||
|
'map','filter','reduce','forEach','find','findIndex','push','pop','shift',
|
||||||
|
'unshift','splice','concat','every','some','flat','flatMap',
|
||||||
|
'getItem','setItem','removeItem','addEventListener','removeEventListener',
|
||||||
|
'dispatchEvent','querySelector','querySelectorAll','getElementById',
|
||||||
|
'getElementsByClassName','getElementsByTagName','createElement',
|
||||||
|
'createTextNode','appendChild','removeChild','insertBefore','replaceChild',
|
||||||
|
'setAttribute','getAttribute','removeAttribute','classList',
|
||||||
|
'getBoundingClientRect','getComputedStyle','requestAnimationFrame',
|
||||||
|
'parseInt','parseFloat','isNaN','isFinite',
|
||||||
|
'disconnect','observe','unobserve','compareDocumentPosition',
|
||||||
|
'preventDefault','stopPropagation','stopImmediatePropagation',
|
||||||
|
'writeText','execCommand','createIcons','getContext','toDataURL',
|
||||||
|
'setTransform','beginPath','closePath','moveTo','lineTo','arc','fill','stroke',
|
||||||
|
'save','restore','translate','scale','rotate','clearRect','fillText',
|
||||||
|
'setLineDash','measureText','getPropertyValue','focus','blur','click',
|
||||||
|
'setTimeout','clearTimeout','setInterval','clearInterval',
|
||||||
|
'encodeURIComponent','decodeURIComponent',
|
||||||
|
'getRecentEvents','getState','getConfig','getMenuData','getVaultIcon',
|
||||||
|
'patternToRegex','isTagFiltered','fmtSize','localCompare',
|
||||||
|
'fromCharCode','codePointAt','normalize','padStart','padEnd','repeat',
|
||||||
|
'createRange','surroundContents','getClientRects',
|
||||||
|
'switch','declare','module','require',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Phase 3: Find function calls and variable refs without import/definition
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// State variable names that MUST use state. prefix
|
||||||
|
const STATE_VARS = new Set([
|
||||||
|
'APP_VERSION','currentVault','currentPath','allVaults','selectedContextVault',
|
||||||
|
'searchTimeout','searchAbortController','advancedSearchOffset','advancedSearchTotal',
|
||||||
|
'advancedSearchSort','advancedSearchLastQuery','suggestAbortController',
|
||||||
|
'dropdownActiveIndex','dropdownItems','currentSearchId','selectedTags',
|
||||||
|
'searchCaseSensitive','searchWholeWord','searchRegex','searchFilterVisible',
|
||||||
|
'SEARCH_HISTORY_KEY','MAX_HISTORY_ENTRIES','SUGGEST_DEBOUNCE_MS',
|
||||||
|
'ADVANCED_SEARCH_LIMIT','MIN_SEARCH_LENGTH','SEARCH_TIMEOUT_MS',
|
||||||
|
'showingSource','cachedRawSource','editorView','editorVault','editorPath',
|
||||||
|
'fallbackEditorEl','_iconDebounceTimer','outlineObserver','activeHeadingId',
|
||||||
|
'headingsCache','rightSidebarVisible','rightSidebarWidth',
|
||||||
|
'sidebarFilterCaseSensitive','activeSidebarTab','filterDebounce','vaultSettings',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function findUndefinedRefs(modName) {
|
||||||
|
const info = modules[modName];
|
||||||
|
if (!info) return;
|
||||||
|
|
||||||
|
// Build "available" set: imported names + locally defined + browser globals
|
||||||
|
const available = new Set([...info.localDefs, ...BROWSER_GLOBALS]);
|
||||||
|
|
||||||
|
// Also add imported namespace prefixes
|
||||||
|
for (const imp of info.imports) {
|
||||||
|
if (imp.ns) available.add(imp.ns);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan for standalone identifiers used as function calls or variable refs
|
||||||
|
const content = info.content;
|
||||||
|
|
||||||
|
// Find all identifiers that look like function calls or property access
|
||||||
|
// Pattern: word(
|
||||||
|
const callRegex = /\b([a-zA-Z_$][\w$]*)\s*\(/g;
|
||||||
|
let m;
|
||||||
|
const checkedLines = new Set();
|
||||||
|
|
||||||
|
while ((m = callRegex.exec(content)) !== null) {
|
||||||
|
const name = m[1];
|
||||||
|
if (available.has(name)) continue;
|
||||||
|
if (JS_KEYWORDS.has(name)) continue;
|
||||||
|
if (name[0] === name[0].toUpperCase() && name[0] !== '_') continue; // skip classes/types
|
||||||
|
|
||||||
|
// Get the line for context
|
||||||
|
const lineNum = content.substring(0, m.index).split('\n').length;
|
||||||
|
if (checkedLines.has(`${name}:${lineNum}`)) continue;
|
||||||
|
checkedLines.add(`${name}:${lineNum}`);
|
||||||
|
|
||||||
|
// Skip if it's a method call like .name(
|
||||||
|
const before = content.substring(Math.max(0, m.index - 1), m.index);
|
||||||
|
if (before === '.') continue;
|
||||||
|
|
||||||
|
// Skip if it's inside a string or comment
|
||||||
|
const lineStart = content.lastIndexOf('\n', m.index) + 1;
|
||||||
|
const lineEnd = content.indexOf('\n', m.index);
|
||||||
|
const line = content.substring(lineStart, lineEnd > 0 ? lineEnd : content.length);
|
||||||
|
if (line.trim().startsWith('//') || line.trim().startsWith('*')) continue;
|
||||||
|
|
||||||
|
errors.push(`${modName}:${lineNum} — '${name}()' called but not imported or defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for bare state variable references (without state. prefix)
|
||||||
|
for (const varName of STATE_VARS) {
|
||||||
|
const bareRegex = new RegExp(`(?<!\\.)(?<!state\\.)\\b${varName}\\b`, 'g');
|
||||||
|
let bm;
|
||||||
|
while ((bm = bareRegex.exec(content)) !== null) {
|
||||||
|
// Skip import/export lines
|
||||||
|
const lineStart = content.lastIndexOf('\n', bm.index) + 1;
|
||||||
|
const line = content.substring(lineStart, content.indexOf('\n', bm.index));
|
||||||
|
if (line.match(/^\s*(import|export)\s/)) continue;
|
||||||
|
// Skip if this file IS state.js
|
||||||
|
if (modName === 'state.js') continue;
|
||||||
|
// Skip if it's inside a string
|
||||||
|
const lineNum = content.substring(0, bm.index).split('\n').length;
|
||||||
|
errors.push(`${modName}:${lineNum} — '${varName}' used without state. prefix`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for const reassignments of imported names
|
||||||
|
for (const imp of info.imports) {
|
||||||
|
if (imp.ns || !imp.name) continue;
|
||||||
|
const reassignRegex = new RegExp(`^\\s*${imp.name}\\s*=\\s*[^=]`, 'gm');
|
||||||
|
let rm;
|
||||||
|
while ((rm = reassignRegex.exec(content)) !== null) {
|
||||||
|
const lineNum = content.substring(0, rm.index).split('\n').length;
|
||||||
|
errors.push(`${modName}:${lineNum} — imported '${imp.name}' is reassigned (const violation)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Run
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const files = readdirSync(JS_DIR).filter(f => f.endsWith('.js') && f !== 'sidebar_raw.js');
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
parseModule(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateImports();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
findUndefinedRefs(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Report
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error(`\n❌ ${errors.length} frontend error(s) found:\n`);
|
||||||
|
for (const err of errors) {
|
||||||
|
console.error(` • ${err}`);
|
||||||
|
}
|
||||||
|
console.error(`\n💡 Fix: add missing imports, exports, or state. prefixes`);
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
const totalExports = Object.values(allExports).reduce((s, e) => s + e.size, 0);
|
||||||
|
console.log(`\n✅ ${files.length} modules, ${totalExports} exports — 0 issues`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user