Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
01bfb68
feat(cli-tools): generate dev custom functions
peterpeterparker Mar 6, 2026
4ebc9b1
feat: filter queries
peterpeterparker Mar 6, 2026
7041035
feat: in progress
peterpeterparker Mar 7, 2026
688ad79
chore: revert
peterpeterparker Mar 7, 2026
179beb8
Merge branch 'main' into feat/generate-functions
peterpeterparker Mar 7, 2026
911717a
feat: redo global
peterpeterparker Mar 7, 2026
b391fc4
feat: fix evaluate function and map updates or queries
peterpeterparker Mar 7, 2026
f87596c
feat: no queries no updates nothing to do
peterpeterparker Mar 7, 2026
eb0ddc0
feat: move generate to did-tools
peterpeterparker Mar 7, 2026
4450dd4
Merge branch 'main' into feat/generate-functions
peterpeterparker Mar 7, 2026
631d4c7
feat: start parsing
peterpeterparker Mar 7, 2026
b311795
Merge branch 'main' into feat/generate-functions
peterpeterparker Mar 7, 2026
8639390
feat: parse
peterpeterparker Mar 7, 2026
fcb4acc
feat: parse
peterpeterparker Mar 7, 2026
4397c82
feat: zod-schemas
peterpeterparker Mar 7, 2026
eb99027
chore: merge main
peterpeterparker Mar 7, 2026
653ea25
feat: parse
peterpeterparker Mar 7, 2026
2366a77
Merge branch 'main' into feat/generate-functions
peterpeterparker Mar 7, 2026
07336a5
feat: update order for dependencies tree
peterpeterparker Mar 7, 2026
8f45726
fix: async
peterpeterparker Mar 7, 2026
e096318
chore: fmt
peterpeterparker Mar 7, 2026
f35c226
chore: cleanup
peterpeterparker Mar 7, 2026
6599108
chore: redo
peterpeterparker Mar 7, 2026
1c07180
Merge branch 'main' into feat/generate-functions
peterpeterparker Mar 7, 2026
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
2 changes: 2 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions packages/cli-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@dfinity/utils": "^4.1",
"@junobuild/cdn": "^2.3",
"@junobuild/config": "^2.7",
"@junobuild/did-tools": "^0.3.10",
"@junobuild/storage": "^2.3",
"esbuild": "^0.27.0",
"ora": "^9"
Expand Down
10 changes: 2 additions & 8 deletions packages/cli-tools/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,13 @@ import {rm} from 'node:fs/promises';
*/
export const buildFunctions = async ({
infile,
outfile,
banner
}: {
infile: string;
outfile: string;
banner?: {[type: string]: string};
}): Promise<Omit<EsbuildResult, 'outputFiles'>> => {
const {outputFiles: _, ...rest} = await esbuild({
}): Promise<EsbuildResult> =>
await esbuild({
infile,
outfile,
platform: 'browser',
treeShaking: true,
define: {
Expand All @@ -45,9 +42,6 @@ export const buildFunctions = async ({
banner
});

return rest;
};

/**
* Builds a script using `esbuild` for `juno run`.
*
Expand Down
120 changes: 120 additions & 0 deletions packages/cli-tools/src/commands/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {isNullish} from '@dfinity/utils';
import {generateFunctions} from '@junobuild/did-tools';
import type {Metafile} from 'esbuild';
import {writeFile} from 'node:fs/promises';
import {buildFunctions} from './build';

export interface GenerateArgs {
infile: string;
outfileJs: string;
outfileRs: string;
banner?: {[type: string]: string};
}

export interface GenerateResultData {
version: string;
output: [string, Metafile['outputs'][0]];
}

type GenerateBuildResultData = GenerateResultData & {code: Uint8Array};

export interface GenerateResultError {
status: 'error';
warnings?: string[];
errors: string[];
}

export type GenerateResult = {status: 'success'; result: GenerateResultData} | GenerateResultError;

export type GenerateCodeResult =
| {status: 'success'; result: GenerateBuildResultData}
| GenerateResultError;

export const buildAndGenerateFunctions = async ({
outfileJs,
outfileRs,
...args
}: GenerateArgs): Promise<GenerateResult> => {
const build = await buildWithEsbuild(args);

if (build.status === 'error') {
return build;
}

const {
result: {code, ...buildResult}
} = build;

// We generate the custom functions before the JavaScript script because
// there might be none, therefore no Rust module to generate but, also because
// the Docker image currently watches the script to initiate new automatic build.
await writeDevFunctions({code, outfileRs});

await writeDevScript({code, outfileJs});

return {status: 'success', result: buildResult};
};

const buildWithEsbuild = async ({
infile,
banner
}: Omit<GenerateArgs, 'outfileJs' | 'outfileRs'>): Promise<GenerateCodeResult> => {
const {
metafile,
errors: buildErrors,
warnings: buildWarnings,
version,
outputFiles
} = await buildFunctions({
infile,
banner
});

const warnings = buildWarnings.map(({text}) => text);
const errors = buildErrors.map(({text}) => text);

if (errors.length > 0) {
return {status: 'error', errors, warnings};
}

const entry = Object.entries(metafile.outputs);

if (entry.length === 0) {
return {
status: 'error',
errors: ['Unexpected: No metafile resulting from the build was found.']
};
}

const code = outputFiles?.[0]?.contents;

if (isNullish(code)) {
return {
status: 'error',
errors: ['Unexpected: No script build for the functions.']
};
}

return {
status: 'success',
result: {
output: entry[0],
version,
code
}
};
};

const writeDevFunctions = async ({
code,
outfileRs
}: Pick<GenerateBuildResultData, 'code'> & Pick<GenerateArgs, 'outfileRs'>) => {
await generateFunctions({code, outputFile: outfileRs});
};

const writeDevScript = async ({
code,
outfileJs
}: Pick<GenerateBuildResultData, 'code'> & Pick<GenerateArgs, 'outfileJs'>) => {
await writeFile(outfileJs, code);
};
1 change: 1 addition & 0 deletions packages/cli-tools/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './commands/build';
export * from './commands/deploy';
export * from './commands/generate';
export * from './commands/publish';
export * from './constants/deploy.constants';
export type * from './types/deploy';
Expand Down
1 change: 1 addition & 0 deletions packages/did-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@babel/types": "7.28.4",
"@dfinity/utils": "^4.1",
"@dfinity/zod-schemas": "^3.1.0",
"@junobuild/functions": "^0.5.6",
"zod": "^4"
}
}
65 changes: 65 additions & 0 deletions packages/did-tools/src/functions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type {Query, Update} from '@junobuild/functions';
import {writeFile} from 'node:fs/promises';
import {parseFunctions} from './services/parser.services';

export interface GenerateArgs {
outputFile: string;
code: Uint8Array;
}

export const generateFunctions = async ({code, outputFile}: GenerateArgs) => {
const originalConsole = globalThis.console;
const originalRandom = globalThis.Math.random;

try {
// @junobuild/functions replaces globalThis.console with a version that calls
// __ic_cdk_print, which only exists in the WASM host environment. We stub it
// so the import doesn't throw when the module is evaluated in Node.
globalThis.__ic_cdk_print = (msg: string) => process.stdout.write(`${msg}\n`);

// It might be needed
// globalThis.__juno_satellite_random = () => {
// const buf = new Uint32Array(1);
// crypto.getRandomValues(buf);
// return buf[0];
// };

const devModule = await import(
`data:text/javascript;base64,${Buffer.from(code).toString(`base64`)}`
);

// Lazy load the functions this way it uses the globalThis stubs we defined above
const {QuerySchema, UpdateSchema} = await import('@junobuild/functions');

const [queries, updates] = Object.entries(devModule).reduce<
[[string, Query][], [string, Update][]]
>(
([queries, updates], entry) => {
const [key, value] = entry;

const config = typeof value === 'function' ? value({}) : value;

const query = QuerySchema.safeParse(config);
const update = UpdateSchema.safeParse(config);

return [
[...queries, ...(query.success ? [[key, query.data] as [string, Query]] : [])],
[...updates, ...(update.success ? [[key, update.data] as [string, Update]] : [])]
];
},
[[], []]
);

// No custom functions to generate
if (queries.length === 0 && updates.length === 0) {
return;
}

const functions = parseFunctions({queries, updates});

await writeFile(outputFile, functions, 'utf-8');
} finally {
globalThis.console = originalConsole;
globalThis.Math.random = originalRandom;
}
};
Loading