There is probably an easier way but I generally update the generate script in package.json
to chain it with a custom made script
UPDATE: indeed there is an easier way than following script:
// in dfx.json
"mycanister": {
"declarations": {
"node_compatibility": true,
"env_override": ""
}
}
source: With typescript how can I directly use the types created by "dfx generate"? - #3 by kpeacock
"generate": "dfx generate && node scripts/update.types.mjs",
In this custom made script, I do two things:
- I remove the default
createActor
andcanisterId
automatically generated because they crash my build and I want to use my own ids and actor functions - I move the auto generated
.did.js
to another file name such as.factory.did.js
for TypeScript compatibility
The first part might not fits your need but the second my help you. e.g. such a script:
#!/usr/bin/env node
import { existsSync, readdirSync } from 'fs';
import { readFile, rename, writeFile } from 'fs/promises';
import { join } from 'path';
/**
* We have to manipulate the types as long as https://github.com/dfinity/sdk/discussions/2761 is not implemented
*/
const cleanTypes = async ({ dest = `./src/declarations` }) => {
const promises = readdirSync(dest).map(
(dir) =>
new Promise(async (resolve) => {
const indexPath = join(dest, dir, 'index.js');
if (!existsSync(indexPath)) {
resolve();
return;
}
const content = await readFile(indexPath, 'utf-8');
const clean = content
.replace(/export const \w* = createActor\(canisterId\);/g, '')
.replace(/export const canisterId = process\.env\.\w*_CANISTER_ID;/g, '');
await writeFile(indexPath, clean, 'utf-8');
resolve();
})
);
await Promise.all(promises);
};
const renameFactory = async ({ dest = `./src/declarations` }) => {
const promises = readdirSync(dest).map(
(dir) =>
new Promise(async (resolve) => {
const factoryPath = join(dest, dir, `${dir}.did.js`);
const formattedPath = join(dest, dir, `${dir}.factory.did.js`);
if (!existsSync(factoryPath)) {
resolve();
return;
}
await rename(factoryPath, formattedPath);
resolve();
})
);
await Promise.all(promises);
};
(async () => {
try {
await cleanTypes({});
await renameFactory({});
console.log(`Types declarations copied!`);
} catch (err) {
console.error(`Error while copying the types declarations.`, err);
}
})();
Once this in place and assuming the types find place in src/declarations
I can use these in my libs and apps in TypeScript.
e.g. creating an actor for the Cmc canister becomes:
import type { _SERVICE as CMCActor } from '$declarations/cmc/cmc.did';
import { idlFactory as idlFactorCMC } from '$declarations/cmc/cmc.factory.did';
etc.
export const getCMCActor = async (): Promise<CMCActor> => {
// Canister IDs are automatically expanded to .env config - see vite.config.ts
const canisterId = import.meta.env.VITE_IC_CMC_CANISTER_ID;
const agent = await getAgent({ identity: new AnonymousIdentity() });
return Actor.createActor(idlFactorCMC, {
agent,
canisterId
});
};
Importing the types work then through the .did
files as well. e.g. from one of my custom canister:
import type { Doc } from '$declarations/satellite/satellite.did';
Does that help?
Update: I shoud add that $declarations/
is my relative path to declaration which I set in tsconfig
to avoid having ../../../etc
everywhere.
It works with SvelteKit, not sure it works out of the box with tsc.
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
...
"paths": {
"$lib": ["src/frontend/src/lib"],
"$lib/*": ["src/frontend/src/lib/*"],
"$declarations": ["src/declarations"],
"$declarations/*": ["src/declarations/*"]
}
}
}