From d86cf7ed2898599ca9b351c9654e20b9c742da2b Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sun, 23 Nov 2025 10:56:27 +0100 Subject: [PATCH 1/4] feat!: migrate from junobuild-didc to icp-sdk/bindgen --- src/constants/dev.constants.ts | 9 ++- .../functions/build/build.rust.services.ts | 62 +++++++++---------- src/utils/build.rust.utils.ts | 28 +++++---- src/utils/env.utils.ts | 2 +- 4 files changed, 53 insertions(+), 48 deletions(-) diff --git a/src/constants/dev.constants.ts b/src/constants/dev.constants.ts index ff4a3307..cab51779 100644 --- a/src/constants/dev.constants.ts +++ b/src/constants/dev.constants.ts @@ -4,11 +4,14 @@ export const SATELLITE_PROJECT_NAME = 'satellite'; export const SPUTNIK_PROJECT_NAME = 'sputnik'; export const DEVELOPER_PROJECT_SRC_PATH = join(process.cwd(), 'src'); -export const DEVELOPER_PROJECT_SATELLITE_PATH = join(DEVELOPER_PROJECT_SRC_PATH, 'satellite'); +export const DEVELOPER_PROJECT_SATELLITE_PATH = join( + DEVELOPER_PROJECT_SRC_PATH, + SATELLITE_PROJECT_NAME +); export const DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH = join( DEVELOPER_PROJECT_SRC_PATH, 'declarations', - 'satellite' + SATELLITE_PROJECT_NAME ); export const CARGO_TOML = 'Cargo.toml'; @@ -31,7 +34,7 @@ export const DEVELOPER_PROJECT_SATELLITE_INDEX_MJS = join( const TEMPLATE_PATH = '../templates/eject'; export const RUST_TEMPLATE_PATH = join(TEMPLATE_PATH, 'rust'); -export const RUST_TEMPLATE_SATELLITE_PATH = join(RUST_TEMPLATE_PATH, 'src', 'satellite'); +export const RUST_TEMPLATE_SATELLITE_PATH = join(RUST_TEMPLATE_PATH, 'src', SATELLITE_PROJECT_NAME); export const TS_TEMPLATE_PATH = join(TEMPLATE_PATH, 'typescript'); export const MJS_TEMPLATE_PATH = join(TEMPLATE_PATH, 'javascript'); diff --git a/src/services/functions/build/build.rust.services.ts b/src/services/functions/build/build.rust.services.ts index d0965470..79a0f7a7 100644 --- a/src/services/functions/build/build.rust.services.ts +++ b/src/services/functions/build/build.rust.services.ts @@ -3,7 +3,7 @@ import {execute, formatBytes, gzipFile, spawn} from '@junobuild/cli-tools'; import {generateApi} from '@junobuild/did-tools'; import {red, yellow} from 'kleur'; import {existsSync} from 'node:fs'; -import {copyFile, lstat, mkdir, readFile, rename, writeFile} from 'node:fs/promises'; +import {copyFile, lstat, mkdir, readFile, rename, rm, writeFile} from 'node:fs/promises'; import {join, relative} from 'node:path'; import ora, {type Ora} from 'ora'; import {compare, minVersion, satisfies} from 'semver'; @@ -20,15 +20,16 @@ import { } from '../../../constants/dev.constants'; import type {BuildArgs, BuildType} from '../../../types/build'; import { + checkBindgen, checkCandidExtractor, checkIcWasm, - checkJunoDidc, checkWasi2ic } from '../../../utils/build.rust.utils'; import {readSatelliteDid} from '../../../utils/did.utils'; import {checkRustVersion} from '../../../utils/env.utils'; import {formatTime} from '../../../utils/format.utils'; import {readPackageJson} from '../../../utils/pkg.utils'; +import {detectPackageManager} from '../../../utils/pm.utils'; import {readEmulatorConfigAndCreateDeployTargetDir} from '../../emulator/_fs.services'; import {prepareJunoPkgForSatellite, prepareJunoPkgForSputnik} from './build.metadata.services'; import {dispatchEmulatorTouchSatellite} from './touch.services'; @@ -55,9 +56,9 @@ export const buildRust = async ({ return; } - const {valid: validDidc} = await checkJunoDidc(); + const {valid: validBindgen} = await checkBindgen(); - if (!validDidc) { + if (!validBindgen) { return; } @@ -216,40 +217,35 @@ const didc = async () => { return; } - const generate = async (type: 'js' | 'ts') => { - const output = satellitedIdl(type); + const pm = detectPackageManager(); - await spawn({ - command: 'junobuild-didc', - args: ['-i', SATELLITE_CUSTOM_DID_FILE, '-t', type, '-o', output] - }); + const command = pm === 'npm' || isNullish(pm) ? 'npx' : pm; - const content = await readFile(output, 'utf-8'); - - // Depending on the `tsconfig`, the `factory.did.js` file might be validated. - // Cleaning the file prevents errors such as: - // TS7031: Binding element 'IDL' implicitly has an 'any' type. - const cleanJs = (content: string): string => { - const cleanFactory = content.replace( - /export const idlFactory = \({ IDL }\) => {/g, - `// @ts-expect-error -export const idlFactory = ({ IDL }) => {` - ); - return cleanFactory.replace( - /export const init = \({ IDL }\) => {/g, - `// @ts-expect-error -export const init = ({ IDL }) => {` - ); - }; - - const cleanedContent = type === 'js' ? cleanJs(content) : content; + // --actor-disabled: skip generating actor files, since we handle those ourselves + // --force: overwrite files. Required; otherwise, icp-bindgen would delete files at preprocess, + // which causes issues when multiple .did files are located in the same folder. + await spawn({ + command, + args: [ + 'icp-bindgen', + '--did-file', + SATELLITE_CUSTOM_DID_FILE, + '--out-dir', + DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH, + '--actor-disabled', + '--force' + ] + }); - await writeFile(output, `${AUTO_GENERATED}\n\n${cleanedContent}`); - }; + // icp-bindgen generates the files in a `declarations` subfolder + // using a different suffix for JavaScript as the one we used to use. + // That's why we have to post-process the results. + const generatedFolder = join(DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH, 'declarations'); - const promises = (['js', 'ts'] as Array<'js' | 'ts'>).map(generate); + await rename(join(generatedFolder, `${SATELLITE_PROJECT_NAME}.did.d.ts`), satellitedIdl('ts')); + await rename(join(generatedFolder, `${SATELLITE_PROJECT_NAME}.did.js`), satellitedIdl('js')); - await Promise.all(promises); + await rm(generatedFolder, {recursive: true, force: true}); }; const api = async () => { diff --git a/src/utils/build.rust.utils.ts b/src/utils/build.rust.utils.ts index 48043d52..bd4fcbae 100644 --- a/src/utils/build.rust.utils.ts +++ b/src/utils/build.rust.utils.ts @@ -1,7 +1,9 @@ +import {isNullish} from '@dfinity/utils'; import {execute} from '@junobuild/cli-tools'; import {magenta, yellow} from 'kleur'; import {IC_WASM_MIN_VERSION} from '../constants/dev.constants'; -import {checkCargoBinInstalled, checkIcWasmVersion} from './env.utils'; +import {checkIcWasmVersion, checkToolInstalled} from './env.utils'; +import {detectPackageManager} from './pm.utils'; import {confirmAndExit} from './prompt.utils'; export const checkIcWasm = async (): Promise<{valid: boolean}> => { @@ -28,7 +30,7 @@ export const checkIcWasm = async (): Promise<{valid: boolean}> => { }; export const checkCandidExtractor = async (): Promise<{valid: boolean}> => { - const {valid} = await checkCargoBinInstalled({ + const {valid} = await checkToolInstalled({ command: 'candid-extractor', args: ['--version'] }); @@ -53,10 +55,14 @@ export const checkCandidExtractor = async (): Promise<{valid: boolean}> => { return {valid: true}; }; -export const checkJunoDidc = async (): Promise<{valid: boolean}> => { - const {valid} = await checkCargoBinInstalled({ - command: 'junobuild-didc', - args: ['--version'] +export const checkBindgen = async (): Promise<{valid: boolean}> => { + const pm = detectPackageManager(); + + const command = pm === 'npm' || isNullish(pm) ? 'npx' : pm; + + const {valid} = await checkToolInstalled({ + command, + args: ['icp-bindgen', '--version'] }); if (valid === false) { @@ -66,13 +72,13 @@ export const checkJunoDidc = async (): Promise<{valid: boolean}> => { if (valid === 'error') { await confirmAndExit( `It seems that ${magenta( - 'junobuild-didc' - )} is not installed. This is a useful tool for generating automatically JavaScript or TypeScript bindings. Would you like to install it?` + 'icp-bindgen' + )} is not installed. This tool generates JavaScript or TypeScript bindings. Would you like to install it?` ); await execute({ - command: 'cargo', - args: ['install', `junobuild-didc`] + command: pm ?? 'npm', + args: [pm === 'npm' ? 'i' : 'add', '@icp-sdk/bindgen', '-D'] }); } @@ -80,7 +86,7 @@ export const checkJunoDidc = async (): Promise<{valid: boolean}> => { }; export const checkWasi2ic = async (): Promise<{valid: boolean}> => { - const {valid} = await checkCargoBinInstalled({ + const {valid} = await checkToolInstalled({ command: 'wasi2ic', args: ['--version'] }); diff --git a/src/utils/env.utils.ts b/src/utils/env.utils.ts index 396ff1c1..181c3771 100644 --- a/src/utils/env.utils.ts +++ b/src/utils/env.utils.ts @@ -77,7 +77,7 @@ export const checkIcWasmVersion = async (): Promise<{valid: boolean | 'error'}> return {valid: true}; }; -export const checkCargoBinInstalled = async ({ +export const checkToolInstalled = async ({ command, args }: { From 894c75717bf2b38a05384c40f1648db3abfb2c93 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sun, 23 Nov 2025 10:59:12 +0100 Subject: [PATCH 2/4] feat: review text --- src/utils/build.rust.utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/build.rust.utils.ts b/src/utils/build.rust.utils.ts index bd4fcbae..367dbbf9 100644 --- a/src/utils/build.rust.utils.ts +++ b/src/utils/build.rust.utils.ts @@ -71,9 +71,9 @@ export const checkBindgen = async (): Promise<{valid: boolean}> => { if (valid === 'error') { await confirmAndExit( - `It seems that ${magenta( + `${magenta( 'icp-bindgen' - )} is not installed. This tool generates JavaScript or TypeScript bindings. Would you like to install it?` + )} is not available. This tool is required to generate API bindings. Would you like to install it now?` ); await execute({ From 7f216fda9b98704bbdbf942337221fe9373c6291 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sun, 23 Nov 2025 10:59:54 +0100 Subject: [PATCH 3/4] feat: review text --- src/utils/build.rust.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/build.rust.utils.ts b/src/utils/build.rust.utils.ts index 367dbbf9..eee53ec1 100644 --- a/src/utils/build.rust.utils.ts +++ b/src/utils/build.rust.utils.ts @@ -72,7 +72,7 @@ export const checkBindgen = async (): Promise<{valid: boolean}> => { if (valid === 'error') { await confirmAndExit( `${magenta( - 'icp-bindgen' + '@icp-sdk/bindgen' )} is not available. This tool is required to generate API bindings. Would you like to install it now?` ); From dce9c1740e1c721126cf67b62b5fc0d4ef6c80c7 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sun, 23 Nov 2025 11:10:55 +0100 Subject: [PATCH 4/4] feat: silence output --- src/services/functions/build/build.rust.services.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/services/functions/build/build.rust.services.ts b/src/services/functions/build/build.rust.services.ts index 79a0f7a7..b9b96122 100644 --- a/src/services/functions/build/build.rust.services.ts +++ b/src/services/functions/build/build.rust.services.ts @@ -234,7 +234,8 @@ const didc = async () => { DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH, '--actor-disabled', '--force' - ] + ], + silentOut: true }); // icp-bindgen generates the files in a `declarations` subfolder @@ -242,8 +243,8 @@ const didc = async () => { // That's why we have to post-process the results. const generatedFolder = join(DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH, 'declarations'); - await rename(join(generatedFolder, `${SATELLITE_PROJECT_NAME}.did.d.ts`), satellitedIdl('ts')); - await rename(join(generatedFolder, `${SATELLITE_PROJECT_NAME}.did.js`), satellitedIdl('js')); + await rename(join(generatedFolder, `${EXTENSION_DID_FILE_NAME}.d.ts`), satellitedIdl('ts')); + await rename(join(generatedFolder, `${EXTENSION_DID_FILE_NAME}.js`), satellitedIdl('js')); await rm(generatedFolder, {recursive: true, force: true}); };