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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## DFX

### fix(generate): add missing typescript types and fix issues with bindings array in dfx.json

### chore: update Candid UI canister with commit 79d55e7f568aec00e16dd0329926cc7ea8e3a28b

### refactor: Factor out code for calling arbitrary bundled binaries
Expand Down
86 changes: 85 additions & 1 deletion e2e/tests-dfx/generate.bash
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ teardown() {
dfx build
dfx canister install --all

dfx --version
dfx generate

assert_file_exists "src/declarations/hello_backend/hello_backend.did"
Expand All @@ -29,3 +28,88 @@ teardown() {
assert_file_exists "src/declarations/hello_backend/index.js"
assert_file_exists "src/declarations/hello_backend/index.d.ts"
}

@test "dfx generate creates only JS files" {
jq '.canisters.hello_backend.declarations.bindings=["js"]' dfx.json | sponge dfx.json

dfx_start
dfx canister create --all
dfx build
dfx canister install --all

dfx generate

assert_file_not_exists "src/declarations/hello_backend/hello_backend.did"
assert_file_exists "src/declarations/hello_backend/hello_backend.did.js"
assert_file_not_exists "src/declarations/hello_backend/hello_backend.did.d.ts"
assert_file_exists "src/declarations/hello_backend/index.js"
assert_file_not_exists "src/declarations/hello_backend/index.d.ts"
}

@test "dfx generate creates only TS files" {
jq '.canisters.hello_backend.declarations.bindings=["ts"]' dfx.json | sponge dfx.json

dfx_start
dfx canister create --all
dfx build
dfx canister install --all

dfx generate

assert_file_not_exists "src/declarations/hello_backend/hello_backend.did"
assert_file_not_exists "src/declarations/hello_backend/hello_backend.did.js"
assert_file_exists "src/declarations/hello_backend/hello_backend.did.d.ts"
assert_file_not_exists "src/declarations/hello_backend/index.js"
assert_file_exists "src/declarations/hello_backend/index.d.ts"
}

@test "dfx generate creates only JS & TS files" {
jq '.canisters.hello_backend.declarations.bindings=["js", "ts"]' dfx.json | sponge dfx.json

dfx_start
dfx canister create --all
dfx build
dfx canister install --all

dfx generate

assert_file_not_exists "src/declarations/hello_backend/hello_backend.did"
assert_file_exists "src/declarations/hello_backend/hello_backend.did.js"
assert_file_exists "src/declarations/hello_backend/hello_backend.did.d.ts"
assert_file_exists "src/declarations/hello_backend/index.js"
assert_file_exists "src/declarations/hello_backend/index.d.ts"
}

@test "dfx generate creates only DID files" {
jq '.canisters.hello_backend.declarations.bindings=["did"]' dfx.json | sponge dfx.json

dfx_start
dfx canister create --all
dfx build
dfx canister install --all

dfx generate

assert_file_exists "src/declarations/hello_backend/hello_backend.did"
assert_file_not_exists "src/declarations/hello_backend/hello_backend.did.js"
assert_file_not_exists "src/declarations/hello_backend/hello_backend.did.d.ts"
assert_file_not_exists "src/declarations/hello_backend/index.js"
assert_file_not_exists "src/declarations/hello_backend/index.d.ts"
}

@test "dfx generate does not create any files" {
jq '.canisters.hello_backend.declarations.bindings=[]' dfx.json | sponge dfx.json

dfx_start
dfx canister create --all
dfx build
dfx canister install --all

dfx generate

assert_file_not_exists "src/declarations/hello_backend/hello_backend.did"
assert_file_not_exists "src/declarations/hello_backend/hello_backend.did.js"
assert_file_not_exists "src/declarations/hello_backend/hello_backend.did.d.ts"
assert_file_not_exists "src/declarations/hello_backend/index.js"
assert_file_not_exists "src/declarations/hello_backend/index.d.ts"
}
4 changes: 4 additions & 0 deletions src/dfx/assets/language_bindings/index.d.ts.hbs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { ActorSubclass, HttpAgentOptions, ActorConfig } from '@dfinity/agent';
import { Principal } from '@dfinity/principal';
import { IDL } from '@dfinity/candid';

import { _SERVICE } from './{{canister_name}}.did';

export declare const idlFactory: IDL.InterfaceFactory;
export declare const canisterId: string;

export declare interface CreateActorOptions {
agentOptions?: HttpAgentOptions;
actorOptions?: ActorConfig;
Expand Down
177 changes: 96 additions & 81 deletions src/dfx/src/lib/builders/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,6 @@ pub trait CanisterBuilder {
)
})?;
}
std::fs::create_dir_all(&generate_output_dir).with_context(|| {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was generating the declarations directory and the DID file before checking if the bindings array is empty.
There's some code lower down in this file that deletes the DID file, but we never get that far because of the early return for an empty bindings array.

So the old flow was like this:

  • create directory
  • create did file
  • if bindings array is empty, return early
  • create JS bindings if applicable
  • create TS bindings if applicable
  • delete DID bindings if not applicable

Now it's like this:

  • if bindings array is empty, return early
  • create directory
  • create did file
  • create JS bindings if applicable
  • create TS bindings if applicable
  • delete DID bindings if not applicable

format!(
"Failed to create dir: {}",
generate_output_dir.to_string_lossy()
)
})?;

let generated_idl_path = self.generate_idl(pool, info, config)?;

let (env, ty) = check_candid_file(generated_idl_path.as_path())?;

let bindings = info
.get_declarations_config()
Expand All @@ -145,6 +135,17 @@ pub trait CanisterBuilder {
);
}

std::fs::create_dir_all(&generate_output_dir).with_context(|| {
format!(
"Failed to create dir: {}",
generate_output_dir.to_string_lossy()
)
})?;

let generated_idl_path = self.generate_idl(pool, info, config)?;

let (env, ty) = check_candid_file(generated_idl_path.as_path())?;

// Typescript
if bindings.contains(&"ts".to_string()) {
let output_did_ts_path = generate_output_dir
Expand All @@ -158,6 +159,8 @@ pub trait CanisterBuilder {
)
})?;
eprintln!(" {}", &output_did_ts_path.display());

compile_handlebars_files("ts", info, generate_output_dir)?;
}

// Javascript
Expand All @@ -174,78 +177,8 @@ pub trait CanisterBuilder {
)
})?;
eprintln!(" {}", &output_did_js_path.display());
// index.js
let mut language_bindings = crate::util::assets::language_bindings()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this to a separate function that takes a param "ts" or "js" and will only generate the files for the provided extension.

.context("Failed to get language bindings archive.")?;
for f in language_bindings
.entries()
.context("Failed to read language bindings archive entries.")?
{
let mut file = f.context("Failed to read language bindings archive entry.")?;

let pathname: PathBuf = file
.path()
.context("Failed to read language bindings entry path name.")?
.to_path_buf();
let extension = pathname.extension();
let is_template = matches! (extension, Some (ext ) if ext == OsStr::new("hbs"));

if is_template {
let mut file_contents = String::new();
file.read_to_string(&mut file_contents)
.context("Failed to read language bindings archive file content.")?;

// create the handlebars registry
let handlebars = Handlebars::new();

let mut data: BTreeMap<String, &String> = BTreeMap::new();

let canister_name = &info.get_name().to_string();

let node_compatibility = info.get_declarations_config().node_compatibility;

// Insert only if node outputs are specified
let actor_export = if node_compatibility {
// leave empty for nodejs
"".to_string()
} else {
format!(
r#"

/**
* A ready-to-use agent for the {0} canister
* @type {{import("@dfinity/agent").ActorSubclass<import("./{0}.did.js")._SERVICE>}}
*/
export const {0} = createActor(canisterId);"#,
canister_name
)
.to_string()
};

data.insert("canister_name".to_string(), canister_name);
data.insert("actor_export".to_string(), &actor_export);

let process_string: String = match &info.get_declarations_config().env_override
{
Some(s) => format!(r#""{}""#, s.clone()),
None => {
format!(
"process.env.{}{}",
&canister_name.to_ascii_uppercase(),
"_CANISTER_ID"
)
}
};

data.insert("canister_name_process_env".to_string(), &process_string);

let new_file_contents =
handlebars.render_template(&file_contents, &data).unwrap();
let new_path = generate_output_dir.join(pathname.with_extension(""));
std::fs::write(&new_path, new_file_contents)
.with_context(|| format!("Failed to write to {}.", new_path.display()))?;
}
}
compile_handlebars_files("js", info, generate_output_dir)?;
}

// Motoko
Expand All @@ -268,6 +201,7 @@ export const {0} = createActor(canisterId);"#,
} else {
eprintln!(" {}", &generated_idl_path.display());
}

Ok(())
}

Expand All @@ -281,6 +215,87 @@ export const {0} = createActor(canisterId);"#,
}
}

fn compile_handlebars_files(
lang: &str,
info: &CanisterInfo,
generate_output_dir: &Path,
) -> DfxResult {
// index.js
let mut language_bindings = crate::util::assets::language_bindings()
.context("Failed to get language bindings archive.")?;
for f in language_bindings
.entries()
.context("Failed to read language bindings archive entries.")?
{
let mut file = f.context("Failed to read language bindings archive entry.")?;

let pathname: PathBuf = file
.path()
.context("Failed to read language bindings entry path name.")?
.to_path_buf();
let file_extension = format!("{}.hbs", lang);
let is_template = pathname
.to_str()
.map_or(false, |name| name.ends_with(&file_extension));

if is_template {
let mut file_contents = String::new();
file.read_to_string(&mut file_contents)
.context("Failed to read language bindings archive file content.")?;

// create the handlebars registry
let handlebars = Handlebars::new();

let mut data: BTreeMap<String, &String> = BTreeMap::new();

let canister_name = &info.get_name().to_string();

let node_compatibility = info.get_declarations_config().node_compatibility;

// Insert only if node outputs are specified
let actor_export = if node_compatibility {
// leave empty for nodejs
"".to_string()
} else {
format!(
r#"

/**
* A ready-to-use agent for the {0} canister
* @type {{import("@dfinity/agent").ActorSubclass<import("./{0}.did.js")._SERVICE>}}
*/
export const {0} = createActor(canisterId);"#,
canister_name
)
.to_string()
};

data.insert("canister_name".to_string(), canister_name);
data.insert("actor_export".to_string(), &actor_export);

let process_string: String = match &info.get_declarations_config().env_override {
Some(s) => format!(r#""{}""#, s.clone()),
None => {
format!(
"process.env.{}{}",
&canister_name.to_ascii_uppercase(),
"_CANISTER_ID"
)
}
};

data.insert("canister_name_process_env".to_string(), &process_string);

let new_file_contents = handlebars.render_template(&file_contents, &data).unwrap();
let new_path = generate_output_dir.join(pathname.with_extension(""));
std::fs::write(&new_path, new_file_contents)
.with_context(|| format!("Failed to write to {}.", new_path.display()))?;
}
}

Ok(())
}

// TODO: this function was copied from src/lib/models/canister.rs
fn ensure_trailing_newline(s: String) -> String {
if s.ends_with('\n') {
Expand Down