Skip to main content
Deno 2 is finally here πŸŽ‰οΈ
Learn more

Tuner

deno.land/x/tuner

Tuner - ΠΌΠΎΠ΄ΡƒΠ»ΡŒ для управлСния конфигурациями ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°. Π”Π°Π½Π½Ρ‹Π΅ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ ΠΎΠΏΠΈΡΡ‹Π²Π°ΡŽΡ‚ΡΡ Π² Π²ΠΈΠ΄Π΅Β .tsΒ Ρ„Π°ΠΉΠ»Π° с экспортируСмым ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠΌ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ содСрТит пСрСчислСниС envΒ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… ΠΈ ΠΏΠΎΠ»Π΅ΠΉ ΠΊΠΎΠ½Ρ„ΠΈΠ³Π°. ΠšΠΎΠ½Ρ„ΠΈΠ³ΠΈ ΠΌΠΎΠ³ΡƒΡ‚ ΠΎΠ±Ρ€Π°Π·ΠΎΠ²Ρ‹Π²Π°Ρ‚ΡŒ ΠΈΠ΅Ρ€Π°Ρ€Ρ…ΠΈΡŽ, Π½Π°ΡΠ»Π΅Π΄ΡƒΡΡΡŒ ΠΎΡ‚ Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΈΡ… ΠΈ ΠΏΠ΅Ρ€Π΅Π·Π°ΠΏΠΈΡΡ‹Π²Π°ΡΡΡŒ Π΄ΠΎΡ‡Π΅Ρ€Π½ΠΈΠΌΠΈ.


ОглавлСниС

ΠŸΡ€ΠΎΡΡ‚Π΅ΠΉΡˆΠΈΠΉ ΠΊΠΎΠ½Ρ„ΠΈΠ³

Минимально ΠΊΠΎΠ½Ρ„ΠΈΠ³ ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ описан Ρ‚Π°ΠΊ:

// config/myConfig.tuner.ts
import Tuner from 'https://deno.land/x/tuner/mod.ts';
export default Tuner.tune(
  {
    config: {
      field1: 'value1',
      field2: 100,
      field3: true,
      field4: ['минималистично', 'ΡƒΠ΄ΠΎΠ±Π½ΠΎ', 'Π½Π΅ ΠΏΡ€Π°Π²Π΄Π° Π»ΠΈ?'],
    },
  },
);

Ѐункция tune Π·Π°Π±ΠΎΡ‚Π»ΠΈΠ²ΠΎ подскаТСт структуру ΠΎΠΆΠΈΠ΄Π°Π΅ΠΌΠΎΠ³ΠΎ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Π°

Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° ΠΊΠΎΠ½Ρ„ΠΈΠ³Π° ΠΈ использованиС происходит Ρ‚Π°ΠΊ:

// main.ts
import Tuner from 'https://deno.land/x/tuner/mod.ts';
const cfg = await Tuner.use.loadConfig();
console.log(cfg.config.field2); // 100

ΠŸΡ€ΠΈ запускС ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ Π½Π°Π»ΠΈΡ‡ΠΈΠ΅ env ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ config, Π΅Π΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ - Π½Π°Π·Π²Π°Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»Π° ΠΊΠΎΠ½Ρ„ΠΈΠ³Π° Π΄ΠΎ .tuner.ts, Π² Π΄Π°Π½Π½ΠΎΠΌ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅ это myConfig.

config=myConfig deno run --allow-all main.ts

ΠšΠΎΠ½Ρ„ΠΈΠ³ с описаниСм env-ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ…

Π’ Tuner имССтся Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ ΠΎΠΏΠΈΡΠ°Ρ‚ΡŒ Ρ‚ΠΈΠΏΡ‹ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… окруТСния ΠΈ повСдСния ΠΏΡ€ΠΈ ΠΈΡ… отсутствии:

  • Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ
  • Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΠ΅ процСсса
  • гСнСрация ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ
  • вычислСниС Π½Π° Π»Π΅Ρ‚Ρƒ

НапримСр, Ρ‚Π°ΠΊ:

// config/myConfig.tuner.ts
import Tuner from 'https://deno.land/x/tuner/mod.ts';
export default Tuner.tune(
  {
    env: {
      // Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π—Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ
      env1: Tuner.Env.getString.orDefault('defalut value1'),
      env2: Tuner.Env.getNumber.orDefault(100),
      env3: Tuner.Env.getBoolean.orDefault(true),
      // ΠŸΡ€ΠΎΠΈΠ³Π½ΠΎΡ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ отсуствиС ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ
      env4: Tuner.Env.getString.orNothing(),
      env5: Tuner.Env.getNumber.orNothing(),
      env6: Tuner.Env.getBoolean.orNothing(),
      // Π—Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΡ‚ΡŒ процСсс
      env7: Tuner.Env.getString.orExit(
        'сообщСниС ΠΎΠ± ошибкС, Π½Π΅ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ',
      ),
      env8: Tuner.Env.getNumber.orExit(
        'Π²Ρ‹Π²Π΅Π΄Π΅Ρ‚ Π² консоль ΠΏΠ΅Ρ€Π΅Π΄ Π²Ρ‹Ρ…ΠΎΠ΄ΠΎΠΌ',
      ),
      env9: Tuner.Env.getBoolean.orExit(),
      // Π‘Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅
      env10: Tuner.Env.getString.orThrow(new Error('ошибка')),
      env11: Tuner.Env.getNumber.orThrow(new Error()),
      env12: Tuner.Env.getBoolean.orThrow(new Error()),
      // Π’Ρ‹Ρ‡ΠΈΡΠ»Π΅Π½ΠΈΡ‚ΡŒ Π΄Π°Π½Π½Ρ‹Ρ… ΠΏΠΎ ΠΏΠ΅Ρ€Π΅Π΄Π°Π½Π½ΠΎΠΌΡƒ колбэку
      //(ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ асинхронным, Ссли Π΄Π°Π½Π½Ρ‹Π΅ Π½ΡƒΠΆΠ½ΠΎ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ с диска ΠΈΠ»ΠΈ ΡƒΠ΄Π°Π»Π΅Π½Π½ΠΎ, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€)
      env13: Tuner.Env.getString.orCompute(() => 'computed value1'),
      env14: Tuner.Env.getNumber.orAsyncCompute(() =>
        new Promise(() => 100)
      ),
    },
    config: {
      field1: 'value1',
      field2: 100,
      field3: true,
      field4: ['минималистично', 'ΡƒΠ΄ΠΎΠ±Π½ΠΎ', 'Π½Π΅ ΠΏΡ€Π°Π²Π΄Π° Π»ΠΈ?'],
    },
  },
);

РазумССтся, ΠΌΠΎΠΆΠ½ΠΎ просто ΡƒΠΊΠ°Π·Π°Ρ‚ΡŒ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅-ΠΏΡ€ΠΈΠΌΠΈΡ‚ΠΈΠ², Π²Ρ€ΠΎΠ΄Π΅ env1: 100

ОбъСдинСниС ΠΊΠΎΠ½Ρ„ΠΈΠ³ΠΎΠ²

Tuner позволяСт β€œΡΠΎΠ±Ρ€Π°Ρ‚ΡŒβ€ ΠΊΠΎΠ½Ρ„ΠΈΠ³, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΠΈ, Π½ΡƒΠΆΠ½ΠΎ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π²Ρ‹ΡΡ‚Ρ€ΠΎΠΈΡ‚ΡŒ ΠΈΠ· Π½ΠΈΡ… Ρ†Π΅ΠΏΠΎΡ‡ΠΊΡƒ:

  • Π’Π΅ΠΊΡƒΡ‰ΠΈΠΉ ΠΊΠΎΠ½Ρ„ΠΈΠ³ дополнится всСми полями Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΎΠ³ΠΎ, ΠΏΡ€ΠΈ этом сохранит свои значСния
  • Π’Π΅ΠΊΡƒΡ‰ΠΈΠΉ ΠΊΠΎΠ½Ρ„ΠΈΠ³ дополнится всСми полями Π΄ΠΎΡ‡Π΅Ρ€Π½Π΅Π³ΠΎ, ΠΏΡ€ΠΈ этом ΡΠΎΠ²ΠΏΠ°Π΄Π°ΡŽΡ‰ΠΈΠ΅ поля Π±ΡƒΠ΄ΡƒΡ‚ пСрСписаны значСниями ΠΈΠ· Π΄ΠΎΡ‡Π΅Ρ€Π½Π΅Π³ΠΎ ΠΊΠΎΠ½Ρ„ΠΈΠ³Π°
  • ЗначСния-Ρ„ΡƒΠΊΠ½Ρ†ΠΈΠΈ, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡ‹Π΅ для описания env-ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… Ρ‚Π°ΠΊΠΆΠ΅ ΠΏΠΎΠ΄Ρ‡ΠΈΠ½ΡΡŽΡ‚ΡΡ этим ΠΏΡ€Π°Π²ΠΈΠ»Π°ΠΌ
flowchart LR;
subgraph W[" "]
direction BT
    base["base\nΠ ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΈΠΉ ΠΊΠΎΠ½Ρ„ΠΈΠ³\n{a: 400, b: 401, c:402}"]
    rab["Π Π°Π±ΠΎΡ‡ΠΈΠΉ ΠΊΠΎΠ½Ρ„ΠΈΠ³\n{a: 300, b: 301}\nChild:A\nparent:base"]
    A["А\nΠ”ΠΎΡ‡Π΅Ρ€Π½ΠΈΠΉ ΠΊΠΎΠ½Ρ„ΠΈΠ³ Ρ€Π°Π±ΠΎΡ‡Π΅Π³ΠΎ\n{b: 200, e:201}\nChild:B"]
    B["B\nΠ”ΠΎΡ‡Π΅Ρ€Π½ΠΈΠΉ ΠΊΠΎΠ½Ρ„ΠΈΠ³ A\n{a: 100, d: 101}"]
end
style rab stroke:#300,stroke-width:6px
B-->|"Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ: a=100,d=101"|A-->|"ΠŸΠ΅Ρ€Π΅ΠΏΠΈΡΠ°Ρ‚ΡŒ a->100, b->200\nΠ”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ: d=101,e=201"|rab-->|"ΠŸΠ΅Ρ€Π΅ΠΏΠΈΡΠ°Ρ‚ΡŒ: a->100,b->200\nΠ”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ: d=101, e=201"|base
W-->|Π Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚|F["{a: 100, b: 200, c:402, d: 101, e:201}"]

ΠŸΡ€ΠΈ этом, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, ΠΊΠΎΠ½Ρ„ΠΈΠ³Ρƒ Π’ Π½Π΅ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ ΡƒΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ А Π² качСствС Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΡΠΊΠΎΠ³ΠΎ.

РСализация:

// config/develop.tuner.ts
import Tuner from 'https://deno.land/x/tuner/mod.ts';
export default Tuner.tune({
  child: Tuner.Load.local.configDir('a.tuner.ts'),
  parent: Tuner.Load.local.configDir('base.tuner.ts'),
  config: {
    a: 300,
    b: 301,
  },
});

//config/base.tuner.ts
import Tuner from 'https://deno.land/x/tuner/mod.ts';
export default Tuner.tune({
  config: { a: 400, b: 401, c: 402 },
});

//config/a.tuner.ts
import Tuner from 'https://deno.land/x/tuner/mod.ts';
export default Tuner.tune({
  child: Tuner.Load.local.configDir('b.tuner.ts'),
  config: {
    b: 200,
    e: 201,
  },
});

//config/b.tuner.ts
import Tuner from 'https://deno.land/x/tuner/mod.ts';
export default Tuner.tune({
  config: { a: 100, d: 101 },
});

//main.ts
import Tuner from 'https://deno.land/x/tuner/mod.ts';
const cfg = await Tuner.use.loadConfig();
console.log(cfg);
//{ config: { a: 100, b: 200, c: 402, e: 201, d: 101 }, env: {} }

Tuner.Load ΠΏΡ€Π΅Π΄Π»Π°Π³Π°Π΅Ρ‚ Π»ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ ΠΈ ΡƒΠ΄Π°Π»Π΅Π½Π½Ρ‹ΠΉ Π²Π°Ρ€ΠΈΠ°Π½Ρ‚ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊΠΎΠ½Ρ„ΠΈΠ³Π°.

Tuner.Load.local

Ѐункция Π’Π΅Ρ€Π½Π΅Ρ‚ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ ΠΊΠΎΠ½Ρ„ΠΈΠ³Π° ΠΈΠ· Ρ„Π°ΠΉΠ»Π° ΠΏΠΎ …
absolutePath(path:string) …указанному ΠΏΠΎΠ»Π½ΠΎΠΌΡƒ ΠΏΡƒΡ‚ΠΈ Π΄ΠΎ Π½Π΅Π³ΠΎ
configDir(path:string) …пути, ΠΎΡ‚Π½ΠΎΡΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ с Π½Π°Π·Π²Π°Π½ΠΈΠ΅ΠΌ β€œconfig”
cwd(path:string) β€¦ΠΎΡ‚Π½ΠΎΡΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠΌΡƒ ΠΏΡƒΡ‚ΠΈ Π² Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°

Tuner.Load.remote

Ѐукнция ОписаниС ΠŸΡ€ΠΈΠΌΠ΅Ρ€ (ΠΏΡƒΡΡ‚ΡŒ Ρ„Π°ΠΉΠ» ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ Π»Π΅ΠΆΠΈΡ‚ ΠΏΠΎ адрСсу http://some_server/b.tuner.ts)
import(path:string) Π Π°Π±ΠΎΡ‚Π°Π΅Ρ‚, ΠΊΠ°ΠΊ ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹ΠΉ ΠΈΠΌΠΏΠΎΡ€Ρ‚ child: Tuner.Load.remote.import(”http://some_server/b.tuner.ts”)
callbackReturnModule(cb: () β‡’ Promise<{default: ITunerConfig}>) ΠŸΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ колбэк, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π²Π΅Ρ€Π½Π΅Ρ‚ промис с ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌΡ‹ΠΌ ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΌ child: Tuner.Load.remote.callbackReturnModule(() β‡’ import(”http://some_server/b.tuner.ts”))
callbackReturnString((cb: () => Promise)) ΠŸΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ колбэк, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π²Π΅Ρ€Π½Π΅Ρ‚ промис с тСкстом модуля Π² Π²ΠΈΠ΄Π΅ строки (Π·Π°Π±ΠΈΡ€Π°Π΅ΠΌ ΠΊΠΎΠ΄ ΠΊΠΎΠ½Ρ„ΠΈΠ³Π° ΠΈΠ· Ρ„ΠΎΡ€ΠΌ, Π±Π»ΠΎΠΊΠΎΠ² Π² Notion ΠΈ Ρ‚Π΄) child: Tuner.Load.remote.callbackReturnString(() β‡’ someFetchingFunctionStringReturned(options: {…}))

ΠšΡ€ΠΎΠΌΠ΅ Ρ‚ΠΎΠ³ΠΎ, Tuner.Load.remote ΠΈΠΌΠ΅Π΅Ρ‚ встроСнныС ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ с Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹ΠΌΠΈ сСрвисами Ρ‡Π΅Ρ€Π΅Π· Tuner.Load.remote.providers:

  • notion(key:string, blockUrl:string) - ΠΎΡ‚Π΄Π°Π΅ΠΌ ΠΊΠ»ΡŽΡ‡ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ(Tuner.getEnv ΠΏΠΎΠΌΠΎΠΆΠ΅Ρ‚ Π½Π°ΠΉΡ‚ΠΈ env-ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΡƒΡŽ Π² ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠΈ ΠΈΠ»ΠΈ .env Ρ„Π°ΠΉΠ»Π΅) ΠΈ ссылку Π½Π° Π±Π»ΠΎΠΊ Π² Notion, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌ описан ΠΌΠΎΠ΄ΡƒΠ»ΡŒ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ
  • github(key: string, owner: string, repo: string, filePath: string) - ΠΊΠ»ΡŽΡ‡, Π½ΠΈΠΊ дСрТатСля Ρ€Π΅ΠΏΠΎ, Π½Π°Π·Π²Π°Π½ΠΈΠ΅ Ρ€Π΅ΠΏΠΎ ΠΈ ΠΏΡƒΡ‚ΡŒ Π΄ΠΎ Ρ„Π°ΠΉΠ»Π°.

ГСнСрация схСмы ΠΊΠΎΠ½Ρ„ΠΈΠ³Π°

Для ΡƒΠ΄ΠΎΠ±Π½ΠΎΠΉ Ρ€Π°Π±ΠΎΡ‚Ρ‹ с ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠΌ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ Π²ΠΎ врСмя Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ рСкомСндуСтся ΡΠ³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Ρ‚ΠΈΠΏ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Π°.

Tuner.use.generateSchema(obj: ObjectType, variableName: string, filePath: string) сформируСт Ρ„Π°ΠΉΠ» ΠΏΠΎ ΠΏΡƒΡ‚ΠΈ filePath со схСмой ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Π° obj ΠΈ экспортируСт Ρ‚ΠΈΠΏ с Π½Π°Π·Π²Π°Π½ΠΈΠ΅ΠΌ variableName, пСрСвСдя ΠΏΠ΅Ρ€Π²ΡƒΡŽ Π±ΡƒΠΊΠ²Ρƒ Π² Π·Π°Π³Π»Π°Π²Π½Ρ‹ΠΉ рСгистр.

const cfg = await Tuner.use.loadConfig();
Tuner.use.generateSchema(
  cfg,
  'config',
  'config/configSchema.ts',
);

Π€Π°ΠΉΠ» config/configSchema.ts

import { z } from 'https://deno.land/x/zod/mod.ts';

export const configSchema = z.object({
  config: z.object({
    a: z.number(),
    b: z.number(),
    c: z.number(),
    e: z.number(),
    d: z.number(),
  }),
  env: z.object({}),
});

export type Config = z.infer<typeof configSchema>;

//β”œβ”€ config
//β”‚  β”œβ”€ a
//β”‚  β”œβ”€ b
//β”‚  β”œβ”€ c
//β”‚  β”œβ”€ e
//β”‚  └─ d
//└─ env
//