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

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@
"@dfinity/utils": "^4.1.0",
"@icp-sdk/canisters": "^3.3.0",
"@icp-sdk/core": "^5.0.0",
"@junobuild/admin": "^4.1.0-next-2026-03-12",
"@junobuild/cdn": "^2.3.0-next-2026-03-12",
"@junobuild/cli-tools": "^0.10.2-next-2026-03-12",
"@junobuild/config": "^2.11.0-next-2026-03-12",
"@junobuild/config-loader": "^0.4.8-next-2026-03-12",
"@junobuild/core": "^5.2.0-next-2026-03-12",
"@junobuild/functions-tools": "^0.4.0-next-2026-03-12",
"@junobuild/ic-client": "^8.0.0-next-2026-03-12",
"@junobuild/storage": "^2.3.0-next-2026-03-12",
"@junobuild/utils": "^0.2.6-next-2026-03-12",
"@junobuild/zod": "^0.0.2-next-2026-03-12",
"@junobuild/admin": "^4.1.0-next-2026-03-12.2",
"@junobuild/cdn": "^2.3.0-next-2026-03-12.2",
"@junobuild/cli-tools": "^0.10.2-next-2026-03-12.2",
"@junobuild/config": "^2.11.0-next-2026-03-12.2",
"@junobuild/config-loader": "^0.4.8-next-2026-03-12.2",
"@junobuild/core": "^5.2.0-next-2026-03-12.2",
"@junobuild/functions-tools": "^0.4.0-next-2026-03-12.2",
"@junobuild/ic-client": "^8.0.0-next-2026-03-12.2",
"@junobuild/storage": "^2.3.0-next-2026-03-12.2",
"@junobuild/utils": "^0.2.6-next-2026-03-12.2",
"@junobuild/zod": "^0.0.2-next-2026-03-12.2",
"chokidar": "^4.0.3",
"conf": "^14.0.0",
"open": "^11.0.0",
Expand All @@ -56,7 +56,7 @@
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@junobuild/emulator-playwright": "^0.0.5",
"@junobuild/functions": "^0.5.6-next-2026-03-12",
"@junobuild/functions": "^0.5.6-next-2026-03-12.2",
"@playwright/test": "^1.58.1",
"@types/node": "24.10.9",
"@types/prompts": "^2.4.9",
Expand Down
111 changes: 111 additions & 0 deletions src/services/functions/build/build.api.services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {isNullish} from '@dfinity/utils';
import type {GenerateResultData} from '@junobuild/cli-tools';
import {
generateIdlApi as generateIdlApiLib,
generateZodApi as generateZodApiLib,
type TransformerOptions
} from '@junobuild/functions-tools';
import {existsSync} from 'node:fs';
import {mkdir, rm} from 'node:fs/promises';
import {detectJunoConfigType} from '../../../configs/juno.config';
import {DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH} from '../../../constants/dev.constants';
import type {BuildLang} from '../../../types/build';
import {satellitedIdl} from '../../../utils/build.utils';
import {readPackageJson} from '../../../utils/pkg.utils';

export const generateIdlApi = async () => {
const inputFile = satellitedIdl('ts');

if (!existsSync(inputFile)) {
return;
}

const detectedConfig = detectJunoConfigType();
const lang = detectedConfig?.configType === 'ts' ? 'ts' : 'mjs';

const generateFn: GenerateFn = async (params) => {
await generateIdlApiLib({inputFile, ...params});
};

await generateApi({generateFn, lang});
};

export const generateZodApi = async ({
generatedData,
lang
}: {
generatedData: GenerateResultData;
lang: Omit<BuildLang, 'rs'>;
}) => {
const {generate} = generatedData;

if (isNullish(generate)) {
const {outputFile} = buildOutput({lang});
await rm(outputFile, {force: true});
return;
}

const {updates, queries} = generate;
const functions = [...queries, ...updates];

const generateFn: GenerateFn = async (params) => {
await generateZodApiLib({
...params,
functions
});
};

await generateApi({generateFn, lang});
};

type GenerateFn = (params: {
outputFile: string;
transformerOptions: TransformerOptions;
}) => Promise<void>;

const generateApi = async ({
generateFn,
lang
}: {
generateFn: GenerateFn;
lang: Omit<BuildLang, 'rs'>;
}) => {
const readCoreLib = async (): Promise<'core' | 'core-standalone'> => {
try {
const {dependencies} = await readPackageJson();
return Object.keys(dependencies ?? {}).includes('@junobuild/core-standalone')
? 'core-standalone'
: 'core';
} catch (_err: unknown) {
// This should not block the developer therefore we fallback to core which is the common way of using the library
return 'core';
}
};

const coreLib = await readCoreLib();

// In TypeScript, unlike for Rust, the declarations folder might not exist yet when the functions
// are parsed for the first time
await mkdir(DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH, {recursive: true});

const {outputFile, outputLanguage} = buildOutput({lang});

await generateFn({
outputFile,
transformerOptions: {
outputLanguage,
coreLib
}
});
};

const buildOutput = ({
lang
}: {
lang: Omit<BuildLang, 'rs'>;
}): Pick<TransformerOptions, 'outputLanguage'> & {outputFile: string} => {
const outputLanguage = lang === 'mjs' ? 'js' : 'ts';
const outputFile = `${DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH}/satellite.api.${outputLanguage}`;

return {outputFile, outputLanguage};
};
117 changes: 20 additions & 97 deletions src/services/functions/build/build.did.services.ts
Original file line number Diff line number Diff line change
@@ -1,110 +1,33 @@
import {isNullish} from '@dfinity/utils';
import {spawn} from '@junobuild/cli-tools';
import {generateApi as generateApiLib} from '@junobuild/functions-tools';
import {existsSync} from 'node:fs';
import {readFile, rename, rm} from 'node:fs/promises';
import {join} from 'node:path';
import {detectJunoConfigType} from '../../../configs/juno.config';
import {type GenerateResultData} from '@junobuild/cli-tools';
import {generateDid as generateDidLib} from '@junobuild/functions-tools';
import {rm, writeFile} from 'node:fs/promises';
import {
AUTO_GENERATED,
EXTENSION_DID_FILE_NAME,
SATELLITE_CUSTOM_DID_FILE
SATELLITE_CUSTOM_DID_FILE,
SATELLITE_DID_FILE
} from '../../../constants/build.constants';
import {DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH} from '../../../constants/dev.constants';
import {checkLocalIcpBindgen} from '../../../utils/build.bindgen.utils';
import {readPackageJson} from '../../../utils/pkg.utils';
import {detectPackageManager} from '../../../utils/pm.utils';
import {readSatelliteDid} from '../../../utils/did.utils';

const satellitedIdl = (type: 'js' | 'ts'): string =>
`${DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH}/satellite.${type === 'ts' ? 'did.d.ts' : 'factory.did.js'}`;
export const generateJsTsDid = async ({generatedData}: {generatedData: GenerateResultData}) => {
const {generate} = generatedData;

export const generateDid = async () => {
// No satellite_extension.did and therefore no services to generate to JS and TS.
if (!existsSync(SATELLITE_CUSTOM_DID_FILE)) {
if (isNullish(generate)) {
await rm(SATELLITE_CUSTOM_DID_FILE, {force: true});
await rm(SATELLITE_DID_FILE, {force: true});
return;
}

// We check if the developer has added any API endpoints. If none, we do not need to generate the bindings for JS and TS.
const extensionDid = await readFile(SATELLITE_CUSTOM_DID_FILE, 'utf-8');
const noAdditionalExtensionDid = 'service : { build_version : () -> (text) query }';
const {updates, queries} = generate;

if (extensionDid.trim() === noAdditionalExtensionDid) {
return;
}

await executeIcpBindgen();

// 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');

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});
};

const executeIcpBindgen = async () => {
const pm = detectPackageManager();

// The assertion on checkIcpBindgen() which requires the installation of icp-bindgen
// is performed earlier to reaching this point. Therefore, we can optimistically
// assume that if no tool is available locally, it should be available globally.
const {valid: localValid} = await checkLocalIcpBindgen({pm});
const withGlobalCmd = localValid !== true;

const localCommand = pm === 'npm' || isNullish(pm) ? 'npx' : pm;

// --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: withGlobalCmd ? 'icp-bindgen' : localCommand,
args: [
...(withGlobalCmd ? [] : ['icp-bindgen']),
'--did-file',
SATELLITE_CUSTOM_DID_FILE,
'--out-dir',
DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH,
'--actor-disabled',
'--force'
],
silentOut: true
});
};

export const generateApi = async () => {
const inputFile = satellitedIdl('ts');

if (!existsSync(inputFile)) {
return;
}

const detectedConfig = detectJunoConfigType();
const outputLanguage = detectedConfig?.configType === 'ts' ? 'ts' : 'js';

const outputFile = `${DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH}/satellite.api.${outputLanguage}`;

const readCoreLib = async (): Promise<'core' | 'core-standalone'> => {
try {
const {dependencies} = await readPackageJson();
return Object.keys(dependencies ?? {}).includes('@junobuild/core-standalone')
? 'core-standalone'
: 'core';
} catch (_err: unknown) {
// This should not block the developer therefore we fallback to core which is the common way of using the library
return 'core';
}
};
await generateDidLib({updates, queries, outputFile: SATELLITE_CUSTOM_DID_FILE});

const coreLib = await readCoreLib();
const templateDid = await readSatelliteDid();

await generateApiLib({
inputFile,
outputFile,
transformerOptions: {
outputLanguage,
coreLib
}
});
await writeFile(
SATELLITE_DID_FILE,
`${AUTO_GENERATED}\n\nimport service "${EXTENSION_DID_FILE_NAME}";\n\n${templateDid}`,
'utf-8'
);
};
69 changes: 69 additions & 0 deletions src/services/functions/build/build.idl.services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {isNullish} from '@dfinity/utils';
import {spawn} from '@junobuild/cli-tools';
import {existsSync} from 'node:fs';
import {readFile, rename, rm} from 'node:fs/promises';
import {join} from 'node:path';
import {
EXTENSION_DID_FILE_NAME,
SATELLITE_CUSTOM_DID_FILE
} from '../../../constants/build.constants';
import {DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH} from '../../../constants/dev.constants';
import {checkLocalIcpBindgen} from '../../../utils/build.bindgen.utils';
import {satellitedIdl} from '../../../utils/build.utils';
import {detectPackageManager} from '../../../utils/pm.utils';

export const generateIdl = async () => {
// No satellite_extension.did and therefore no services to generate to JS and TS.
if (!existsSync(SATELLITE_CUSTOM_DID_FILE)) {
return;
}

// We check if the developer has added any API endpoints. If none, we do not need to generate the bindings for JS and TS.
const extensionDid = await readFile(SATELLITE_CUSTOM_DID_FILE, 'utf-8');
const noAdditionalExtensionDid = 'service : { build_version : () -> (text) query }';

if (extensionDid.trim() === noAdditionalExtensionDid) {
return;
}

await executeIcpBindgen();

// 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');

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});
};

const executeIcpBindgen = async () => {
const pm = detectPackageManager();

// The assertion on checkIcpBindgen() which requires the installation of icp-bindgen
// is performed earlier to reaching this point. Therefore, we can optimistically
// assume that if no tool is available locally, it should be available globally.
const {valid: localValid} = await checkLocalIcpBindgen({pm});
const withGlobalCmd = localValid !== true;

const localCommand = pm === 'npm' || isNullish(pm) ? 'npx' : pm;

// --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: withGlobalCmd ? 'icp-bindgen' : localCommand,
args: [
...(withGlobalCmd ? [] : ['icp-bindgen']),
'--did-file',
SATELLITE_CUSTOM_DID_FILE,
'--out-dir',
DEVELOPER_PROJECT_SATELLITE_DECLARATIONS_PATH,
'--actor-disabled',
'--force'
],
silentOut: true
});
};
Loading
Loading