Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 70 additions & 40 deletions src/core/apps/AppDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@ export interface AppSummary {
envPath: string;
}

async function isDirectory(p: string): Promise<boolean> {
return (await fs.stat(p).catch(() => null))?.isDirectory() ?? false;
}

async function isFile(p: string): Promise<boolean> {
return (await fs.stat(p).catch(() => null))?.isFile() ?? false;
}

/**
* Parcourt appsRoot et détecte les applications ayant un dossier frontend.
* Deux schémas supportés :
* - À la racine : {application}/frontend
* - Sous entcore : entcore/{application}/frontend (id = "entcore/{application}")
* Parcourt appsRoot et détecte les applications frontend.
* Quatre schémas supportés (par ordre de priorité dans chaque zone) :
* - Racine : {application}/frontend
* - Racine direct : {application}/.env + package.json (sans sous-dossier frontend)
* - Entcore : entcore/{application}/frontend (id = "entcore/{application}")
* - Entcore TS : entcore/{application}/src/main/ts (id = "entcore/{application}")
*/
export async function discoverApps(appsRoot: string): Promise<AppSummary[]> {
let entries: string[];
Expand All @@ -35,49 +45,69 @@ export async function discoverApps(appsRoot: string): Promise<AppSummary[]> {
// Schéma racine : {application}/frontend
for (const name of entries) {
const appPath = path.join(appsRoot, name);
const stat = await fs.stat(appPath).catch(() => null);
if (!stat?.isDirectory()) continue;
if (!(await isDirectory(appPath))) continue;

const frontendDir = path.join(appPath, 'frontend');
if (!(await isDirectory(frontendDir))) continue;

apps.push({ id: name, name, path: appPath, envPath: path.join(frontendDir, '.env') });
}

// Schéma racine direct : {application}/.env + package.json (sans sous-dossier frontend)
for (const name of entries) {
if (apps.some((a) => a.id === name)) continue;

const appPath = path.join(appsRoot, name);
if (!(await isDirectory(appPath))) continue;
if (!(await isFile(path.join(appPath, '.env')))) continue;
if (!(await isFile(path.join(appPath, 'package.json')))) continue;
if (await isDirectory(path.join(appPath, 'frontend'))) continue;

apps.push({ id: name, name, path: appPath, envPath: path.join(appPath, '.env') });
}

const entcorePath = path.join(appsRoot, 'entcore');
if (!(await isDirectory(entcorePath))) return apps.sort((a, b) => a.name.localeCompare(b.name));

const frontendPath = path.join(appPath, 'frontend');
const frontendStat = await fs.stat(frontendPath).catch(() => null);
if (!frontendStat?.isDirectory()) continue;
let entcoreEntries: string[];
try {
entcoreEntries = await fs.readdir(entcorePath);
} catch {
entcoreEntries = [];
}

// Schéma entcore : entcore/{application}/frontend (id = "entcore/{application}")
for (const appName of entcoreEntries) {
const appPath = path.join(entcorePath, appName);
if (!(await isDirectory(appPath))) continue;

const frontendDir = path.join(appPath, 'frontend');
if (!(await isDirectory(frontendDir))) continue;

const envPath = path.join(frontendPath, '.env');
apps.push({
id: name,
name,
id: `entcore/${appName}`,
name: appName,
path: appPath,
envPath,
envPath: path.join(frontendDir, '.env'),
});
}

// Schéma entcore : entcore/{application}/frontend (id = "entcore/{application}")
const entcorePath = path.join(appsRoot, 'entcore');
const entcoreStat = await fs.stat(entcorePath).catch(() => null);
if (entcoreStat?.isDirectory()) {
let entcoreEntries: string[];
try {
entcoreEntries = await fs.readdir(entcorePath);
} catch {
entcoreEntries = [];
}
for (const appName of entcoreEntries) {
const appPath = path.join(entcorePath, appName);
const stat = await fs.stat(appPath).catch(() => null);
if (!stat?.isDirectory()) continue;

const frontendPath = path.join(appPath, 'frontend');
const frontendStat = await fs.stat(frontendPath).catch(() => null);
if (!frontendStat?.isDirectory()) continue;

const envPath = path.join(frontendPath, '.env');
apps.push({
id: `entcore/${appName}`,
name: appName,
path: appPath,
envPath,
});
}
// Schéma entcore TS : entcore/{application}/src/main/ts (id = "entcore/{application}")
for (const appName of entcoreEntries) {
if (apps.some((a) => a.id === `entcore/${appName}`)) continue;

const appPath = path.join(entcorePath, appName);
if (!(await isDirectory(appPath))) continue;

const tsDir = path.join(appPath, 'src', 'main', 'ts');
if (!(await isDirectory(tsDir))) continue;

apps.push({
id: `entcore/${appName}`,
name: appName,
path: appPath,
envPath: path.join(tsDir, '.env'),
});
}

return apps.sort((a, b) => a.name.localeCompare(b.name));
Expand Down
40 changes: 39 additions & 1 deletion tests/core/apps/AppDiscovery.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdtemp, mkdir, rm } from 'fs/promises';
import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';

Expand Down Expand Up @@ -69,4 +69,42 @@ describe('AppDiscovery', () => {
expect(rootApp!.path).not.toContain('entcore');
expect(entcoreApp!.path).toContain('entcore');
});

it('détecte une app racine directe avec .env + package.json (sans dossier frontend)', async () => {
await mkdir(join(tempRoot, 'app1'), { recursive: true });
await writeFile(join(tempRoot, 'app1', '.env'), '');
await writeFile(join(tempRoot, 'app1', 'package.json'), '{}');
const apps = await discoverApps(tempRoot);
expect(apps).toHaveLength(1);
expect(apps[0].id).toBe('app1');
expect(apps[0].envPath).toBe(join(tempRoot, 'app1', '.env'));
});

it('ignore le schéma racine direct si un dossier frontend existe (priorité frontend)', async () => {
await mkdir(join(tempRoot, 'app1', 'frontend'), { recursive: true });
await writeFile(join(tempRoot, 'app1', '.env'), '');
await writeFile(join(tempRoot, 'app1', 'package.json'), '{}');
const apps = await discoverApps(tempRoot);
expect(apps).toHaveLength(1);
expect(apps[0].envPath).toContain('frontend');
expect(apps[0].envPath).toContain('.env');
});

it('détecte une app entcore TS via src/main/ts', async () => {
await mkdir(join(tempRoot, 'entcore', 'app1', 'src', 'main', 'ts'), { recursive: true });
const apps = await discoverApps(tempRoot);
expect(apps).toHaveLength(1);
expect(apps[0].id).toBe('entcore/app1');
expect(apps[0].name).toBe('app1');
expect(apps[0].envPath).toBe(join(tempRoot, 'entcore', 'app1', 'src', 'main', 'ts', '.env'));
});

it('ignore le schéma entcore TS si un dossier frontend existe (priorité frontend)', async () => {
await mkdir(join(tempRoot, 'entcore', 'app1', 'frontend'), { recursive: true });
await mkdir(join(tempRoot, 'entcore', 'app1', 'src', 'main', 'ts'), { recursive: true });
const apps = await discoverApps(tempRoot);
expect(apps).toHaveLength(1);
expect(apps[0].envPath).toContain('frontend');
expect(apps[0].envPath).toContain('.env');
});
});
Loading