diff --git a/package-lock.json b/package-lock.json index 063f97ac..c8feb92f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@junobuild/core": "^0.1.9", "@junobuild/did-tools": "^0.2.0", "@junobuild/utils": "^0.1.1", + "chokidar": "^4.0.3", "conf": "^13.1.0", "open": "^10.1.0", "ora": "^8.2.0", @@ -2579,6 +2580,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -5921,6 +5937,19 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8527,6 +8556,14 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" }, + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "requires": { + "readdirp": "^4.0.1" + } + }, "cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -10697,6 +10734,11 @@ "util-deprecate": "^1.0.1" } }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" + }, "reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", diff --git a/package.json b/package.json index 2c0c66ed..d83cf0f5 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@junobuild/core": "^0.1.9", "@junobuild/did-tools": "^0.2.0", "@junobuild/utils": "^0.1.1", + "chokidar": "^4.0.3", "conf": "^13.1.0", "open": "^10.1.0", "ora": "^8.2.0", diff --git a/src/help/dev.build.help.ts b/src/help/dev.build.help.ts index 4a65bb8c..3a803b05 100644 --- a/src/help/dev.build.help.ts +++ b/src/help/dev.build.help.ts @@ -9,13 +9,15 @@ const usage = `Usage: ${green('juno')} ${cyan('dev')} ${magenta('build')} ${yell Options: ${yellow('-l, --lang')} Specify the language for building the serverless functions: ${magenta('rust')}, ${magenta('typescript')} or ${magenta('javascript')}. ${yellow('-p, --path')} Path to the source to bundle. + ${yellow('-w, --watch')} Rebuild your functions automatically when source files change. ${yellow('-h, --help')} Output usage information. Notes: - If no language is provided, the CLI attempts to determine the appropriate build. - Language can be shortened to ${magenta('rs')} for Rust, ${magenta('ts')} for TypeScript and ${magenta('mjs')} for JavaScript. -- The path option maps to ${magenta('--manifest-path')} for Rust (Cargo) or to the source file for TypeScript and JavaScript (e.g. ${magenta('index.ts')} or ${magenta('index.mjs')}).`; +- The path option maps to ${magenta('--manifest-path')} for Rust (Cargo) or to the source file for TypeScript and JavaScript (e.g. ${magenta('index.ts')} or ${magenta('index.mjs')}). +- The watch option rebuilds when source files change, with a default debounce delay of 10 seconds; optionally, pass a delay in milliseconds.`; const doc = `${DEV_BUILD_DESCRIPTION} diff --git a/src/help/help.ts b/src/help/help.ts index dab301b1..873590ad 100644 --- a/src/help/help.ts +++ b/src/help/help.ts @@ -22,6 +22,8 @@ __) || | || \\| |/ \\ export const TITLE = `${JUNO_LOGO} CLI ${grey(`v${version}`)}`; +export const SMALL_TITLE = `Juno CLI ${grey(`v${version}`)}`; + export const help = ` ${TITLE} diff --git a/src/services/build/build.services.ts b/src/services/build/build.services.ts index 1fa0c94f..d1fa2bc2 100644 --- a/src/services/build/build.services.ts +++ b/src/services/build/build.services.ts @@ -1,20 +1,32 @@ -import {nonNullish} from '@dfinity/utils'; -import {nextArg} from '@junobuild/cli-tools'; +import {debounce, nonNullish} from '@dfinity/utils'; +import {hasArgs, nextArg} from '@junobuild/cli-tools'; +import chokidar from 'chokidar'; import {red} from 'kleur'; import {existsSync} from 'node:fs'; -import {basename, extname} from 'node:path'; +import {basename, dirname, extname} from 'node:path'; import { DEVELOPER_PROJECT_SATELLITE_CARGO_TOML, DEVELOPER_PROJECT_SATELLITE_INDEX_MJS, - DEVELOPER_PROJECT_SATELLITE_INDEX_TS + DEVELOPER_PROJECT_SATELLITE_INDEX_TS, + DEVELOPER_PROJECT_SATELLITE_PATH } from '../../constants/dev.constants'; +import {SMALL_TITLE} from '../../help/help'; import {type BuildArgs} from '../../types/build'; import {buildJavaScript, buildTypeScript} from './build.javascript'; import {buildRust} from './build.rust.services'; export const build = async (args?: string[]) => { - const {lang, path} = buildArgs(args); + const {watch, ...params} = buildArgs(args); + if (watch === true || nonNullish(watch)) { + watchBuild({watch, ...params}); + return; + } + + await executeBuild(params); +}; + +const executeBuild = async ({lang, path}: Omit) => { // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (lang) { case 'rs': @@ -74,14 +86,51 @@ export const build = async (args?: string[]) => { ); }; +const watchBuild = ({watch, path, ...params}: BuildArgs) => { + const doBuild = async () => { + console.log('Rebuilding serverless functions...'); + await executeBuild({path, ...params}); + }; + + const DEFAULT_TIMEOUT = 10_000; + const timeout = + nonNullish(watch) && typeof watch === 'string' ? parseInt(watch) : DEFAULT_TIMEOUT; + + const debounceBuild = debounce(doBuild, !isNaN(timeout) ? timeout : DEFAULT_TIMEOUT); + + const watchOnEvent = () => { + debounceBuild(); + }; + + const watchPath = nonNullish(path) ? dirname(path) : DEVELOPER_PROJECT_SATELLITE_PATH; + + console.log(SMALL_TITLE); + console.log('👀 Watching for file changes.'); + + chokidar + .watch(watchPath, { + ignoreInitial: true, + awaitWriteFinish: true + }) + .on('add', watchOnEvent) + .on('change', watchOnEvent) + .on('error', (err) => { + console.log(red('️‼️ Unexpected error while live reloading:'), err); + }); +}; + const buildArgs = (args?: string[]): BuildArgs => { const path = nextArg({args, option: '-p'}) ?? nextArg({args, option: '--path'}); const {lang} = buildLang(args); + const watch = hasArgs({args, options: ['-w', '--watch']}); + const watchValue = nextArg({args, option: '-w'}) ?? nextArg({args, option: '--watch'}); + return { path, - lang + lang, + watch: watchValue ?? watch }; }; diff --git a/src/types/build.ts b/src/types/build.ts index 772f5a99..74ea442d 100644 --- a/src/types/build.ts +++ b/src/types/build.ts @@ -3,4 +3,5 @@ export type BuildLang = 'ts' | 'mjs' | 'rs'; export interface BuildArgs { lang?: BuildLang; path?: string | undefined; + watch?: boolean | string; }