diff --git a/package-lock.json b/package-lock.json index 13107442..c5da8a0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@junobuild/admin": "^2.2.2", "@junobuild/cdn": "^1.3.2", "@junobuild/cli-tools": "^0.7.2", - "@junobuild/config": "^2.1.1", + "@junobuild/config": "^2.2.0", "@junobuild/config-loader": "^0.4.5", "@junobuild/core": "^2.1.2", "@junobuild/did-tools": "^0.3.3", @@ -1490,9 +1490,9 @@ } }, "node_modules/@junobuild/config": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@junobuild/config/-/config-2.1.1.tgz", - "integrity": "sha512-V8gqFrbg39TZhZvWgRobLcN5HEG4tLIQb58ZWZLHyK1LvbRKC7j3XaYZphkNAoxcqOte72dIdOxEROGdqr2H3w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@junobuild/config/-/config-2.2.0.tgz", + "integrity": "sha512-nRhvQ02CdDuKL3fCGoMrY0zRc/MHOZg8Z9swIhFXyXf1cPNurNN4c5ek1okZA5okAUIchF9WXEXhVGRIU+6Lkg==", "license": "MIT", "peerDependencies": { "@dfinity/zod-schemas": "^1.1.0", @@ -7304,9 +7304,9 @@ } }, "@junobuild/config": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@junobuild/config/-/config-2.1.1.tgz", - "integrity": "sha512-V8gqFrbg39TZhZvWgRobLcN5HEG4tLIQb58ZWZLHyK1LvbRKC7j3XaYZphkNAoxcqOte72dIdOxEROGdqr2H3w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@junobuild/config/-/config-2.2.0.tgz", + "integrity": "sha512-nRhvQ02CdDuKL3fCGoMrY0zRc/MHOZg8Z9swIhFXyXf1cPNurNN4c5ek1okZA5okAUIchF9WXEXhVGRIU+6Lkg==", "requires": {} }, "@junobuild/config-loader": { diff --git a/package.json b/package.json index cc86fde0..ecea8984 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@junobuild/admin": "^2.2.2", "@junobuild/cdn": "^1.3.2", "@junobuild/cli-tools": "^0.7.2", - "@junobuild/config": "^2.1.1", + "@junobuild/config": "^2.2.0", "@junobuild/config-loader": "^0.4.5", "@junobuild/core": "^2.1.2", "@junobuild/did-tools": "^0.3.3", diff --git a/src/commands/run.ts b/src/commands/run.ts new file mode 100644 index 00000000..65763831 --- /dev/null +++ b/src/commands/run.ts @@ -0,0 +1,10 @@ +import {logHelpRun} from '../help/run.help'; +import {run as runServices} from '../services/run.services'; + +export const run = async (args?: string[]) => { + await runServices(args); +}; + +export const helpRun = (args?: string[]) => { + logHelpRun(args); +}; diff --git a/src/constants/help.constants.ts b/src/constants/help.constants.ts index c6fed6d4..2c6e6743 100644 --- a/src/constants/help.constants.ts +++ b/src/constants/help.constants.ts @@ -23,6 +23,7 @@ export const VERSION_DESCRIPTION = 'Check the version of the CLI.'; export const STATUS_DESCRIPTION = 'Check the status of the modules.'; export const WHOAMI_DESCRIPTION = 'Display your current profile, access key, and links to your satellite.'; +export const RUN_DESCRIPTION = 'Run a custom script in the CLI context.'; export const EMULATOR_START_DESCRIPTION = 'Start the emulator for local development.'; export const EMULATOR_WAIT_DESCRIPTION = 'Wait until the emulator is ready.'; @@ -58,8 +59,9 @@ export const OPTIONS_BUILD = `${yellow('-l, --lang')} Specify the lan ${yellow('--source-path')} Optional path to the TypeScript or JavaScript entry file.`; export const OPTIONS_CONFIG = `${OPTION_MODE} ${OPTION_PROFILE}`; +export const OPTIONS_CONTAINER = `${yellow('--container-url')} Override a custom container URL. If not provided, defaults to production or the local container in development mode.`; export const OPTIONS_ENV = `${OPTIONS_CONFIG} - ${yellow('--container-url')} Override a custom container URL. If not provided, defaults to production or the local container in development mode. + ${OPTIONS_CONTAINER} ${yellow('--console-url')} Specify a custom URL to access the developer Console.`; export const NOTE_KEEP_STAGED = `The option ${yellow('--keep-staged')} only applies when ${yellow('--no-apply')} is NOT used (i.e. the change is applied immediately).`; diff --git a/src/help/run.help.ts b/src/help/run.help.ts new file mode 100644 index 00000000..93253669 --- /dev/null +++ b/src/help/run.help.ts @@ -0,0 +1,35 @@ +import {cyan, green, yellow} from 'kleur'; +import { + OPTION_HELP, + OPTIONS_CONFIG, + OPTIONS_CONTAINER, + RUN_DESCRIPTION +} from '../constants/help.constants'; +import {helpOutput} from './common.help'; +import {TITLE} from './help'; + +const usage = `Usage: ${green('juno')} ${cyan('run')} ${yellow('[options]')} + +Options: + ${yellow('-s, --src')} The path to your JavaScript or TypeScript script. + ${OPTIONS_CONFIG} + ${OPTIONS_CONTAINER} + ${OPTION_HELP}`; + +const doc = `${RUN_DESCRIPTION} + +\`\`\` +${usage} +\`\`\` +`; + +const help = `${TITLE} + +${RUN_DESCRIPTION} + +${usage} +`; + +export const logHelpRun = (args?: string[]) => { + console.log(helpOutput(args) === 'doc' ? doc : help); +}; diff --git a/src/index.ts b/src/index.ts index 6e3ef8f1..5c794c26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import {emulator, helpEmulator} from './commands/emulator'; import {functions, helpFunctions} from './commands/functions'; import {init} from './commands/init'; import {open} from './commands/open'; +import {helpRun, run as runCmd} from './commands/run'; import {snapshot} from './commands/snapshot'; import {startStop} from './commands/start-stop'; import {status} from './commands/status'; @@ -85,6 +86,9 @@ export const run = async () => { case 'dev': helpDev(args); break; + case 'run': + helpRun(args); + break; case 'fn': case 'functions': helpFunctions(args); @@ -169,6 +173,9 @@ export const run = async () => { case 'dev': await dev(args); break; + case 'run': + await runCmd(args); + break; case 'fn': case 'functions': await functions(args); diff --git a/src/services/functions/build/build.javascript.services.ts b/src/services/functions/build/build.javascript.services.ts index 119d57f4..5c57220f 100644 --- a/src/services/functions/build/build.javascript.services.ts +++ b/src/services/functions/build/build.javascript.services.ts @@ -1,7 +1,7 @@ import {notEmptyString} from '@dfinity/utils'; -import {buildEsm, execute, formatBytes} from '@junobuild/cli-tools'; +import {buildEsm, formatBytes} from '@junobuild/cli-tools'; import type {Metafile} from 'esbuild'; -import {green, magenta, red, yellow} from 'kleur'; +import {green, red, yellow} from 'kleur'; import {join} from 'node:path'; import { DEPLOY_SPUTNIK_PATH, @@ -10,9 +10,8 @@ import { INDEX_TS } from '../../../constants/dev.constants'; import type {BuildArgs, BuildLang, BuildMetadata} from '../../../types/build'; +import {installEsbuild} from '../../../utils/esbuild.utils'; import {formatTime} from '../../../utils/format.utils'; -import {detectPackageManager} from '../../../utils/pm.utils'; -import {confirmAndExit} from '../../../utils/prompt.utils'; import {readEmulatorConfigAndCreateDeployTargetDir} from '../../emulator/_fs.services'; import {prepareJavaScriptBuildMetadata} from './build.metadata.services'; @@ -101,34 +100,6 @@ const buildWithEsbuild = async ({ }; }; -const installEsbuild = async () => { - const esbuildInstalled = await hasEsbuild(); - - if (esbuildInstalled) { - return; - } - - await confirmAndExit( - `${magenta('esbuild')} is required to build the serverless functions. Install it now?` - ); - - const pm = detectPackageManager(); - - await execute({ - command: pm ?? 'npm', - args: [pm === 'npm' ? 'i' : 'add', 'esbuild', '-D'] - }); -}; - -const hasEsbuild = async (): Promise => { - try { - await import('esbuild'); - return true; - } catch (_err: unknown) { - return false; - } -}; - const printResults = ({ metadata, buildResult diff --git a/src/services/run.services.ts b/src/services/run.services.ts new file mode 100644 index 00000000..8030566a --- /dev/null +++ b/src/services/run.services.ts @@ -0,0 +1,101 @@ +import {Principal} from '@dfinity/principal'; +import {assertNonNullish, isNullish, nonNullish} from '@dfinity/utils'; +import {nextArg} from '@junobuild/cli-tools'; +import {OnRunSchema, type RunFnOrObject, RunFnOrObjectSchema} from '@junobuild/config'; +import {build} from 'esbuild'; +import {red, yellow} from 'kleur'; +import {ENV} from '../env'; +import {assertConfigAndLoadSatelliteContext} from '../utils/satellite.utils'; + +export const run = async (args?: string[]) => { + const infile = nextArg({args, option: '-s'}) ?? nextArg({args, option: '--src'}); + + if (isNullish(infile)) { + console.log(red('Missing required path to script: --src ')); + return; + } + + const {onRun} = await importOnRun({infile}); + + if (isNullish(onRun)) { + console.log(yellow('Cannot import a task to run. 🤷‍♂️')); + console.log(`Does your script ${infile} export a function named "onRun"?`); + return; + } + + if (!RunFnOrObjectSchema.safeParse(onRun).success) { + console.log(red('Your "onRun" export is invalid. It must be of type RunFnOrObject.')); + return; + } + + const job = + typeof onRun === 'function' + ? onRun({ + mode: ENV.mode, + profile: ENV.profile + }) + : onRun; + + const assertJob = OnRunSchema.safeParse(job); + + if (!assertJob.success) { + console.log(red('Your job to run is invalid. It must be of type OnRun.')); + return; + } + + const {data: assertedJob} = assertJob; + + const { + satellite: {satelliteId, identity} + } = await assertConfigAndLoadSatelliteContext(); + + await assertedJob.run({ + satelliteId: Principal.fromText(satelliteId), + identity, + ...(nonNullish(ENV.containerUrl) && {container: ENV.containerUrl}) + }); +}; + +const importOnRun = async ({ + infile +}: { + infile: string; +}): Promise<{onRun: RunFnOrObject | undefined}> => { + const {code} = await buildCode({infile}); + + const {onRun} = await import( + `data:text/javascript;base64,${Buffer.from(code).toString(`base64`)}` + ); + + return {onRun: typeof onRun === 'undefined' ? undefined : onRun}; +}; + +const buildCode = async ({infile}: {infile: string}): Promise<{code: Uint8Array}> => { + const {outputFiles} = await build({ + entryPoints: [infile], + bundle: true, + minify: true, + format: 'esm', + platform: 'node', + write: false, + supported: { + 'top-level-await': false, + 'inline-script': false + }, + define: { + self: 'globalThis' + }, + metafile: true, + banner: { + js: `import { createRequire as topLevelCreateRequire } from 'node:module'; +import { resolve } from 'node:path'; +const require = topLevelCreateRequire(resolve(process.cwd(), '.juno-pseudo-require-anchor.mjs'));` + } + }); + + const code = outputFiles[0]?.contents; + + assertNonNullish(code, 'No script build'); + + return {code}; +}; diff --git a/src/utils/esbuild.utils.ts b/src/utils/esbuild.utils.ts new file mode 100644 index 00000000..925f8e39 --- /dev/null +++ b/src/utils/esbuild.utils.ts @@ -0,0 +1,30 @@ +import {execute} from '@junobuild/cli-tools'; +import {magenta} from 'kleur'; +import {detectPackageManager} from './pm.utils'; +import {confirmAndExit} from './prompt.utils'; + +export const installEsbuild = async () => { + const esbuildInstalled = await hasEsbuild(); + + if (esbuildInstalled) { + return; + } + + await confirmAndExit(`${magenta('esbuild')} is required for building. Install it now?`); + + const pm = detectPackageManager(); + + await execute({ + command: pm ?? 'npm', + args: [pm === 'npm' ? 'i' : 'add', 'esbuild', '-D'] + }); +}; + +const hasEsbuild = async (): Promise => { + try { + await import('esbuild'); + return true; + } catch (_err: unknown) { + return false; + } +}; diff --git a/src/utils/satellite.utils.ts b/src/utils/satellite.utils.ts index f99b4abd..77a4f9c0 100644 --- a/src/utils/satellite.utils.ts +++ b/src/utils/satellite.utils.ts @@ -60,6 +60,8 @@ const assertAndReadSatelliteId = ({ const satelliteId = ids?.[mode] ?? id ?? deprecatedSatelliteId; + // TODO: Principal.isPrincipal + if (isNullish(satelliteId)) { console.log(red(`A satellite ID for ${mode} must be set in your configuration.`)); process.exit(1);