diff --git a/src/commands/init.ts b/src/commands/init.ts index 7ac35072..d2c76a6e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,24 +1,11 @@ -import {isNullish, nonNullish} from '@dfinity/utils'; -import {assertAnswerCtrlC, hasArgs} from '@junobuild/cli-tools'; -import type {PartialConfigFile} from '@junobuild/config-loader'; -import {cyan, yellow} from 'kleur'; -import {unlink} from 'node:fs/promises'; -import {basename} from 'node:path'; -import prompts from 'prompts'; -import {getCliOrbiters, getCliSatellites, getToken} from '../configs/cli.config'; -import { - detectJunoConfigType, - junoConfigExist, - junoConfigFile, - writeJunoConfig, - writeJunoConfigPlaceholder -} from '../configs/juno.config'; -import {promptConfigType} from '../services/init.services'; +import {isNullish} from '@dfinity/utils'; +import {hasArgs} from '@junobuild/cli-tools'; +import {cyan} from 'kleur'; +import {getToken} from '../configs/cli.config'; +import {junoConfigExist} from '../configs/juno.config'; +import {initConfigInteractive, initConfigNoneInteractive} from '../services/init.services'; import {login as consoleLogin} from '../services/login.services'; -import type {CliOrbiterConfig, CliSatelliteConfig} from '../types/cli.config'; -import type {PackageManager} from '../types/pm'; -import {detectPackageManager} from '../utils/pm.utils'; -import {NEW_CMD_LINE, confirm, confirmAndExit} from '../utils/prompt.utils'; +import {confirm, confirmAndExit} from '../utils/prompt.utils'; export const init = async (args?: string[]) => { if (hasArgs({args, options: ['-m', '--minimal']})) { @@ -62,206 +49,3 @@ const assertOverwrite = async () => { ); } }; - -const initConfigNoneInteractive = async () => { - const writeFn = async ({source, ...rest}: InitConfigParams) => { - await writeJunoConfigPlaceholder({ - ...rest, - config: { - satellite: {source} - } - }); - }; - - await initConfig({ - writeFn - }); -}; - -const initConfigInteractive = async () => { - const satelliteId = await initSatelliteConfig(); - const orbiterId = await initOrbiterConfig(); - - const writeFn = async ({source, ...rest}: InitConfigParams) => { - await writeJunoConfig({ - ...rest, - config: { - satellite: {id: satelliteId, source}, - ...(nonNullish(orbiterId) && {orbiter: {id: orbiterId}}) - } - }); - }; - - await initConfig({ - writeFn - }); -}; - -type InitConfigParams = PartialConfigFile & {pm: PackageManager | undefined} & {source: string}; - -const initConfig = async ({writeFn}: {writeFn: (params: InitConfigParams) => Promise}) => { - const pm = detectPackageManager(); - - const source = await promptSource({pm}); - - const {configType, configPath: originalConfigPath} = await initConfigType(); - - await writeFn({ - configType, - configPath: originalConfigPath, - pm, - source - }); - - // We delete the deprecated juno.json, which is now replaced with juno.config.json|ts|js, as just created above. - // The developer was prompted about overwriting the configuration previously. - if (nonNullish(originalConfigPath) && basename(originalConfigPath) === 'juno.json') { - await unlink(originalConfigPath); - } - - if (configType === 'json') { - return; - } - - showConfigTips({pm}); -}; - -const showConfigTips = ({pm}: {pm: PackageManager | undefined}) => { - const cmd = `${pm ?? 'npm'} ${isNullish(pm) || pm === 'npm' ? 'i' : 'add'} @junobuild/config -D`; - - console.log( - `${NEW_CMD_LINE}๐Ÿ’ก You can leverage your IDE's intellisense by installing the library: ${yellow(cmd)}${NEW_CMD_LINE}` - ); -}; - -const initSatelliteConfig = async (): Promise => { - const satellites = await getCliSatellites(); - - const satellite = await (satellites.length > 0 - ? promptSatellites(satellites) - : promptSatellite()); - - if (satellite === '_manual_') { - return await promptSatellite(); - } - - return satellite; -}; - -const initOrbiterConfig = async (): Promise => { - const authOrbiters = await getCliOrbiters(); - - if (authOrbiters === undefined || authOrbiters.length === 0) { - return undefined; - } - - const orbiter = await promptOrbiters(authOrbiters); - - if (orbiter === '_none_') { - return undefined; - } - - return orbiter; -}; - -const promptSatellites = async (satellites: CliSatelliteConfig[]): Promise => { - const {satellite}: {satellite: string} = await prompts({ - type: 'select', - name: 'satellite', - message: 'Which satellite should be linked with this dapp?', - choices: [ - ...satellites.map(({p, n}) => ({title: n, value: p})), - {title: '', value: '_manual_'} - ], - initial: 0 - }); - - // In case of control+c - assertAnswerCtrlC(satellite); - - return satellite; -}; - -const initConfigType = async (): Promise => { - if (!(await junoConfigExist())) { - // We try to automatically detect if we should create a TypeScript or JavaScript (mjs) configuration. - const detectedConfig = detectJunoConfigType(); - - if (nonNullish(detectedConfig)) { - return detectedConfig; - } - - const configType = await promptConfigType(); - return {configType}; - } - - return junoConfigFile(); -}; - -const promptSatellite = async (): Promise => { - const {satellite}: {satellite: string} = await prompts([ - { - type: 'text', - name: 'satellite', - message: `What's the ${cyan('id')} of your satellite?` - } - ]); - - assertAnswerCtrlC(satellite, 'The satellite ID is mandatory'); - - return satellite; -}; - -const promptSource = async ({pm}: {pm: PackageManager | undefined}): Promise => { - const cmd = `${pm ?? 'npm'}${isNullish(pm) || pm === 'npm' ? ' run' : ''} build`; - - const {output}: {output: string} = await prompts({ - type: 'select', - name: 'output', - message: `What is the output folder of your \`${cmd}\` command?`, - choices: [ - {title: 'build', value: 'build'}, - {title: 'dist', value: 'dist'}, - {title: 'out', value: 'out'}, - {title: '', value: '_manual_'} - ], - initial: 0 - }); - - // In case of control+c - assertAnswerCtrlC(output); - - if (output !== '_manual_') { - return output; - } - - const {source}: {source: string} = await prompts([ - { - type: 'text', - name: 'source', - message: 'Please enter the name of your output folder' - } - ]); - - assertAnswerCtrlC(source); - - return source; -}; - -const promptOrbiters = async (orbiters: CliOrbiterConfig[]): Promise => { - const {orbiter}: {orbiter: string} = await prompts({ - type: 'select', - name: 'orbiter', - message: 'Which orbiter do you use for the analytics in this dapp?', - choices: [ - ...orbiters.map(({p, n}) => ({title: n ?? p, value: p})), - {title: '', value: '_none_'} - ], - initial: 0 - }); - - // In case of control+c - assertAnswerCtrlC(orbiter); - - return orbiter; -}; diff --git a/src/configs/juno.config.ts b/src/configs/juno.config.ts index 1a936971..c35d2a88 100644 --- a/src/configs/juno.config.ts +++ b/src/configs/juno.config.ts @@ -94,7 +94,7 @@ export const writeJunoConfig = async ({ }); const content = template - .replace('', id) + .replace('', id) .replace('', source ?? DEPLOY_DEFAULT_SOURCE) .replace('', pm === 'npm' ? 'npm run' : (pm ?? '')) .replace('', orbiter?.id ?? ''); diff --git a/src/services/init.services.ts b/src/services/init.services.ts index da15f95a..f4f3b550 100644 --- a/src/services/init.services.ts +++ b/src/services/init.services.ts @@ -1,6 +1,22 @@ +import {isNullish, nonNullish} from '@dfinity/utils'; import {assertAnswerCtrlC} from '@junobuild/cli-tools'; -import type {ConfigType} from '@junobuild/config-loader'; +import type {ConfigType, PartialConfigFile} from '@junobuild/config-loader'; +import {cyan, yellow} from 'kleur'; +import {unlink} from 'node:fs/promises'; +import {basename} from 'node:path'; import prompts from 'prompts'; +import {getCliOrbiters, getCliSatellites} from '../configs/cli.config'; +import { + detectJunoConfigType, + junoConfigExist, + junoConfigFile, + writeJunoConfig, + writeJunoConfigPlaceholder +} from '../configs/juno.config'; +import type {CliOrbiterConfig, CliSatelliteConfig} from '../types/cli.config'; +import type {PackageManager} from '../types/pm'; +import {detectPackageManager} from '../utils/pm.utils'; +import {NEW_CMD_LINE} from '../utils/prompt.utils'; export const promptConfigType = async (): Promise => { const {configType}: {configType: ConfigType} = await prompts({ @@ -20,3 +36,208 @@ export const promptConfigType = async (): Promise => { return configType; }; + +export type InitConfigParams = PartialConfigFile & {pm: PackageManager | undefined} & { + source: string; +}; + +export const initConfigNoneInteractive = async () => { + const writeFn = async ({source, ...rest}: InitConfigParams) => { + await writeJunoConfigPlaceholder({ + ...rest, + config: { + satellite: {source} + } + }); + }; + + await initConfig({ + writeFn + }); +}; + +export const initConfigInteractive = async () => { + const satelliteId = await initSatelliteConfig(); + const orbiterId = await initOrbiterConfig(); + + const writeFn = async ({source, ...rest}: InitConfigParams) => { + await writeJunoConfig({ + ...rest, + config: { + satellite: {id: satelliteId, source}, + ...(nonNullish(orbiterId) && {orbiter: {id: orbiterId}}) + } + }); + }; + + await initConfig({ + writeFn + }); +}; + +const initConfig = async ({writeFn}: {writeFn: (params: InitConfigParams) => Promise}) => { + const pm = detectPackageManager(); + + const source = await promptSource({pm}); + + const {configType, configPath: originalConfigPath} = await initConfigType(); + + await writeFn({ + configType, + configPath: originalConfigPath, + pm, + source + }); + + // We delete the deprecated juno.json, which is now replaced with juno.config.json|ts|js, as just created above. + // The developer was prompted about overwriting the configuration previously. + if (nonNullish(originalConfigPath) && basename(originalConfigPath) === 'juno.json') { + await unlink(originalConfigPath); + } + + if (configType === 'json') { + return; + } + + showConfigTips({pm}); +}; + +const promptSource = async ({pm}: {pm: PackageManager | undefined}): Promise => { + const cmd = `${pm ?? 'npm'}${isNullish(pm) || pm === 'npm' ? ' run' : ''} build`; + + const {output}: {output: string} = await prompts({ + type: 'select', + name: 'output', + message: `What is the output folder of your \`${cmd}\` command?`, + choices: [ + {title: 'build', value: 'build'}, + {title: 'dist', value: 'dist'}, + {title: 'out', value: 'out'}, + {title: '', value: '_manual_'} + ], + initial: 0 + }); + + // In case of control+c + assertAnswerCtrlC(output); + + if (output !== '_manual_') { + return output; + } + + const {source}: {source: string} = await prompts([ + { + type: 'text', + name: 'source', + message: 'Please enter the name of your output folder' + } + ]); + + assertAnswerCtrlC(source); + + return source; +}; + +const initConfigType = async (): Promise => { + if (!(await junoConfigExist())) { + // We try to automatically detect if we should create a TypeScript or JavaScript (mjs) configuration. + const detectedConfig = detectJunoConfigType(); + + if (nonNullish(detectedConfig)) { + return detectedConfig; + } + + const configType = await promptConfigType(); + return {configType}; + } + + return junoConfigFile(); +}; + +const showConfigTips = ({pm}: {pm: PackageManager | undefined}) => { + const cmd = `${pm ?? 'npm'} ${isNullish(pm) || pm === 'npm' ? 'i' : 'add'} @junobuild/config -D`; + + console.log( + `${NEW_CMD_LINE}๐Ÿ’ก You can leverage your IDE's intellisense by installing the library: ${yellow(cmd)}${NEW_CMD_LINE}` + ); +}; + +const initSatelliteConfig = async (): Promise => { + const satellites = await getCliSatellites(); + + const satellite = await (satellites.length > 0 + ? promptSatellites(satellites) + : promptSatellite()); + + if (satellite === '_manual_') { + return await promptSatellite(); + } + + return satellite; +}; + +const initOrbiterConfig = async (): Promise => { + const authOrbiters = await getCliOrbiters(); + + if (authOrbiters === undefined || authOrbiters.length === 0) { + return undefined; + } + + const orbiter = await promptOrbiters(authOrbiters); + + if (orbiter === '_none_') { + return undefined; + } + + return orbiter; +}; + +const promptSatellites = async (satellites: CliSatelliteConfig[]): Promise => { + const {satellite}: {satellite: string} = await prompts({ + type: 'select', + name: 'satellite', + message: 'Which satellite should be linked with this dapp?', + choices: [ + ...satellites.map(({p, n}) => ({title: n, value: p})), + {title: '', value: '_manual_'} + ], + initial: 0 + }); + + // In case of control+c + assertAnswerCtrlC(satellite); + + return satellite; +}; + +const promptSatellite = async (): Promise => { + const {satellite}: {satellite: string} = await prompts([ + { + type: 'text', + name: 'satellite', + message: `What's the ${cyan('id')} of your satellite?` + } + ]); + + assertAnswerCtrlC(satellite, 'The satellite ID is mandatory'); + + return satellite; +}; + +const promptOrbiters = async (orbiters: CliOrbiterConfig[]): Promise => { + const {orbiter}: {orbiter: string} = await prompts({ + type: 'select', + name: 'orbiter', + message: 'Which orbiter do you use for the analytics in this dapp?', + choices: [ + ...orbiters.map(({p, n}) => ({title: n ?? p, value: p})), + {title: '', value: '_none_'} + ], + initial: 0 + }); + + // In case of control+c + assertAnswerCtrlC(orbiter); + + return orbiter; +}; diff --git a/src/services/start/docker.services.ts b/src/services/start/docker.services.ts index d54e02e9..b913183a 100644 --- a/src/services/start/docker.services.ts +++ b/src/services/start/docker.services.ts @@ -1,10 +1,11 @@ import {nonNullish} from '@dfinity/utils'; -import {execute} from '@junobuild/cli-tools'; +import {assertAnswerCtrlC, execute} from '@junobuild/cli-tools'; import type {PartialConfigFile} from '@junobuild/config-loader'; -import {magenta} from 'kleur'; import {existsSync} from 'node:fs'; -import {writeFile} from 'node:fs/promises'; +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 { detectJunoDevConfigType, junoDevConfigExist, @@ -14,10 +15,11 @@ import {JUNO_DEV_CONFIG_FILENAME} from '../../constants/constants'; import {assertDockerRunning, checkDockerVersion} from '../../utils/env.utils'; import {copyTemplateFile, readTemplateFile} from '../../utils/fs.utils'; import {confirmAndExit} from '../../utils/prompt.utils'; -import {promptConfigType} from '../init.services'; +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(); @@ -28,8 +30,7 @@ export const startContainer = async () => { await assertDockerRunning(); - await assertJunoDevConfig(); - await assertDockerCompose(); + await assertAndInitConfig(); console.log('๐Ÿงช Launching local emulator...'); @@ -63,7 +64,7 @@ const assertJunoDevConfig = async () => { `A config file is required for development. Would you like the CLI to create one for you?` ); - const {configType, configPath} = await buildConfigType(); + const {configType, configPath} = await buildConfigType('satellite'); await copyTemplateFile({ template: `${JUNO_DEV_CONFIG_FILENAME}.${configType}`, @@ -73,9 +74,22 @@ const assertJunoDevConfig = async () => { }); }; -const buildConfigType = async (): Promise => { +const assertJunoConfig = async () => { + if (await junoConfigExist()) { + return; + } + + await confirmAndExit(`Your project needs a config file for Juno. Should we create one now?`); + + await initConfigNoneInteractive(); +}; + +type ConfigContext = 'skylab' | 'satellite'; + +const buildConfigType = async (context: ConfigContext): Promise => { // We try to automatically detect if we should create a TypeScript or JavaScript (mjs) configuration. - const detectedConfig = detectJunoDevConfigType(); + const fn = context === 'satellite' ? detectJunoDevConfigType : detectJunoConfigType; + const detectedConfig = fn(); if (nonNullish(detectedConfig)) { return detectedConfig; @@ -87,25 +101,51 @@ const buildConfigType = async (): Promise => { }; const assertDockerCompose = async () => { - if (existsSync('docker-compose.yml')) { + if (existsSync(DOCKER_COMPOSE_FILENAME)) { return; } - await confirmAndExit( - `The CLI utilizes Docker Compose, which is handy for customizing configurations. Would you like the CLI to generate a default ${magenta( - 'docker-compose.yml' - )} file for you?` - ); + const {image}: {image: string} = await prompts({ + type: 'select', + name: 'image', + message: 'What kind of emulator would you like to run locally?', + choices: [ + { + title: `Production-like setup with Console UI and known services`, + value: `skylab` + }, + {title: `Minimal setup without any UI`, value: `satellite`} + ] + }); + + assertAnswerCtrlC(image); const template = await readTemplateFile({ - template: 'docker-compose.yml', + template: `docker-compose.${image}.yml`, sourceFolder: TEMPLATE_PATH }); - const {configPath} = junoDevConfigFile(); + const readConfig = image === 'satellite' ? junoDevConfigFile : junoConfigFile; + const {configPath} = readConfig(); const configFile = basename(configPath); - const content = template.replaceAll('', configFile); + const content = template + .replaceAll('', configFile) + .replaceAll('', configFile); + + await writeFile(join(DESTINATION_PATH, DOCKER_COMPOSE_FILENAME), content, 'utf-8'); +}; + +const assertAndInitConfig = async () => { + await assertDockerCompose(); - await writeFile(join(DESTINATION_PATH, 'docker-compose.yml'), content, 'utf-8'); + const dockerCompose = await readFile(DOCKER_COMPOSE_FILENAME, 'utf-8'); + const isSkylab = /image:\s*junobuild\/skylab(:[^\s]*)?/.test(dockerCompose); + + if (isSkylab) { + await assertJunoConfig(); + return; + } + + await assertJunoDevConfig(); }; diff --git a/templates/docker/docker-compose.satellite.yml b/templates/docker/docker-compose.satellite.yml new file mode 100644 index 00000000..b997a2fc --- /dev/null +++ b/templates/docker/docker-compose.satellite.yml @@ -0,0 +1,21 @@ +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.yml b/templates/docker/docker-compose.skylab.yml similarity index 82% rename from templates/docker/docker-compose.yml rename to templates/docker/docker-compose.skylab.yml index 56e4ec70..5901f60b 100644 --- a/templates/docker/docker-compose.yml +++ b/templates/docker/docker-compose.skylab.yml @@ -11,8 +11,9 @@ services: volumes: # Persistent volume to store internal state - juno_skylab:/juno/.juno - # Local dev config file to customize Satellite behavior - - ./:/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), diff --git a/templates/init/juno.config.js b/templates/init/juno.config.js index 2489a5b5..9508104a 100644 --- a/templates/init/juno.config.js +++ b/templates/init/juno.config.js @@ -3,7 +3,10 @@ import {defineConfig} from '@junobuild/config'; /** @type {import('@junobuild/config').JunoConfig} */ export default defineConfig({ satellite: { - id: '', + ids: { + development: '', + production: '' + }, source: '' }, orbiter: { diff --git a/templates/init/juno.config.ts b/templates/init/juno.config.ts index ee3b0b0b..5636a0dc 100644 --- a/templates/init/juno.config.ts +++ b/templates/init/juno.config.ts @@ -2,7 +2,10 @@ import {defineConfig} from '@junobuild/config'; export default defineConfig({ satellite: { - id: '', + ids: { + development: '', + production: '' + }, source: '' }, orbiter: { diff --git a/templates/init/juno.predeploy.config.js b/templates/init/juno.predeploy.config.js index 149e4d40..454b130c 100644 --- a/templates/init/juno.predeploy.config.js +++ b/templates/init/juno.predeploy.config.js @@ -3,7 +3,10 @@ import {defineConfig} from '@junobuild/config'; /** @type {import('@junobuild/config').JunoConfig} */ export default defineConfig({ satellite: { - id: '', + ids: { + development: '', + production: '' + }, source: '', predeploy: [' build'] }, diff --git a/templates/init/juno.predeploy.config.ts b/templates/init/juno.predeploy.config.ts index 9b9589d4..ff7772cc 100644 --- a/templates/init/juno.predeploy.config.ts +++ b/templates/init/juno.predeploy.config.ts @@ -2,7 +2,10 @@ import {defineConfig} from '@junobuild/config'; export default defineConfig({ satellite: { - id: '', + ids: { + development: '', + production: '' + }, source: '', predeploy: [' build'] }, diff --git a/templates/init/juno.satellite.config.js b/templates/init/juno.satellite.config.js index c6df8289..3f12cb8d 100644 --- a/templates/init/juno.satellite.config.js +++ b/templates/init/juno.satellite.config.js @@ -3,7 +3,10 @@ import {defineConfig} from '@junobuild/config'; /** @type {import('@junobuild/config').JunoConfig} */ export default defineConfig({ satellite: { - id: '', + ids: { + development: '', + production: '' + }, source: '' } }); diff --git a/templates/init/juno.satellite.config.ts b/templates/init/juno.satellite.config.ts index 9778f7b6..0e4aa9b5 100644 --- a/templates/init/juno.satellite.config.ts +++ b/templates/init/juno.satellite.config.ts @@ -2,7 +2,10 @@ import {defineConfig} from '@junobuild/config'; export default defineConfig({ satellite: { - id: '', + ids: { + development: '', + production: '' + }, source: '' } }); diff --git a/templates/init/juno.satellite.predeploy.config.js b/templates/init/juno.satellite.predeploy.config.js index 5542a466..cd28e8b5 100644 --- a/templates/init/juno.satellite.predeploy.config.js +++ b/templates/init/juno.satellite.predeploy.config.js @@ -3,7 +3,10 @@ import {defineConfig} from '@junobuild/config'; /** @type {import('@junobuild/config').JunoConfig} */ export default defineConfig({ satellite: { - id: '', + ids: { + development: '', + production: '' + }, source: '', predeploy: [' build'] } diff --git a/templates/init/juno.satellite.predeploy.config.ts b/templates/init/juno.satellite.predeploy.config.ts index 40a9e5e0..5052e5d2 100644 --- a/templates/init/juno.satellite.predeploy.config.ts +++ b/templates/init/juno.satellite.predeploy.config.ts @@ -2,7 +2,10 @@ import {defineConfig} from '@junobuild/config'; export default defineConfig({ satellite: { - id: '', + ids: { + development: '', + production: '' + }, source: '', predeploy: [' build'] }