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
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
@@ -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);
};
4 changes: 3 additions & 1 deletion src/constants/help.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.';
Expand Down Expand Up @@ -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).`;
35 changes: 35 additions & 0 deletions src/help/run.help.ts
Original file line number Diff line number Diff line change
@@ -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);
};
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -85,6 +86,9 @@ export const run = async () => {
case 'dev':
helpDev(args);
break;
case 'run':
helpRun(args);
break;
case 'fn':
case 'functions':
helpFunctions(args);
Expand Down Expand Up @@ -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);
Expand Down
35 changes: 3 additions & 32 deletions src/services/functions/build/build.javascript.services.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';

Expand Down Expand Up @@ -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<boolean> => {
try {
await import('esbuild');
return true;
} catch (_err: unknown) {
return false;
}
};

const printResults = ({
metadata,
buildResult
Expand Down
101 changes: 101 additions & 0 deletions src/services/run.services.ts
Original file line number Diff line number Diff line change
@@ -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 <path>'));
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};
};
30 changes: 30 additions & 0 deletions src/utils/esbuild.utils.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> => {
try {
await import('esbuild');
return true;
} catch (_err: unknown) {
return false;
}
};
2 changes: 2 additions & 0 deletions src/utils/satellite.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down