From 389de79b5dea4bc35fa05849d44985343f067dc8 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 24 Jun 2025 08:34:48 +0200 Subject: [PATCH 1/8] feat: replace docker compose by run --- src/constants/emulator.constants.ts | 9 ++ src/services/dev/start/docker.services.ts | 149 +++++++++++++++++++++- src/utils/env.utils.ts | 38 ++++++ 3 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 src/constants/emulator.constants.ts diff --git a/src/constants/emulator.constants.ts b/src/constants/emulator.constants.ts new file mode 100644 index 00000000..67d94c03 --- /dev/null +++ b/src/constants/emulator.constants.ts @@ -0,0 +1,9 @@ +import type {EmulatorSkylab} from '@junobuild/config'; + +export const EMULATOR_SKYLAB: Required = { + ports: { + server: 5987, + admin: 5999, + console: 5866 + } +}; diff --git a/src/services/dev/start/docker.services.ts b/src/services/dev/start/docker.services.ts index 71d6b898..af918379 100644 --- a/src/services/dev/start/docker.services.ts +++ b/src/services/dev/start/docker.services.ts @@ -1,19 +1,33 @@ import {nonNullish} from '@dfinity/utils'; import {assertAnswerCtrlC, execute} from '@junobuild/cli-tools'; +import {type EmulatorConfig, type EmulatorPorts} from '@junobuild/config'; import type {PartialConfigFile} from '@junobuild/config-loader'; import {existsSync} from 'node:fs'; import {readFile, writeFile} from 'node:fs/promises'; import {basename, join} from 'node:path'; import prompts from 'prompts'; -import {detectJunoConfigType, junoConfigExist, junoConfigFile} from '../../../configs/juno.config'; +import { + detectJunoConfigType, + junoConfigExist, + junoConfigFile, + readJunoConfig +} from '../../../configs/juno.config'; import { detectJunoDevConfigType, junoDevConfigExist, junoDevConfigFile } from '../../../configs/juno.dev.config'; import {JUNO_DEV_CONFIG_FILENAME} from '../../../constants/constants'; -import {assertDockerRunning, checkDockerVersion} from '../../../utils/env.utils'; +import {EMULATOR_SKYLAB} from '../../../constants/emulator.constants'; +import {ENV} from '../../../env'; +import { + assertDockerRunning, + checkDockerVersion, + hasExistingDockerContainer, + isDockerContainerRunning +} from '../../../utils/env.utils'; import {copyTemplateFile, readTemplateFile} from '../../../utils/fs.utils'; +import {readPackageJson} from '../../../utils/pkg.utils'; import {confirmAndExit} from '../../../utils/prompt.utils'; import {initConfigNoneInteractive, promptConfigType} from '../../init.services'; @@ -34,10 +48,7 @@ export const startContainer = async () => { console.log('🧪 Launching local emulator...'); - await execute({ - command: 'docker', - args: ['compose', 'up'] - }); + await runDocker(); }; export const stop = async () => { @@ -157,3 +168,129 @@ const assertAndInitJunoConfig = async (skylab: boolean) => { await assertJunoDevConfig(); }; + +const runDocker = async () => { + const normalizeDockerName = (pkgName: string): string => + pkgName + .replace(/^@[^/]+\//, '') + .replace(/[^a-zA-Z0-9_.-]/g, '-') + .replace(/^[^a-zA-Z0-9]+/, '') + .toLowerCase(); + + const readProjectName = async (): Promise => { + try { + const {name} = await readPackageJson(); + return name; + } catch (_err: unknown) { + // This should not block the developer therefore we fallback to core which is the common way of using the library + return undefined; + } + }; + + const getEmulatorConfig = async (): Promise => { + if (!(await junoConfigExist())) { + return {skylab: EMULATOR_SKYLAB}; + } + + const config = await readJunoConfig(ENV); + return config?.emulator ?? {skylab: EMULATOR_SKYLAB}; + }; + + const config = await getEmulatorConfig(); + + const emulatorType = + 'satellite' in config ? 'satellite' : 'console' in config ? 'console' : 'skylab'; + + const containerName = normalizeDockerName((await readProjectName()) ?? `juno-${emulatorType}`); + + const result = await isDockerContainerRunning({containerName}); + + if ('err' in result) { + // TODO: show error + return; + } + + if (result.running) { + // TODO: show error already started + return; + } + + const status = await hasExistingDockerContainer({containerName}); + + if ('err' in status) { + // TODO: show error + return; + } + + if (status.exist) { + // Support for Ctrl+C: + // -a: Attach STDOUT/STDERR. Equivalent to `--attach`. + // -i: Keep STDIN open even if not attached. Equivalent to `--interactive`. + await execute({ + command: 'docker', + args: ['start', '-a', '-i', containerName] + }); + return; + } + + const ports: Required = { + server: config[emulatorType]?.ports?.server ?? EMULATOR_SKYLAB.ports.server, + admin: config[emulatorType]?.ports?.admin ?? EMULATOR_SKYLAB.ports.admin + }; + + // Support Ctrl+C: + // -i: Keeps STDIN open for the container. Equivalent to `--interactive`. + // -t: Allocates a pseudo-TTY, enabling terminal-like behavior. Equivalent to `--tty`. + + /** + * Example: + * + * docker run -it \ + * --name juno-skylab-aaabbb \ + * -p 5987:5987 \ + * -p 5999:5999 \ + * -p 5866:5866 \ + * -v juno_skylab_test_61:/juno/.juno \ + * -v "$(pwd)/juno.config.mjs:/juno/juno.config.mjs" \ + * -v "$(pwd)/target/deploy:/juno/target/deploy" \ + * juno-skylab-pocket-ic + */ + + const volume = config.runner?.volume ?? containerName.replaceAll('-', '_'); + + const fn = emulatorType === 'satellite' ? detectJunoDevConfigType : detectJunoConfigType; + const detectedConfig = fn(); + const configFile = nonNullish(detectedConfig?.configPath) + ? basename(detectedConfig.configPath) + : undefined; + const configFilePath = nonNullish(configFile) ? join(process.cwd(), configFile) : undefined; + + const targetDeploy = config.runner?.target ?? join(process.cwd(), 'target', 'deploy'); + + const image = config.runner?.image ?? `junobuild/${emulatorType}:latest`; + + await execute({ + command: 'docker', + args: [ + 'run', + '-it', + '--name', + containerName, + '-p', + `${ports.server}:5987`, + '-p', + `${ports.admin}:5999`, + ...('skylab' in config + ? ['-p', `${config.skylab?.ports?.console ?? EMULATOR_SKYLAB.ports.console}:5866`] + : []), + '-v', + `${volume}:/juno/.juno`, + ...(nonNullish(configFile) && nonNullish(configFilePath) + ? ['-v', `${configFilePath}:/juno/${configFile}`] + : []), + '-v', + `${targetDeploy}:/juno/target/deploy`, + image + ] + }); +}; diff --git a/src/utils/env.utils.ts b/src/utils/env.utils.ts index 5c6c618e..ab8238cd 100644 --- a/src/utils/env.utils.ts +++ b/src/utils/env.utils.ts @@ -119,6 +119,44 @@ export const assertDockerRunning = async () => { } }; +export const isDockerContainerRunning = async ({ + containerName +}: { + containerName: string; +}): Promise<{running: boolean} | {err: unknown}> => { + try { + let output = ''; + await spawn({ + command: 'docker', + args: ['ps', '--quiet', '-f', `name=^/${containerName}$`], + stdout: (o) => (output += o) + }); + + return {running: output.trim().length > 0}; + } catch (err: unknown) { + return {err}; + } +}; + +export const hasExistingDockerContainer = async ({ + containerName +}: { + containerName: string; +}): Promise<{exist: boolean} | {err: unknown}> => { + try { + let output = ''; + await spawn({ + command: 'docker', + args: ['ps', '-aq', '-f', `name=^/${containerName}$`], + stdout: (o) => (output += o) + }); + + return {exist: output.trim().length > 0}; + } catch (err: unknown) { + return {err}; + } +}; + export const checkCargoBinInstalled = async ({ command, args From 1f028d6429268b49050e5941fb528312531e1431 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 24 Jun 2025 08:36:38 +0200 Subject: [PATCH 2/8] feat: ports constants --- src/constants/emulator.constants.ts | 10 +++++++--- src/services/dev/start/docker.services.ts | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/constants/emulator.constants.ts b/src/constants/emulator.constants.ts index 67d94c03..64a63e9f 100644 --- a/src/constants/emulator.constants.ts +++ b/src/constants/emulator.constants.ts @@ -1,9 +1,13 @@ import type {EmulatorSkylab} from '@junobuild/config'; +export const EMULATOR_PORT_SERVER = 5987; +export const EMULATOR_PORT_ADMIN = 5999; +export const EMULATOR_PORT_CONSOLE = 5866; + export const EMULATOR_SKYLAB: Required = { ports: { - server: 5987, - admin: 5999, - console: 5866 + server: EMULATOR_PORT_SERVER, + admin: EMULATOR_PORT_ADMIN, + console: EMULATOR_PORT_CONSOLE } }; diff --git a/src/services/dev/start/docker.services.ts b/src/services/dev/start/docker.services.ts index af918379..b7f95749 100644 --- a/src/services/dev/start/docker.services.ts +++ b/src/services/dev/start/docker.services.ts @@ -18,7 +18,12 @@ import { junoDevConfigFile } from '../../../configs/juno.dev.config'; import {JUNO_DEV_CONFIG_FILENAME} from '../../../constants/constants'; -import {EMULATOR_SKYLAB} from '../../../constants/emulator.constants'; +import { + EMULATOR_PORT_ADMIN, + EMULATOR_PORT_CONSOLE, + EMULATOR_PORT_SERVER, + EMULATOR_SKYLAB +} from '../../../constants/emulator.constants'; import {ENV} from '../../../env'; import { assertDockerRunning, @@ -277,11 +282,14 @@ const runDocker = async () => { '--name', containerName, '-p', - `${ports.server}:5987`, + `${ports.server}:${EMULATOR_PORT_SERVER}`, '-p', - `${ports.admin}:5999`, + `${ports.admin}:${EMULATOR_PORT_ADMIN}`, ...('skylab' in config - ? ['-p', `${config.skylab?.ports?.console ?? EMULATOR_SKYLAB.ports.console}:5866`] + ? [ + '-p', + `${config.skylab?.ports?.console ?? EMULATOR_SKYLAB.ports.console}:${EMULATOR_PORT_CONSOLE}` + ] : []), '-v', `${volume}:/juno/.juno`, From d3867a9439d456890059107187aa8209dc46aeab Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 24 Jun 2025 08:47:21 +0200 Subject: [PATCH 3/8] feat: remove docker compose --- src/configs/juno.dev.config.ts | 3 - src/services/dev/start/docker.services.ts | 80 ++++++------------- templates/docker/docker-compose.satellite.yml | 21 ----- templates/docker/docker-compose.skylab.yml | 24 ------ 4 files changed, 24 insertions(+), 104 deletions(-) delete mode 100644 templates/docker/docker-compose.satellite.yml delete mode 100644 templates/docker/docker-compose.skylab.yml diff --git a/src/configs/juno.dev.config.ts b/src/configs/juno.dev.config.ts index b082a0f1..e13cb804 100644 --- a/src/configs/juno.dev.config.ts +++ b/src/configs/juno.dev.config.ts @@ -2,7 +2,6 @@ import type {ConfigFile, ConfigFilename} from '@junobuild/config-loader'; import { detectJunoConfigType as detectJunoConfigTypeTools, junoConfigExist as junoConfigExistTools, - junoConfigFile as junoConfigFileTools } from '@junobuild/config-loader'; import {JUNO_DEV_CONFIG_FILENAME} from '../constants/constants'; @@ -14,5 +13,3 @@ export const junoDevConfigExist = async (): Promise => { export const detectJunoDevConfigType = (): ConfigFile | undefined => detectJunoConfigTypeTools(JUNO_DEV_CONFIG_FILE); - -export const junoDevConfigFile = (): ConfigFile => junoConfigFileTools(JUNO_DEV_CONFIG_FILE); diff --git a/src/services/dev/start/docker.services.ts b/src/services/dev/start/docker.services.ts index b7f95749..04c8b5c3 100644 --- a/src/services/dev/start/docker.services.ts +++ b/src/services/dev/start/docker.services.ts @@ -2,21 +2,10 @@ import {nonNullish} from '@dfinity/utils'; import {assertAnswerCtrlC, execute} from '@junobuild/cli-tools'; import {type EmulatorConfig, type EmulatorPorts} from '@junobuild/config'; import type {PartialConfigFile} from '@junobuild/config-loader'; -import {existsSync} from 'node:fs'; -import {readFile, writeFile} from 'node:fs/promises'; import {basename, join} from 'node:path'; import prompts from 'prompts'; -import { - detectJunoConfigType, - junoConfigExist, - junoConfigFile, - readJunoConfig -} from '../../../configs/juno.config'; -import { - detectJunoDevConfigType, - junoDevConfigExist, - junoDevConfigFile -} from '../../../configs/juno.dev.config'; +import {detectJunoConfigType, junoConfigExist, readJunoConfig} from '../../../configs/juno.config'; +import {detectJunoDevConfigType, junoDevConfigExist} from '../../../configs/juno.dev.config'; import {JUNO_DEV_CONFIG_FILENAME} from '../../../constants/constants'; import { EMULATOR_PORT_ADMIN, @@ -31,14 +20,13 @@ import { hasExistingDockerContainer, isDockerContainerRunning } from '../../../utils/env.utils'; -import {copyTemplateFile, readTemplateFile} from '../../../utils/fs.utils'; +import {copyTemplateFile} from '../../../utils/fs.utils'; import {readPackageJson} from '../../../utils/pkg.utils'; import {confirmAndExit} from '../../../utils/prompt.utils'; import {initConfigNoneInteractive, promptConfigType} from '../../init.services'; const TEMPLATE_PATH = '../templates/docker'; const DESTINATION_PATH = process.cwd(); -const DOCKER_COMPOSE_FILENAME = 'docker-compose.yml'; export const startContainer = async () => { const {valid} = await checkDockerVersion(); @@ -71,11 +59,7 @@ export const stop = async () => { }); }; -const assertJunoDevConfig = async () => { - if (await junoDevConfigExist()) { - return; - } - +const initJunoDevConfigFile = async () => { await confirmAndExit( `A config file is required for development. Would you like the CLI to create one for you?` ); @@ -90,11 +74,7 @@ const assertJunoDevConfig = async () => { }); }; -const assertJunoConfig = async () => { - if (await junoConfigExist()) { - return; - } - +const initJunoConfigFile = async () => { await confirmAndExit(`Your project needs a config file for Juno. Should we create one now?`); await initConfigNoneInteractive(); @@ -116,10 +96,10 @@ const buildConfigType = async (context: ConfigContext): Promise { - const {image}: {image: 'skylab' | 'satellite' | undefined} = await prompts({ +const promptEmulatorType = async (): Promise<{emulatorType: 'skylab' | 'satellite'}> => { + const {emulatorType}: {emulatorType: 'skylab' | 'satellite' | undefined} = await prompts({ type: 'select', - name: 'image', + name: 'emulatorType', message: 'What kind of emulator would you like to run locally?', choices: [ { @@ -130,48 +110,36 @@ const assertDockerCompose = async () => { ] }); - assertAnswerCtrlC(image); - - const template = await readTemplateFile({ - template: `docker-compose.${image}.yml`, - sourceFolder: TEMPLATE_PATH - }); + assertAnswerCtrlC(emulatorType); - // We should assert the config before creating the docker file otherwise we cannot know if the docker file should reference a TS, JS or JSON config file. - await assertAndInitJunoConfig(image === 'skylab'); - - const readConfig = image === 'satellite' ? junoDevConfigFile : junoConfigFile; - const {configPath} = readConfig(); - const configFile = basename(configPath); - - const content = template - .replaceAll('', configFile) - .replaceAll('', configFile); - - await writeFile(join(DESTINATION_PATH, DOCKER_COMPOSE_FILENAME), content, 'utf-8'); - - return {dockerImage: image}; + return {emulatorType}; }; const assertAndInitConfig = async () => { - if (existsSync(DOCKER_COMPOSE_FILENAME)) { - const dockerCompose = await readFile(DOCKER_COMPOSE_FILENAME, 'utf-8'); - const isSkylab = /image:\s*junobuild\/skylab(:[^\s]*)?/.test(dockerCompose); + const configExist = await junoConfigExist(); - await assertAndInitJunoConfig(isSkylab); + if (configExist) { return; } - await assertDockerCompose(); + const configDevExist = await junoDevConfigExist(); + + if (configDevExist) { + return; + } + + const {emulatorType} = await promptEmulatorType(); + + await initConfigFile(emulatorType === 'skylab'); }; -const assertAndInitJunoConfig = async (skylab: boolean) => { +const initConfigFile = async (skylab: boolean) => { if (skylab) { - await assertJunoConfig(); + await initJunoConfigFile(); return; } - await assertJunoDevConfig(); + await initJunoDevConfigFile(); }; const runDocker = async () => { diff --git a/templates/docker/docker-compose.satellite.yml b/templates/docker/docker-compose.satellite.yml deleted file mode 100644 index b997a2fc..00000000 --- a/templates/docker/docker-compose.satellite.yml +++ /dev/null @@ -1,21 +0,0 @@ -services: - juno-satellite: - image: junobuild/satellite:latest - ports: - # Local replica used to simulate execution - - 5987:5987 - # Little admin server (e.g. to transfer ICP from the ledger) - - 5999:5999 - volumes: - # Persistent volume to store internal state - - juno_satellite:/juno/.juno - # Local dev config file to customize Satellite behavior - - ./:/juno/ - # Shared folder for deploying and hot-reloading serverless functions - # For example, when building functions in TypeScript, the output `.mjs` files are placed here. - # The container then bundles them into your Satellite WASM (also placed here), - # and automatically upgrades the environment. - - ./target/deploy:/juno/target/deploy/ - -volumes: - juno_satellite: diff --git a/templates/docker/docker-compose.skylab.yml b/templates/docker/docker-compose.skylab.yml deleted file mode 100644 index 5901f60b..00000000 --- a/templates/docker/docker-compose.skylab.yml +++ /dev/null @@ -1,24 +0,0 @@ -services: - juno-skylab: - image: junobuild/skylab:latest - ports: - # Local replica used to simulate execution - - 5987:5987 - # Little admin server (e.g. to transfer ICP from the ledger) - - 5999:5999 - # Console UI (like https://console.juno.build) - - 5866:5866 - volumes: - # Persistent volume to store internal state - - juno_skylab:/juno/.juno - # Your Juno configuration file. - # Notably used to provide your development Satellite ID to the emulator. - - ./:/juno/ - # Shared folder for deploying and hot-reloading serverless functions - # For example, when building functions in TypeScript, the output `.mjs` files are placed here. - # The container then bundles them into your Satellite WASM (also placed here), - # and automatically upgrades the environment. - - ./target/deploy:/juno/target/deploy/ - -volumes: - juno_skylab: From 211a156ebaeb692c280c39c7261f9a8607db14e7 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 24 Jun 2025 08:56:15 +0200 Subject: [PATCH 4/8] feat: init config --- src/constants/emulator.constants.ts | 9 +++++++- src/services/dev/start/docker.services.ts | 27 +++++++++++++++-------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/constants/emulator.constants.ts b/src/constants/emulator.constants.ts index 64a63e9f..58149860 100644 --- a/src/constants/emulator.constants.ts +++ b/src/constants/emulator.constants.ts @@ -1,4 +1,4 @@ -import type {EmulatorSkylab} from '@junobuild/config'; +import type {EmulatorSatellite, EmulatorSkylab} from '@junobuild/config'; export const EMULATOR_PORT_SERVER = 5987; export const EMULATOR_PORT_ADMIN = 5999; @@ -11,3 +11,10 @@ export const EMULATOR_SKYLAB: Required = { console: EMULATOR_PORT_CONSOLE } }; + +export const EMULATOR_SATELLITE: Required = { + ports: { + server: EMULATOR_PORT_SERVER, + admin: EMULATOR_PORT_ADMIN + } +}; diff --git a/src/services/dev/start/docker.services.ts b/src/services/dev/start/docker.services.ts index 04c8b5c3..4c63f35c 100644 --- a/src/services/dev/start/docker.services.ts +++ b/src/services/dev/start/docker.services.ts @@ -11,6 +11,7 @@ import { EMULATOR_PORT_ADMIN, EMULATOR_PORT_CONSOLE, EMULATOR_PORT_SERVER, + EMULATOR_SATELLITE, EMULATOR_SKYLAB } from '../../../constants/emulator.constants'; import {ENV} from '../../../env'; @@ -60,6 +61,10 @@ export const stop = async () => { }; const initJunoDevConfigFile = async () => { + if (await junoDevConfigExist()) { + return; + } + await confirmAndExit( `A config file is required for development. Would you like the CLI to create one for you?` ); @@ -122,20 +127,17 @@ const assertAndInitConfig = async () => { return; } - const configDevExist = await junoDevConfigExist(); - - if (configDevExist) { - return; - } - - const {emulatorType} = await promptEmulatorType(); + const {emulatorType} = (await junoDevConfigExist()) + ? {emulatorType: 'satellite'} + : await promptEmulatorType(); await initConfigFile(emulatorType === 'skylab'); }; const initConfigFile = async (skylab: boolean) => { + await initJunoConfigFile(); + if (skylab) { - await initJunoConfigFile(); return; } @@ -161,10 +163,17 @@ const runDocker = async () => { }; const getEmulatorConfig = async (): Promise => { - if (!(await junoConfigExist())) { + const configExist = await junoConfigExist(); + const devConfigExist = await junoDevConfigExist(); + + if (!configExist && !devConfigExist) { return {skylab: EMULATOR_SKYLAB}; } + if (!configExist && devConfigExist) { + return {satellite: EMULATOR_SATELLITE}; + } + const config = await readJunoConfig(ENV); return config?.emulator ?? {skylab: EMULATOR_SKYLAB}; }; From aa848133792fc18ff1cf5377725587b31a102957 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 24 Jun 2025 08:57:21 +0200 Subject: [PATCH 5/8] chore: lint --- src/configs/juno.dev.config.ts | 2 +- src/services/dev/start/docker.services.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/configs/juno.dev.config.ts b/src/configs/juno.dev.config.ts index e13cb804..9fdc6e48 100644 --- a/src/configs/juno.dev.config.ts +++ b/src/configs/juno.dev.config.ts @@ -1,7 +1,7 @@ import type {ConfigFile, ConfigFilename} from '@junobuild/config-loader'; import { detectJunoConfigType as detectJunoConfigTypeTools, - junoConfigExist as junoConfigExistTools, + junoConfigExist as junoConfigExistTools } from '@junobuild/config-loader'; import {JUNO_DEV_CONFIG_FILENAME} from '../constants/constants'; diff --git a/src/services/dev/start/docker.services.ts b/src/services/dev/start/docker.services.ts index 4c63f35c..8d832466 100644 --- a/src/services/dev/start/docker.services.ts +++ b/src/services/dev/start/docker.services.ts @@ -175,7 +175,7 @@ const runDocker = async () => { } const config = await readJunoConfig(ENV); - return config?.emulator ?? {skylab: EMULATOR_SKYLAB}; + return config.emulator ?? {skylab: EMULATOR_SKYLAB}; }; const config = await getEmulatorConfig(); @@ -265,7 +265,7 @@ const runDocker = async () => { ...('skylab' in config ? [ '-p', - `${config.skylab?.ports?.console ?? EMULATOR_SKYLAB.ports.console}:${EMULATOR_PORT_CONSOLE}` + `${config.skylab.ports?.console ?? EMULATOR_SKYLAB.ports.console}:${EMULATOR_PORT_CONSOLE}` ] : []), '-v', From 0fb90736837e065c3cd2980408eb63b5f432ac35 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 24 Jun 2025 09:20:20 +0200 Subject: [PATCH 6/8] fix: read config --- src/configs/juno.dev.config.ts | 5 ++++- src/services/dev/start/docker.services.ts | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/configs/juno.dev.config.ts b/src/configs/juno.dev.config.ts index 9fdc6e48..b082a0f1 100644 --- a/src/configs/juno.dev.config.ts +++ b/src/configs/juno.dev.config.ts @@ -1,7 +1,8 @@ import type {ConfigFile, ConfigFilename} from '@junobuild/config-loader'; import { detectJunoConfigType as detectJunoConfigTypeTools, - junoConfigExist as junoConfigExistTools + junoConfigExist as junoConfigExistTools, + junoConfigFile as junoConfigFileTools } from '@junobuild/config-loader'; import {JUNO_DEV_CONFIG_FILENAME} from '../constants/constants'; @@ -13,3 +14,5 @@ export const junoDevConfigExist = async (): Promise => { export const detectJunoDevConfigType = (): ConfigFile | undefined => detectJunoConfigTypeTools(JUNO_DEV_CONFIG_FILE); + +export const junoDevConfigFile = (): ConfigFile => junoConfigFileTools(JUNO_DEV_CONFIG_FILE); diff --git a/src/services/dev/start/docker.services.ts b/src/services/dev/start/docker.services.ts index 8d832466..8f61164f 100644 --- a/src/services/dev/start/docker.services.ts +++ b/src/services/dev/start/docker.services.ts @@ -1,11 +1,11 @@ import {nonNullish} from '@dfinity/utils'; import {assertAnswerCtrlC, execute} from '@junobuild/cli-tools'; -import {type EmulatorConfig, type EmulatorPorts} from '@junobuild/config'; +import {type EmulatorConfig, EmulatorConfigSchema, type EmulatorPorts} from '@junobuild/config'; import type {PartialConfigFile} from '@junobuild/config-loader'; import {basename, join} from 'node:path'; import prompts from 'prompts'; -import {detectJunoConfigType, junoConfigExist, readJunoConfig} from '../../../configs/juno.config'; -import {detectJunoDevConfigType, junoDevConfigExist} from '../../../configs/juno.dev.config'; +import {detectJunoConfigType, junoConfigExist, junoConfigFile, readJunoConfig} from '../../../configs/juno.config'; +import {detectJunoDevConfigType, junoDevConfigExist, junoDevConfigFile} from '../../../configs/juno.dev.config'; import {JUNO_DEV_CONFIG_FILENAME} from '../../../constants/constants'; import { EMULATOR_PORT_ADMIN, @@ -180,10 +180,17 @@ const runDocker = async () => { const config = await getEmulatorConfig(); + const {success} = EmulatorConfigSchema.safeParse(config); + if (!success) { + // TODO + console.log('Not valid'); + return; + } + const emulatorType = 'satellite' in config ? 'satellite' : 'console' in config ? 'console' : 'skylab'; - const containerName = normalizeDockerName((await readProjectName()) ?? `juno-${emulatorType}`); + const containerName = normalizeDockerName((config?.runner?.name ?? await readProjectName()) ?? `juno-${emulatorType}`); const result = await isDockerContainerRunning({containerName}); @@ -240,7 +247,7 @@ const runDocker = async () => { const volume = config.runner?.volume ?? containerName.replaceAll('-', '_'); - const fn = emulatorType === 'satellite' ? detectJunoDevConfigType : detectJunoConfigType; + const fn = emulatorType === 'satellite' ? junoDevConfigFile : junoConfigFile; const detectedConfig = fn(); const configFile = nonNullish(detectedConfig?.configPath) ? basename(detectedConfig.configPath) From a3360e237a2adeee7ad2cc7492413fcc69b7e8e3 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 24 Jun 2025 12:39:30 +0200 Subject: [PATCH 7/8] feat: stop --- src/services/dev/start/docker.services.ts | 185 +++++++++++++++------- src/utils/env.utils.ts | 16 +- 2 files changed, 135 insertions(+), 66 deletions(-) diff --git a/src/services/dev/start/docker.services.ts b/src/services/dev/start/docker.services.ts index 8f61164f..87d547c7 100644 --- a/src/services/dev/start/docker.services.ts +++ b/src/services/dev/start/docker.services.ts @@ -1,11 +1,20 @@ import {nonNullish} from '@dfinity/utils'; -import {assertAnswerCtrlC, execute} from '@junobuild/cli-tools'; +import {assertAnswerCtrlC, execute, spawn} from '@junobuild/cli-tools'; import {type EmulatorConfig, EmulatorConfigSchema, type EmulatorPorts} from '@junobuild/config'; import type {PartialConfigFile} from '@junobuild/config-loader'; import {basename, join} from 'node:path'; import prompts from 'prompts'; -import {detectJunoConfigType, junoConfigExist, junoConfigFile, readJunoConfig} from '../../../configs/juno.config'; -import {detectJunoDevConfigType, junoDevConfigExist, junoDevConfigFile} from '../../../configs/juno.dev.config'; +import { + detectJunoConfigType, + junoConfigExist, + junoConfigFile, + readJunoConfig +} from '../../../configs/juno.config'; +import { + detectJunoDevConfigType, + junoDevConfigExist, + junoDevConfigFile +} from '../../../configs/juno.dev.config'; import {JUNO_DEV_CONFIG_FILENAME} from '../../../constants/constants'; import { EMULATOR_PORT_ADMIN, @@ -25,6 +34,8 @@ import {copyTemplateFile} from '../../../utils/fs.utils'; import {readPackageJson} from '../../../utils/pkg.utils'; import {confirmAndExit} from '../../../utils/prompt.utils'; import {initConfigNoneInteractive, promptConfigType} from '../../init.services'; +import {red, yellow} from 'kleur'; +import {displaySegment} from '../../../utils/display.utils'; const TEMPLATE_PATH = '../templates/docker'; const DESTINATION_PATH = process.cwd(); @@ -42,7 +53,7 @@ export const startContainer = async () => { console.log('🧪 Launching local emulator...'); - await runDocker(); + await startEmulator(); }; export const stop = async () => { @@ -54,10 +65,7 @@ export const stop = async () => { await assertDockerRunning(); - await execute({ - command: 'docker', - args: ['compose', 'stop'] - }); + await stopEmulator(); }; const initJunoDevConfigFile = async () => { @@ -144,70 +152,26 @@ const initConfigFile = async (skylab: boolean) => { await initJunoDevConfigFile(); }; -const runDocker = async () => { - const normalizeDockerName = (pkgName: string): string => - pkgName - .replace(/^@[^/]+\//, '') - .replace(/[^a-zA-Z0-9_.-]/g, '-') - .replace(/^[^a-zA-Z0-9]+/, '') - .toLowerCase(); - - const readProjectName = async (): Promise => { - try { - const {name} = await readPackageJson(); - return name; - } catch (_err: unknown) { - // This should not block the developer therefore we fallback to core which is the common way of using the library - return undefined; - } - }; - - const getEmulatorConfig = async (): Promise => { - const configExist = await junoConfigExist(); - const devConfigExist = await junoDevConfigExist(); - - if (!configExist && !devConfigExist) { - return {skylab: EMULATOR_SKYLAB}; - } - - if (!configExist && devConfigExist) { - return {satellite: EMULATOR_SATELLITE}; - } - - const config = await readJunoConfig(ENV); - return config.emulator ?? {skylab: EMULATOR_SKYLAB}; - }; +const startEmulator = async () => { + const parsedResult = await parseEmulatorConfig(); - const config = await getEmulatorConfig(); - - const {success} = EmulatorConfigSchema.safeParse(config); - if (!success) { - // TODO - console.log('Not valid'); + if (!parsedResult.success) { return; } - const emulatorType = - 'satellite' in config ? 'satellite' : 'console' in config ? 'console' : 'skylab'; - - const containerName = normalizeDockerName((config?.runner?.name ?? await readProjectName()) ?? `juno-${emulatorType}`); - - const result = await isDockerContainerRunning({containerName}); + const {containerName, config, emulatorType} = parsedResult; - if ('err' in result) { - // TODO: show error - return; - } + const {running} = await assertDockerContainerRunning({containerName}); - if (result.running) { - // TODO: show error already started + if (running) { + console.log(yellow(`The Docker container ${containerName} is already running.`)); return; } const status = await hasExistingDockerContainer({containerName}); if ('err' in status) { - // TODO: show error + console.log(red(`Unable to check if Docker container ${containerName} already exists.`)); return; } @@ -286,3 +250,104 @@ const runDocker = async () => { ] }); }; + +const stopEmulator = async () => { + const parsedResult = await parseEmulatorConfig(); + + if (!parsedResult.success) { + return; + } + + const {containerName} = parsedResult; + + const {running} = await assertDockerContainerRunning({containerName}); + + if (!running) { + console.log(yellow(`The Docker container ${containerName} is already stopped.`)); + return; + } + + await spawn({ + command: 'docker', + args: ['stop', containerName], + silentOut: true + }); +} + +const parseEmulatorConfig = async (): Promise< + | { + success: true; + config: EmulatorConfig; + containerName: string; + emulatorType: 'skylab' | 'satellite' | 'console'; + } + | {success: false} +> => { + const normalizeDockerName = (pkgName: string): string => + pkgName + .replace(/^@[^/]+\//, '') + .replace(/[^a-zA-Z0-9_.-]/g, '-') + .replace(/^[^a-zA-Z0-9]+/, '') + .toLowerCase(); + + const readProjectName = async (): Promise => { + try { + const {name} = await readPackageJson(); + return name; + } catch (_err: unknown) { + // This should not block the developer therefore we fallback to core which is the common way of using the library + return undefined; + } + }; + + const getEmulatorConfig = async (): Promise => { + const configExist = await junoConfigExist(); + const devConfigExist = await junoDevConfigExist(); + + if (!configExist && !devConfigExist) { + return {skylab: EMULATOR_SKYLAB}; + } + + if (!configExist && devConfigExist) { + return {satellite: EMULATOR_SATELLITE}; + } + + const config = await readJunoConfig(ENV); + return config.emulator ?? {skylab: EMULATOR_SKYLAB}; + }; + + const config = await getEmulatorConfig(); + + const {success} = EmulatorConfigSchema.safeParse(config); + if (!success) { + // TODO + console.log('Not valid'); + return {success: false}; + } + + const emulatorType = + 'satellite' in config ? 'satellite' : 'console' in config ? 'console' : 'skylab'; + + const containerName = normalizeDockerName( + config?.runner?.name ?? (await readProjectName()) ?? `juno-${emulatorType}` + ); + + return { + success: true, + config, + containerName, + emulatorType + }; +}; + +const assertDockerContainerRunning = async ({containerName}: {containerName: string}): Promise<{running: boolean}> => { + const result = await isDockerContainerRunning({containerName}); + + if ('err' in result) { + console.log(red(`Unable to verify if container ${containerName} is running. Is Docker installed?`)); + process.exit(1); + } + + return result; +}; + diff --git a/src/utils/env.utils.ts b/src/utils/env.utils.ts index ab8238cd..fb362314 100644 --- a/src/utils/env.utils.ts +++ b/src/utils/env.utils.ts @@ -1,5 +1,5 @@ import {execute, spawn} from '@junobuild/cli-tools'; -import {green, yellow} from 'kleur'; +import {green, red, yellow} from 'kleur'; import {lt, major} from 'semver'; import {NODE_VERSION} from '../constants/constants'; import { @@ -101,7 +101,7 @@ export const checkDockerVersion = async (): Promise<{valid: boolean | 'error'}> return {valid: false}; } } catch (_e: unknown) { - console.error(`Cannot detect Docker version. Is Docker installed on your machine?`); + console.log(`${red("Cannot detect Docker version.")} Is Docker installed on your machine?`); return {valid: 'error'}; } @@ -110,11 +110,13 @@ export const checkDockerVersion = async (): Promise<{valid: boolean | 'error'}> export const assertDockerRunning = async () => { try { - await execute({ + await spawn({ command: 'docker', - args: ['ps', '--quiet'] + args: ['ps', '--quiet'], + silentOut: true }); } catch (_e: unknown) { + console.log(red("Docker does not appear to be running.")); process.exit(1); } }; @@ -129,7 +131,8 @@ export const isDockerContainerRunning = async ({ await spawn({ command: 'docker', args: ['ps', '--quiet', '-f', `name=^/${containerName}$`], - stdout: (o) => (output += o) + stdout: (o) => (output += o), + silentOut: true }); return {running: output.trim().length > 0}; @@ -148,7 +151,8 @@ export const hasExistingDockerContainer = async ({ await spawn({ command: 'docker', args: ['ps', '-aq', '-f', `name=^/${containerName}$`], - stdout: (o) => (output += o) + stdout: (o) => (output += o), + silentOut: true }); return {exist: output.trim().length > 0}; From 4f88f04faf5e4be83e9f1db46adf19f94c659efd Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 24 Jun 2025 12:41:53 +0200 Subject: [PATCH 8/8] chore: lint --- src/services/dev/start/docker.services.ts | 20 ++++++++++++-------- src/utils/env.utils.ts | 6 +++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/services/dev/start/docker.services.ts b/src/services/dev/start/docker.services.ts index 87d547c7..e6ef6b19 100644 --- a/src/services/dev/start/docker.services.ts +++ b/src/services/dev/start/docker.services.ts @@ -2,6 +2,7 @@ import {nonNullish} from '@dfinity/utils'; import {assertAnswerCtrlC, execute, spawn} from '@junobuild/cli-tools'; import {type EmulatorConfig, EmulatorConfigSchema, type EmulatorPorts} from '@junobuild/config'; import type {PartialConfigFile} from '@junobuild/config-loader'; +import {red, yellow} from 'kleur'; import {basename, join} from 'node:path'; import prompts from 'prompts'; import { @@ -34,8 +35,6 @@ import {copyTemplateFile} from '../../../utils/fs.utils'; import {readPackageJson} from '../../../utils/pkg.utils'; import {confirmAndExit} from '../../../utils/prompt.utils'; import {initConfigNoneInteractive, promptConfigType} from '../../init.services'; -import {red, yellow} from 'kleur'; -import {displaySegment} from '../../../utils/display.utils'; const TEMPLATE_PATH = '../templates/docker'; const DESTINATION_PATH = process.cwd(); @@ -213,7 +212,7 @@ const startEmulator = async () => { const fn = emulatorType === 'satellite' ? junoDevConfigFile : junoConfigFile; const detectedConfig = fn(); - const configFile = nonNullish(detectedConfig?.configPath) + const configFile = nonNullish(detectedConfig.configPath) ? basename(detectedConfig.configPath) : undefined; const configFilePath = nonNullish(configFile) ? join(process.cwd(), configFile) : undefined; @@ -272,7 +271,7 @@ const stopEmulator = async () => { args: ['stop', containerName], silentOut: true }); -} +}; const parseEmulatorConfig = async (): Promise< | { @@ -329,7 +328,7 @@ const parseEmulatorConfig = async (): Promise< 'satellite' in config ? 'satellite' : 'console' in config ? 'console' : 'skylab'; const containerName = normalizeDockerName( - config?.runner?.name ?? (await readProjectName()) ?? `juno-${emulatorType}` + config.runner?.name ?? (await readProjectName()) ?? `juno-${emulatorType}` ); return { @@ -340,14 +339,19 @@ const parseEmulatorConfig = async (): Promise< }; }; -const assertDockerContainerRunning = async ({containerName}: {containerName: string}): Promise<{running: boolean}> => { +const assertDockerContainerRunning = async ({ + containerName +}: { + containerName: string; +}): Promise<{running: boolean}> => { const result = await isDockerContainerRunning({containerName}); if ('err' in result) { - console.log(red(`Unable to verify if container ${containerName} is running. Is Docker installed?`)); + console.log( + red(`Unable to verify if container ${containerName} is running. Is Docker installed?`) + ); process.exit(1); } return result; }; - diff --git a/src/utils/env.utils.ts b/src/utils/env.utils.ts index fb362314..4a048c39 100644 --- a/src/utils/env.utils.ts +++ b/src/utils/env.utils.ts @@ -1,4 +1,4 @@ -import {execute, spawn} from '@junobuild/cli-tools'; +import {spawn} from '@junobuild/cli-tools'; import {green, red, yellow} from 'kleur'; import {lt, major} from 'semver'; import {NODE_VERSION} from '../constants/constants'; @@ -101,7 +101,7 @@ export const checkDockerVersion = async (): Promise<{valid: boolean | 'error'}> return {valid: false}; } } catch (_e: unknown) { - console.log(`${red("Cannot detect Docker version.")} Is Docker installed on your machine?`); + console.log(`${red('Cannot detect Docker version.')} Is Docker installed on your machine?`); return {valid: 'error'}; } @@ -116,7 +116,7 @@ export const assertDockerRunning = async () => { silentOut: true }); } catch (_e: unknown) { - console.log(red("Docker does not appear to be running.")); + console.log(red('Docker does not appear to be running.')); process.exit(1); } };