diff --git a/CHANGELOG.md b/CHANGELOG.md index 795f2b53ba..2b66c8b16c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ # UNRELEASED +### feat: new starter templates + +`dfx new` now has a new set of customizable project templates and an interactive menu for selecting them. Supports the Svelte, Vue, and React frameworks, and Azle and Kybra backends. + +### fix: --no-frontend no longer creates a frontend + +Previously `dfx new --no-frontend` still created a frontend canister. This behavior is now accessed via `--frontend simple-assets`. + ### feat: `dfx cycles redeem-faucet-coupon` It is now possible to redeem faucet coupons to cycles ledger accounts. diff --git a/Cargo.lock b/Cargo.lock index e47c0bb252..64ce796246 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1596,6 +1596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" dependencies = [ "console", + "fuzzy-matcher", "shell-words", "tempfile", "thiserror", @@ -2110,6 +2111,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "generic-array" version = "0.14.7" diff --git a/docs/cli-reference/dfx-new.md b/docs/cli-reference/dfx-new.md index 4e67dde235..32bde55cdf 100644 --- a/docs/cli-reference/dfx-new.md +++ b/docs/cli-reference/dfx-new.md @@ -4,6 +4,8 @@ Use the `dfx new` command to create a new project for the IC. This command creat You can use the `--dry-run` option to preview the directories and files to be created without adding them to the file system. +If called without any arguments besides the project name, it will interactively prompt you for the values of `--type`, `--frontend`, and `--extras`. + ## Basic usage ``` bash @@ -14,11 +16,13 @@ dfx new _project_name_ [flag] You can use the following optional flags with the `dfx new` command: -| Flag | Description | -|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `--dry-run` | Generates a preview the directories and files to be created for a new project without adding them to the file system. | -| `--frontend` | Installs the template frontend code for the default project canister. The default value for the flag is `true` if `node.js` is currently installed on your local computer. If `node.js` is not currently installed, you can set this flag to `true` to attempt to install `node.js` and the template file when creating the project or you can set the flag to `false` to skip the installation of template frontend code entirely. | | -| `--no-frontend` | Skips installing the frontend template code and instead creates an asset canister with a dummy `.txt` file. This is the default behavior if `node.js` is currently not installed on your computer. | +| Flag | Description | +|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--dry-run` | Generates a preview of the directories and files to be created for a new project without adding them to the file system. | +| `--extras ` | Comma-separated list of additional features to add to the project template. `bitcoin` and `internet-identity` will insert the appropriate boilerplate into `dfx.json`, and `frontend-tests` adds a `vitest` skeleton to the frontend project. | +| `--frontend ` | Installs the template frontend code for the default project canister. The default value for the flag is `vanilla` if `node.js` is currently installed on your local computer. If `node.js` is not currently installed, you can set this flag to attempt to install `node.js` and the template file when creating the project or you can set the flag to `none` to skip the installation of template frontend code entirely. Possible values: `svelte`, `react`, `vue`, `vanilla`, `plain-assets`, `none`. | +| `--no-frontend` | Skips installing the frontend template code. This is the default behavior if `node.js` is currently not installed on your computer. Equivalent to `--frontend none`. | +| `--type ` | Selects the template backend code for the default project canister. The default value for the flag is `motoko`. Possible values: `motoko`, `rust`, `azle`, `kybra`. | ## Arguments diff --git a/docs/dfx-json-schema.json b/docs/dfx-json-schema.json index 842511a148..5c7e28a90b 100644 --- a/docs/dfx-json-schema.json +++ b/docs/dfx-json-schema.json @@ -237,6 +237,14 @@ "enum": [ "assets" ] + }, + "workspace": { + "title": "NPM workspace", + "description": "The workspace in package.json that this canister is in, if it is not in the root workspace.", + "type": [ + "string", + "null" + ] } } }, @@ -1068,4 +1076,4 @@ ] } } -} \ No newline at end of file +} diff --git a/e2e/assets/expect_scripts/rust_svelte_with_tests_and_ii.exp b/e2e/assets/expect_scripts/rust_svelte_with_tests_and_ii.exp new file mode 100755 index 0000000000..5b42989ac4 --- /dev/null +++ b/e2e/assets/expect_scripts/rust_svelte_with_tests_and_ii.exp @@ -0,0 +1,21 @@ +#!/usr/bin/expect -df + +match_max 100000 +set timeout 30 + +spawn dfx new e2e_project +expect "Select a backend language:" +# down arrow, Rust should be option 2 +send "\033\[B" +send "\r" + +expect "Select a frontend framework:" +# no down arrow, Svelte should be option 1 +send "\r" +# first and third, should be II and frontend tests respectively +expect "Add extra features" +send " " +send "\033\[B\033\[B" +send " " +send "\r" +expect eof diff --git a/e2e/tests-dfx/assetscanister.bash b/e2e/tests-dfx/assetscanister.bash index bb55041959..56e6c3aca3 100644 --- a/e2e/tests-dfx/assetscanister.bash +++ b/e2e/tests-dfx/assetscanister.bash @@ -5,7 +5,7 @@ load ../utils/_ setup() { standard_setup - dfx_new + dfx_new_assets } teardown() { @@ -91,6 +91,7 @@ check_permission_failure() { PREPARE_PRINCIPAL=$(dfx identity get-principal --identity prepare) COMMIT_PRINCIPAL=$(dfx identity get-principal --identity commit) + rm src/e2e_project_frontend/assets/.ic-assets.json5 install_asset assetscanister # Prep for a DeleteAsset operation echo "to-be-deleted" >src/e2e_project_frontend/assets/to-be-deleted.txt @@ -196,6 +197,7 @@ check_permission_failure() { PREPARE_PRINCIPAL=$(dfx identity get-principal --identity prepare) COMMIT_PRINCIPAL=$(dfx identity get-principal --identity commit) + rm src/e2e_project_frontend/assets/.ic-assets.json5 install_asset assetscanister dfx_start mkdir tmp @@ -1626,6 +1628,7 @@ WARN: { } @test "asset configuration via .ic-assets.json5 - pretty printing when deploying" { + rm src/e2e_project_frontend/assets/.ic-assets.json5 install_asset assetscanister dfx_start @@ -1668,6 +1671,7 @@ WARN: { } @test "syncs asset properties when redeploying" { + rm src/e2e_project_frontend/assets/.ic-assets.json5 install_asset assetscanister dfx_start assert_command dfx deploy diff --git a/e2e/tests-dfx/bitcoin.bash b/e2e/tests-dfx/bitcoin.bash index 58dbe31a2d..eeb425e985 100644 --- a/e2e/tests-dfx/bitcoin.bash +++ b/e2e/tests-dfx/bitcoin.bash @@ -37,7 +37,7 @@ set_local_network_bitcoin_enabled() { } @test "dfx restarts replica when ic-btc-adapter restarts" { - dfx_new hello + dfx_new_assets hello dfx_start --enable-bitcoin install_asset greet diff --git a/e2e/tests-dfx/build_granular.bash b/e2e/tests-dfx/build_granular.bash index d614d63d4d..1b8d971250 100644 --- a/e2e/tests-dfx/build_granular.bash +++ b/e2e/tests-dfx/build_granular.bash @@ -5,7 +5,7 @@ load ../utils/_ setup() { standard_setup - dfx_new + dfx_new_assets } teardown() { diff --git a/e2e/tests-dfx/canister_http.bash b/e2e/tests-dfx/canister_http.bash index feb45c7b6b..f7eddcf1cb 100644 --- a/e2e/tests-dfx/canister_http.bash +++ b/e2e/tests-dfx/canister_http.bash @@ -46,7 +46,7 @@ set_shared_local_network_canister_http_empty() { } @test "dfx restarts replica when ic-https-outcalls-adapter restarts" { - dfx_new hello + dfx_new_assets hello dfx_start install_asset greet diff --git a/e2e/tests-dfx/create.bash b/e2e/tests-dfx/create.bash index 3bd22dcfac..2f78fe0be7 100644 --- a/e2e/tests-dfx/create.bash +++ b/e2e/tests-dfx/create.bash @@ -6,7 +6,7 @@ load ../utils/cycles-ledger setup() { standard_setup - dfx_new + dfx_new_assets } teardown() { diff --git a/e2e/tests-dfx/deploy.bash b/e2e/tests-dfx/deploy.bash index d8949591cd..8161531025 100644 --- a/e2e/tests-dfx/deploy.bash +++ b/e2e/tests-dfx/deploy.bash @@ -6,7 +6,7 @@ load ../utils/cycles-ledger setup() { standard_setup - dfx_new hello + dfx_new_assets hello } teardown() { diff --git a/e2e/tests-dfx/dotenv.bash b/e2e/tests-dfx/dotenv.bash index 2f92e07d0e..c486cc25e4 100644 --- a/e2e/tests-dfx/dotenv.bash +++ b/e2e/tests-dfx/dotenv.bash @@ -5,7 +5,7 @@ load ../utils/_ setup() { standard_setup - dfx_new + dfx_new_assets } teardown() { diff --git a/e2e/tests-dfx/error_diagnosis.bash b/e2e/tests-dfx/error_diagnosis.bash index cf454be310..cf3bf1ce8d 100644 --- a/e2e/tests-dfx/error_diagnosis.bash +++ b/e2e/tests-dfx/error_diagnosis.bash @@ -12,20 +12,6 @@ teardown() { standard_teardown } -@test "Duplicate assets in dist/ from src/" { - dfx_new_frontend hello - install_asset greet - dfx_start - assert_command dfx deploy - - # simulate previous deploy with CopyPlugin step - cp src/hello_frontend/assets/* dist/hello_frontend/ - - assert_command_fail dfx deploy - assert_contains "Remove the CopyPlugin step from webpack.config.js" - assert_contains "Delete all files from the dist/ directory" -} - @test "HTTP 403 has a full diagnosis" { dfx_new hello install_asset greet diff --git a/e2e/tests-dfx/frontend.bash b/e2e/tests-dfx/frontend.bash index a4cf0c601d..e50a510997 100644 --- a/e2e/tests-dfx/frontend.bash +++ b/e2e/tests-dfx/frontend.bash @@ -65,8 +65,8 @@ teardown() { assert_match "Connection refused" } -@test "dfx uses .ic-assets.json file provided in src/__project_name__frontend/src" { - echo '[{"match": "*", "headers": {"x-key": "x-value"}}]' > src/e2e_project_frontend/src/.ic-assets.json +@test "dfx uses .ic-assets.json file provided in src/__project_name__frontend/assets" { + echo '[{"match": "*", "headers": {"x-key": "x-value"}}]' > src/e2e_project_frontend/assets/.ic-assets.json5 dfx_start dfx canister create --all @@ -77,14 +77,14 @@ teardown() { PORT=$(get_webserver_port) assert_command curl -vv http://localhost:"$PORT"/?canisterId="$ID" assert_match "< x-key: x-value" - assert_command curl -vv http://localhost:"$PORT"/index.js?canisterId="$ID" + assert_command curl -vv http://localhost:"$PORT"/favicon.ico?canisterId="$ID" assert_match "< x-key: x-value" } @test "dfx uses a custom build command if one is provided" { - jq '.canisters.e2e_project_frontend.source = ["dist/e2e_project_frontend/"]' dfx.json | sponge dfx.json - jq '.canisters.e2e_project_frontend.build = ["npm run custom-build"]' dfx.json | sponge dfx.json - jq '.scripts["custom-build"] = "mkdir -p ./dist/e2e_project_frontend/assets/ && cp -r ./src/e2e_project_frontend/assets/* ./dist/e2e_project_frontend"' package.json | sponge package.json + jq '.canisters.e2e_project_frontend.source = ["src/e2e_project_frontend/dist2"]' dfx.json | sponge dfx.json + jq '.canisters.e2e_project_frontend.build = ["npm run custom-build --workspace e2e_project_frontend"]' dfx.json | sponge dfx.json + jq '.scripts["custom-build"] = "npm run build && mkdir -p ./dist2/ && cp -r ./dist/* ./dist2"' src/e2e_project_frontend/package.json | sponge src/e2e_project_frontend/package.json dfx_start dfx canister create --all @@ -94,6 +94,6 @@ teardown() { ID=$(dfx canister id e2e_project_frontend) PORT=$(get_webserver_port) - assert_command curl -vv http://localhost:"$PORT"/sample-asset.txt?canisterId="$ID" - assert_match "This is a sample asset!" + assert_command curl -vv http://localhost:"$PORT"/index.html?canisterId="$ID" + assert_match "IC Hello Starter" } diff --git a/e2e/tests-dfx/identity.bash b/e2e/tests-dfx/identity.bash index 3867be21b7..2c9c578cfb 100644 --- a/e2e/tests-dfx/identity.bash +++ b/e2e/tests-dfx/identity.bash @@ -5,7 +5,7 @@ load ../utils/_ setup() { standard_setup - dfx_new + dfx_new_assets } teardown() { diff --git a/e2e/tests-dfx/install.bash b/e2e/tests-dfx/install.bash index e092ad963d..c62382b4de 100644 --- a/e2e/tests-dfx/install.bash +++ b/e2e/tests-dfx/install.bash @@ -5,7 +5,7 @@ load ../utils/_ setup() { standard_setup - dfx_new + dfx_new_assets } teardown() { diff --git a/e2e/tests-dfx/metadata.bash b/e2e/tests-dfx/metadata.bash index 8fee87cdf2..5e1c34c962 100644 --- a/e2e/tests-dfx/metadata.bash +++ b/e2e/tests-dfx/metadata.bash @@ -186,7 +186,7 @@ teardown() { } @test "asset canister provides candid:service metadata" { - dfx_new hello + dfx_new_assets hello dfx_start assert_command dfx deploy diff --git a/e2e/tests-dfx/mode_reinstall.bash b/e2e/tests-dfx/mode_reinstall.bash index a21e4bdd5d..065c109f2f 100644 --- a/e2e/tests-dfx/mode_reinstall.bash +++ b/e2e/tests-dfx/mode_reinstall.bash @@ -5,7 +5,7 @@ load ../utils/_ setup() { standard_setup - dfx_new hello + dfx_new_assets hello } teardown() { diff --git a/e2e/tests-dfx/new.bash b/e2e/tests-dfx/new.bash index 6559b2d5b6..73e94099a5 100644 --- a/e2e/tests-dfx/new.bash +++ b/e2e/tests-dfx/new.bash @@ -7,6 +7,7 @@ setup() { } teardown() { + dfx_stop standard_teardown } @@ -51,9 +52,34 @@ teardown() { assert_eq "motoko" } -@test "dfx new always emits sample-asset.txt" { - assert_command dfx new e2e_frontend --frontend - assert_file_exists e2e_frontend/src/e2e_frontend_frontend/assets/sample-asset.txt - assert_command dfx new e2e_no_frontend --no-frontend - assert_file_exists e2e_no_frontend/src/e2e_no_frontend_frontend/assets/sample-asset.txt +@test "frontend templates apply successfully" { + for frontend in sveltekit vue react vanilla simple-assets none; do + assert_command dfx new e2e_${frontend/-/_} --frontend $frontend + done + assert_file_not_exists e2e_none/src/e2e_none_frontend +} + +@test "frontend templates pass the frontend tests" { + dfx_start + for frontend in sveltekit vue react vanilla; do + assert_command dfx new e2e_$frontend --frontend $frontend --extras frontend-tests + pushd e2e_$frontend + assert_command dfx deploy + assert_command npm test --workspaces + popd + done +} + +@test "backend templates" { + for backend in motoko rust kybra azle; do + assert_command dfx new e2e_$backend --type $backend --no-frontend + done +} + +@test "interactive template selection" { + assert_command "${BATS_TEST_DIRNAME}/../assets/expect_scripts/rust_svelte_with_tests_and_ii.exp" + assert_file_exists e2e_project/Cargo.toml + assert_file_exists e2e_project/src/e2e_project_frontend/src/routes/+page.svelte + assert_file_exists e2e_project/src/e2e_project_frontend/src/setupTests.js + assert_command jq .canisters.internet_identity e2e_project/dfx.json } diff --git a/e2e/tests-dfx/playground.bash b/e2e/tests-dfx/playground.bash index c90316adb9..486b8b0761 100644 --- a/e2e/tests-dfx/playground.bash +++ b/e2e/tests-dfx/playground.bash @@ -5,7 +5,7 @@ load ../utils/_ setup() { standard_setup setup_playground - dfx_new hello + dfx_new_assets hello } teardown() { diff --git a/e2e/tests-dfx/start.bash b/e2e/tests-dfx/start.bash index 276f430ee7..f1441bf43a 100644 --- a/e2e/tests-dfx/start.bash +++ b/e2e/tests-dfx/start.bash @@ -130,7 +130,7 @@ teardown() { } @test "dfx restarts icx-proxy" { - dfx_new hello + dfx_new_assets hello dfx_start install_asset greet @@ -155,7 +155,7 @@ teardown() { } @test "dfx restarts icx-proxy when the replica restarts" { - dfx_new hello + dfx_new_assets hello dfx_start install_asset greet diff --git a/e2e/tests-dfx/usage_env.bash b/e2e/tests-dfx/usage_env.bash index 7cd19f6af5..31ac9aab76 100644 --- a/e2e/tests-dfx/usage_env.bash +++ b/e2e/tests-dfx/usage_env.bash @@ -24,7 +24,7 @@ teardown() { #cache # create a new project to install dfx cache assert_command_fail ls "$DFX_CACHE_ROOT/.cache/dfinity/versions" - dfx new hello + dfx new hello --no-frontend assert_command ls "$DFX_CACHE_ROOT/.cache/dfinity/versions" assert_command_fail ls "$HOME/.cache/dfinity/versions" rm -rf hello @@ -42,7 +42,7 @@ teardown() { #cache # create a new project to install dfx cache assert_command_fail ls "$HOME/.cache/dfinity/versions" - dfx new hello + dfx new hello --no-frontend assert_command ls "$HOME/.cache/dfinity/versions" rm -rf hello ) diff --git a/e2e/tests-dfx/wallet.bash b/e2e/tests-dfx/wallet.bash index 34b5ca885a..33a8417bc7 100644 --- a/e2e/tests-dfx/wallet.bash +++ b/e2e/tests-dfx/wallet.bash @@ -79,7 +79,7 @@ teardown() { } @test "'dfx identity set-wallet --force' bypasses wallet canister verification" { - dfx_new hello + dfx_new_assets hello dfx_start setup_actuallylocal_shared_network @@ -102,7 +102,7 @@ teardown() { } @test "deploy wallet" { - dfx_new hello + dfx_new_assets hello dfx_start setup_actuallylocal_shared_network diff --git a/e2e/tests-icx-asset/icx-asset.bash b/e2e/tests-icx-asset/icx-asset.bash index 786887aea2..f06b1d20b6 100644 --- a/e2e/tests-icx-asset/icx-asset.bash +++ b/e2e/tests-icx-asset/icx-asset.bash @@ -11,7 +11,7 @@ setup() { standard_setup - dfx_new_frontend + dfx_new_assets dfx_start assert_command dfx deploy diff --git a/e2e/tests-replica/deploy.bash b/e2e/tests-replica/deploy.bash index 8ba88f5cf8..b69ac6608e 100644 --- a/e2e/tests-replica/deploy.bash +++ b/e2e/tests-replica/deploy.bash @@ -23,7 +23,7 @@ teardown() { } @test "deploy a canister without dependencies" { - dfx_new hello + dfx_new_assets hello dfx_start install_asset greet assert_command dfx deploy hello_backend @@ -32,7 +32,7 @@ teardown() { } @test "deploy a canister with dependencies" { - dfx_new hello + dfx_new_assets hello dfx_start install_asset greet assert_command dfx deploy hello_frontend diff --git a/e2e/utils/_.bash b/e2e/utils/_.bash index 3b340a73ed..592e6aa298 100644 --- a/e2e/utils/_.bash +++ b/e2e/utils/_.bash @@ -54,7 +54,17 @@ standard_teardown() { dfx_new_frontend() { local project_name=${1:-e2e_project} - dfx new "${project_name}" --frontend + dfx new "${project_name}" --frontend vanilla + test -d "${project_name}" + test -f "${project_name}"/dfx.json + cd "${project_name}" + + echo PWD: "$(pwd)" >&2 +} + +dfx_new_assets() { + local project_name=${1:-e2e_project} + dfx new "${project_name}" --frontend simple-assets test -d "${project_name}" test -f "${project_name}"/dfx.json cd "${project_name}" diff --git a/scripts/workflows/provision-linux.sh b/scripts/workflows/provision-linux.sh index f00de0f386..fffe7d69d9 100755 --- a/scripts/workflows/provision-linux.sh +++ b/scripts/workflows/provision-linux.sh @@ -32,7 +32,12 @@ if [ "$E2E_TEST" = "tests-dfx/certificate.bash" ]; then sudo tar --directory /usr/local/bin --extract --file mitmproxy.tar.gz echo "mitmproxy version: $(mitmproxy --version)" fi -if [ "$E2E_TEST" = "tests-dfx/identity_encryption.bash" ] || [ "$E2E_TEST" = "tests-dfx/identity.bash" ] || [ "$E2E_TEST" = "tests-dfx/generate.bash" ] || [ "$E2E_TEST" = "tests-dfx/start.bash" ]; then +if [ "$E2E_TEST" = "tests-dfx/identity_encryption.bash" ] \ + || [ "$E2E_TEST" = "tests-dfx/identity.bash" ] \ + || [ "$E2E_TEST" = "tests-dfx/generate.bash" ] \ + || [ "$E2E_TEST" = "tests-dfx/start.bash" ] \ + || [ "$E2E_TEST" = "tests-dfx/new.bash" ] +then sudo apt-get install --yes expect fi if [ "$E2E_TEST" = "tests-dfx/deps.bash" ]; then diff --git a/src/dfx-core/src/config/model/dfinity.rs b/src/dfx-core/src/config/model/dfinity.rs index d2fb8e9d0e..5eeb32b307 100644 --- a/src/dfx-core/src/config/model/dfinity.rs +++ b/src/dfx-core/src/config/model/dfinity.rs @@ -309,6 +309,10 @@ pub enum CanisterTypeProperties { /// Optional if there is no build necessary or the assets can be built using the default `npm run build` command. #[schemars(default)] build: SerdeVec, + + /// # NPM workspace + /// The workspace in package.json that this canister is in, if it is not in the root workspace. + workspace: Option, }, /// # Custom-Specific Properties Custom { @@ -1118,6 +1122,7 @@ impl<'de> Visitor<'de> for PropertiesVisitor { let mut build = None; let mut r#type = None; let mut id = None; + let mut workspace = None; while let Some(key) = map.next_key::()? { match &*key { "package" => package = Some(map.next_value()?), @@ -1127,6 +1132,7 @@ impl<'de> Visitor<'de> for PropertiesVisitor { "wasm" => wasm = Some(map.next_value()?), "type" => r#type = Some(map.next_value::()?), "id" => id = Some(map.next_value()?), + "workspace" => workspace = Some(map.next_value()?), _ => continue, } } @@ -1139,6 +1145,7 @@ impl<'de> Visitor<'de> for PropertiesVisitor { Some("assets") => CanisterTypeProperties::Assets { source: source.ok_or_else(|| missing_field("source"))?, build: build.unwrap_or_default(), + workspace, }, Some("custom") => CanisterTypeProperties::Custom { build: build.unwrap_or_default(), diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index 661540d7b5..752ca77d06 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -51,7 +51,7 @@ crc32fast = "1.3.2" crossbeam = "0.8.1" ctrlc = { version = "3.2.1", features = ["termination"] } dfx-core = { path = "../dfx-core" } -dialoguer.workspace = true +dialoguer = { workspace = true, features = ["fuzzy-select"] } directories-next.workspace = true flate2 = { workspace = true, default-features = false, features = ["zlib-ng"] } fn-error-context = "0.2.0" diff --git a/src/dfx/assets/build.rs b/src/dfx/assets/build.rs index f787ee3517..d664aa2baa 100644 --- a/src/dfx/assets/build.rs +++ b/src/dfx/assets/build.rs @@ -129,10 +129,7 @@ fn add_asset_archive(fn_name: &str, f: &mut File, assets_path: &Path) { } fn add_assets_from_directory(fn_name: &str, f: &mut File, path: &str) { - for file in WalkDir::new(path) - .into_iter() - .filter_map(|x| x.ok().filter(|entry| entry.file_type().is_file())) - { + for file in WalkDir::new(path).into_iter().filter_map(|x| x.ok()) { println!("cargo:rerun-if-changed={}", file.path().display()) } let out_dir = env::var("OUT_DIR").unwrap(); @@ -242,27 +239,87 @@ fn add_assets(sources: Sources) { add_assets_from_directory( "new_project_motoko_files", &mut f, - "assets/new_project_motoko_files", + "assets/project_templates/motoko", ); add_assets_from_directory( - "new_project_node_files", + "new_project_rust_files", &mut f, - "assets/new_project_node_files", + "assets/project_templates/rust", ); add_assets_from_directory( - "new_project_rust_files", + "new_project_base_files", &mut f, - "assets/new_project_rust_files", + "assets/project_templates/base", ); add_assets_from_directory( - "new_project_no_frontend_files", + "new_project_js_files", &mut f, - "assets/new_project_no_frontend_files", + "assets/project_templates/any_js", ); add_assets_from_directory( - "new_project_base_files", + "new_project_kybra_files", + &mut f, + "assets/project_templates/kybra", + ); + add_assets_from_directory( + "new_project_azle_files", + &mut f, + "assets/project_templates/azle", + ); + add_assets_from_directory( + "new_project_vanillajs_files", + &mut f, + "assets/project_templates/vanilla_js", + ); + add_assets_from_directory( + "new_project_vanillajs_test_files", + &mut f, + "assets/project_templates/vanilla_js_tests", + ); + add_assets_from_directory( + "new_project_react_files", + &mut f, + "assets/project_templates/react", + ); + add_assets_from_directory( + "new_project_react_test_files", + &mut f, + "assets/project_templates/react_tests", + ); + add_assets_from_directory( + "new_project_svelte_files", + &mut f, + "assets/project_templates/svelte", + ); + add_assets_from_directory( + "new_project_svelte_test_files", + &mut f, + "assets/project_templates/svelte_tests", + ); + add_assets_from_directory( + "new_project_vue_files", + &mut f, + "assets/project_templates/vue", + ); + add_assets_from_directory( + "new_project_vue_test_files", + &mut f, + "assets/project_templates/vue_tests", + ); + add_assets_from_directory( + "new_project_assets_files", + &mut f, + "assets/project_templates/simple_assets", + ); + add_assets_from_directory( + "new_project_internet_identity_files", + &mut f, + "assets/project_templates/internet_identity", + ); + add_assets_from_directory( + "new_project_bitcoin_files", &mut f, - "assets/new_project_base_files", + "assets/project_templates/bitcoin", ); } diff --git a/src/dfx/assets/new_project_node_files/package.json b/src/dfx/assets/new_project_node_files/package.json deleted file mode 100644 index 7901e4d41b..0000000000 --- a/src/dfx/assets/new_project_node_files/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "{project_name}_frontend", - "version": "0.2.0", - "description": "Internet Computer starter application", - "keywords": [ - "Internet Computer", - "Motoko", - "JavaScript", - "Canister" - ], - "scripts": { - "build": "webpack", - "prebuild": "dfx generate", - "start": "webpack serve --mode development --env development", - "deploy:local": "dfx deploy --network=local", - "deploy:ic": "dfx deploy --network=ic", - "generate": "dfx generate {project_name}_backend" - }, - "dependencies": { - "@dfinity/agent": "^{js_agent_version}", - "@dfinity/candid": "^{js_agent_version}", - "@dfinity/principal": "^{js_agent_version}" - }, - "devDependencies": { - "assert": "2.0.0", - "buffer": "6.0.3", - "copy-webpack-plugin": "^11.0.0", - "dotenv": "^16.0.3", - "events": "3.3.0", - "html-webpack-plugin": "5.5.0", - "process": "0.11.10", - "stream-browserify": "3.0.0", - "terser-webpack-plugin": "^5.3.3", - "util": "0.12.4", - "webpack": "^5.73.0", - "webpack-cli": "^4.10.0", - "webpack-dev-server": "^4.8.1" - }, - "engines": { - "node": "^12 || ^14 || ^16 || >=17", - "npm": "^7.17 || >=8" - }, - "browserslist": [ - "last 2 chrome version", - "last 2 firefox version", - "last 2 safari version", - "last 2 edge version" - ] -} diff --git a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/main.css b/src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/main.css deleted file mode 100644 index ccf2e6cf0f..0000000000 --- a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/main.css +++ /dev/null @@ -1,37 +0,0 @@ -body { - font-family: sans-serif; - font-size: 1.5rem; -} - -img { - max-width: 50vw; - max-height: 25vw; - display: block; - margin: auto; -} - -form { - display: flex; - justify-content: center; - gap: 0.5em; - flex-flow: row wrap; - max-width: 40vw; - margin: auto; - align-items: baseline; -} - -button[type="submit"] { - padding: 5px 20px; - margin: 10px auto; - float: right; -} - -#greeting { - margin: 10px auto; - padding: 10px 60px; - border: 1px solid #222; -} - -#greeting:empty { - display: none; -} diff --git a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/sample-asset.txt b/src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/sample-asset.txt deleted file mode 100644 index 7c011d0f9c..0000000000 --- a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/sample-asset.txt +++ /dev/null @@ -1 +0,0 @@ -This is a sample asset! diff --git a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/src/index.html b/src/dfx/assets/new_project_node_files/src/__project_name___frontend/src/index.html deleted file mode 100644 index 1ae832c8b1..0000000000 --- a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/src/index.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - {project_name} - - - - - -
- DFINITY logo -
-
-
- - - -
-
-
- - diff --git a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/src/index.js b/src/dfx/assets/new_project_node_files/src/__project_name___frontend/src/index.js deleted file mode 100644 index 118fa09f7a..0000000000 --- a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/src/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import { {project_name}_backend } from "../../declarations/{project_name}_backend"; - -document.querySelector("form").addEventListener("submit", async (e) => { - e.preventDefault(); - const button = e.target.querySelector("button"); - - const name = document.getElementById("name").value.toString(); - - button.setAttribute("disabled", true); - - // Interact with foo actor, calling the greet method - const greeting = await {project_name}_backend.greet(name); - - button.removeAttribute("disabled"); - - document.getElementById("greeting").innerText = greeting; - - return false; -}); diff --git a/src/dfx/assets/new_project_node_files/webpack.config.js b/src/dfx/assets/new_project_node_files/webpack.config.js deleted file mode 100644 index 5cff845f07..0000000000 --- a/src/dfx/assets/new_project_node_files/webpack.config.js +++ /dev/null @@ -1,96 +0,0 @@ -require("dotenv").config(); -const path = require("path"); -const webpack = require("webpack"); -const HtmlWebpackPlugin = require("html-webpack-plugin"); -const TerserPlugin = require("terser-webpack-plugin"); -const CopyPlugin = require("copy-webpack-plugin"); - -const isDevelopment = process.env.NODE_ENV !== "production"; - -const frontendDirectory = "{project_name}_frontend"; - -const frontend_entry = path.join("src", frontendDirectory, "src", "index.html"); - -module.exports = { - target: "web", - mode: isDevelopment ? "development" : "production", - entry: { - // The frontend.entrypoint points to the HTML file for this build, so we need - // to replace the extension to `.js`. - index: path.join(__dirname, frontend_entry).replace(/\.html$/, ".js"), - }, - devtool: isDevelopment ? "source-map" : false, - optimization: { - minimize: !isDevelopment, - minimizer: [new TerserPlugin()], - }, - resolve: { - extensions: [".js", ".ts", ".jsx", ".tsx"], - fallback: { - assert: require.resolve("assert/"), - buffer: require.resolve("buffer/"), - events: require.resolve("events/"), - stream: require.resolve("stream-browserify/"), - util: require.resolve("util/"), - }, - }, - output: { - filename: "index.js", - path: path.join(__dirname, "dist", frontendDirectory), - }, - - // Depending in the language or framework you are using for - // front-end development, add module loaders to the default - // webpack configuration. For example, if you are using React - // modules and CSS as described in the "Adding a stylesheet" - // tutorial, uncomment the following lines: - // module: { - // rules: [ - // { test: /\.(ts|tsx|jsx)$/, loader: "ts-loader" }, - // { test: /\.css$/, use: ['style-loader','css-loader'] } - // ] - // }, - plugins: [ - new HtmlWebpackPlugin({ - template: path.join(__dirname, frontend_entry), - cache: false, - }), - new webpack.EnvironmentPlugin([ - ...Object.keys(process.env).filter((key) => { - if (key.includes("CANISTER")) return true; - if (key.includes("DFX")) return true; - return false; - }), - ]), - new webpack.ProvidePlugin({ - Buffer: [require.resolve("buffer/"), "Buffer"], - process: require.resolve("process/browser"), - }), - new CopyPlugin({ - patterns: [ - { - from: `src/${frontendDirectory}/src/.ic-assets.json*`, - to: ".ic-assets.json5", - noErrorOnMissing: true, - }, - ], - }), - ], - // proxy /api to port 4943 during development. - // if you edit dfx.json to define a project-specific local network, change the port to match. - devServer: { - proxy: { - "/api": { - target: "http://127.0.0.1:4943", - changeOrigin: true, - pathRewrite: { - "^/api": "/api", - }, - }, - }, - static: path.resolve(__dirname, "src", frontendDirectory, "assets"), - hot: true, - watchFiles: [path.resolve(__dirname, "src", frontendDirectory)], - liveReload: true, - }, -}; diff --git a/src/dfx/assets/new_project_rust_files/Cargo.toml b/src/dfx/assets/new_project_rust_files/Cargo.toml deleted file mode 100644 index d0616ce222..0000000000 --- a/src/dfx/assets/new_project_rust_files/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[workspace] -members = [ - "src/{project_name}_backend", -] diff --git a/src/dfx/assets/project_templates/any_js/package.json b/src/dfx/assets/project_templates/any_js/package.json new file mode 100644 index 0000000000..ec3206c127 --- /dev/null +++ b/src/dfx/assets/project_templates/any_js/package.json @@ -0,0 +1,16 @@ +{ + "name": "{project_name}", + "type": "module", + "scripts": { + "start": "npm start --workspaces --if-present", + "prebuild": "npm run prebuild --workspaces --if-present", + "build": "npm run build --workspaces --if-present", + "pretest": "npm run prebuild --workspaces --if-present", + "test": "npm test --workspaces --if-present" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "workspaces": [ ] +} diff --git a/src/dfx/assets/project_templates/any_js/tsconfig.json b/src/dfx/assets/project_templates/any_js/tsconfig.json new file mode 100644 index 0000000000..f4629166d9 --- /dev/null +++ b/src/dfx/assets/project_templates/any_js/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ES2020", + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "moduleResolution": "node", + "allowJs": true, + "outDir": "HACK_BECAUSE_OF_ALLOW_JS" + } +} diff --git a/src/dfx/assets/project_templates/azle/dfx.json-patch b/src/dfx/assets/project_templates/azle/dfx.json-patch new file mode 100644 index 0000000000..4ada1db29e --- /dev/null +++ b/src/dfx/assets/project_templates/azle/dfx.json-patch @@ -0,0 +1,14 @@ +[ + { + "path": "/canisters/{project_name}_backend", + "op": "add", + "value": { + "type": "custom", + "main": "src/{project_name}_backend/src/index.ts", + "candid": "src/{project_name}_backend/{project_name}_backend.did", + "build": "npx azle {project_name}_backend", + "wasm": ".azle/{project_name}_backend/{project_name}_backend.wasm", + "gzip": true + } + } +] diff --git a/src/dfx/assets/project_templates/azle/package.json-patch b/src/dfx/assets/project_templates/azle/package.json-patch new file mode 100644 index 0000000000..81e3754421 --- /dev/null +++ b/src/dfx/assets/project_templates/azle/package.json-patch @@ -0,0 +1,7 @@ +[ + { + "op": "add", + "path": "/workspaces/-", + "value": "src/{project_name}_backend" + } +] \ No newline at end of file diff --git a/src/dfx/assets/project_templates/azle/src/__project_name___backend/__project_name___backend.did b/src/dfx/assets/project_templates/azle/src/__project_name___backend/__project_name___backend.did new file mode 100644 index 0000000000..b971bf69eb --- /dev/null +++ b/src/dfx/assets/project_templates/azle/src/__project_name___backend/__project_name___backend.did @@ -0,0 +1,3 @@ +service : { + "greet" : (text) -> (text) query; +} diff --git a/src/dfx/assets/project_templates/azle/src/__project_name___backend/package.json b/src/dfx/assets/project_templates/azle/src/__project_name___backend/package.json new file mode 100644 index 0000000000..6a90d825da --- /dev/null +++ b/src/dfx/assets/project_templates/azle/src/__project_name___backend/package.json @@ -0,0 +1,9 @@ +{ + "name": "{project_name}_backend", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "azle": "^0.19.0" + } +} \ No newline at end of file diff --git a/src/dfx/assets/project_templates/azle/src/__project_name___backend/src/index.ts b/src/dfx/assets/project_templates/azle/src/__project_name___backend/src/index.ts new file mode 100644 index 0000000000..87c3d05729 --- /dev/null +++ b/src/dfx/assets/project_templates/azle/src/__project_name___backend/src/index.ts @@ -0,0 +1,7 @@ +import { Canister, query, text } from 'azle'; + +export default Canister({ + greet: query([text], text, (name) => { + return `Hello, ${name}!`; + }) +}) diff --git a/src/dfx/assets/new_project_base_files/README.md b/src/dfx/assets/project_templates/base/README.md similarity index 100% rename from src/dfx/assets/new_project_base_files/README.md rename to src/dfx/assets/project_templates/base/README.md diff --git a/src/dfx/assets/new_project_base_files/__dot__gitignore b/src/dfx/assets/project_templates/base/__dot__gitignore similarity index 88% rename from src/dfx/assets/new_project_base_files/__dot__gitignore rename to src/dfx/assets/project_templates/base/__dot__gitignore index 938bc33a56..49c89a1c95 100644 --- a/src/dfx/assets/new_project_base_files/__dot__gitignore +++ b/src/dfx/assets/project_templates/base/__dot__gitignore @@ -11,7 +11,7 @@ .dfx/ # generated files -src/declarations/ +**/declarations/ # rust target/ @@ -19,6 +19,7 @@ target/ # frontend code node_modules/ dist/ +.svelte-kit/ # environment variables .env diff --git a/src/dfx/assets/new_project_base_files/dfx.json b/src/dfx/assets/project_templates/base/dfx.json similarity index 100% rename from src/dfx/assets/new_project_base_files/dfx.json rename to src/dfx/assets/project_templates/base/dfx.json diff --git a/src/dfx/assets/project_templates/bitcoin/dfx.json-patch b/src/dfx/assets/project_templates/bitcoin/dfx.json-patch new file mode 100644 index 0000000000..b100e8bec9 --- /dev/null +++ b/src/dfx/assets/project_templates/bitcoin/dfx.json-patch @@ -0,0 +1,13 @@ +[ + { + "op": "add", + "path": "/defaults/bitcoin", + "value": { + "enabled": true, + "nodes": [ + "127.0.0.1:18444" + ], + "log_level": "info" + } + } +] diff --git a/src/dfx/assets/project_templates/internet_identity/dfx.json-patch b/src/dfx/assets/project_templates/internet_identity/dfx.json-patch new file mode 100644 index 0000000000..055084c18a --- /dev/null +++ b/src/dfx/assets/project_templates/internet_identity/dfx.json-patch @@ -0,0 +1,17 @@ +[ + { + "op": "add", + "path": "/canisters/internet_identity", + "value": { + "type": "custom", + "candid": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity.did", + "wasm": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity_dev.wasm.gz", + "remote": { + "id": { + "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai" + } + }, + "frontend": { } + } + } +] diff --git a/src/dfx/assets/project_templates/kybra/dfx.json-patch b/src/dfx/assets/project_templates/kybra/dfx.json-patch new file mode 100644 index 0000000000..1357f5e17a --- /dev/null +++ b/src/dfx/assets/project_templates/kybra/dfx.json-patch @@ -0,0 +1,14 @@ +[ + { + "path": "/canisters/{project_name}_backend", + "op": "add", + "value": { + "type": "custom", + "build": "python -m kybra {project_name}_backend src/{project_name}_backend/src/main.py src/{project_name}_backend/{project_name}_backend.did", + "post_install": ".kybra/{project_name}_backend/post_install.sh", + "candid": "src/{project_name}_backend/{project_name}_backend.did", + "wasm": ".kybra/{project_name}_backend/{project_name}_backend.wasm", + "gzip": true + } + } +] \ No newline at end of file diff --git a/src/dfx/assets/project_templates/kybra/requirements.in b/src/dfx/assets/project_templates/kybra/requirements.in new file mode 100644 index 0000000000..318fcdddd4 --- /dev/null +++ b/src/dfx/assets/project_templates/kybra/requirements.in @@ -0,0 +1 @@ +kybra~=0.5.0 diff --git a/src/dfx/assets/project_templates/kybra/requirements.txt b/src/dfx/assets/project_templates/kybra/requirements.txt new file mode 100644 index 0000000000..a696bbab6e --- /dev/null +++ b/src/dfx/assets/project_templates/kybra/requirements.txt @@ -0,0 +1,15 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile requirements.in +# +altgraph==0.17.4 + # via modulegraph +kybra==0.5.2 + # via -r requirements.in +modulegraph==0.19.3 + # via kybra + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/src/dfx/assets/new_project_rust_files/src/__project_name___backend/__project_name___backend.did b/src/dfx/assets/project_templates/kybra/src/__project_name___backend/__project_name___backend.did similarity index 100% rename from src/dfx/assets/new_project_rust_files/src/__project_name___backend/__project_name___backend.did rename to src/dfx/assets/project_templates/kybra/src/__project_name___backend/__project_name___backend.did diff --git a/src/dfx/assets/project_templates/kybra/src/__project_name___backend/src/main.py b/src/dfx/assets/project_templates/kybra/src/__project_name___backend/src/main.py new file mode 100644 index 0000000000..bf0f57a9da --- /dev/null +++ b/src/dfx/assets/project_templates/kybra/src/__project_name___backend/src/main.py @@ -0,0 +1,5 @@ +from kybra import query + +@query +def greet(name: str) -> str: + return f"Hello, {name}!" diff --git a/src/dfx/assets/new_project_motoko_files/README.md.patch b/src/dfx/assets/project_templates/motoko/README.md.patch similarity index 100% rename from src/dfx/assets/new_project_motoko_files/README.md.patch rename to src/dfx/assets/project_templates/motoko/README.md.patch diff --git a/src/dfx/assets/new_project_motoko_files/dfx.json-patch b/src/dfx/assets/project_templates/motoko/dfx.json-patch similarity index 100% rename from src/dfx/assets/new_project_motoko_files/dfx.json-patch rename to src/dfx/assets/project_templates/motoko/dfx.json-patch diff --git a/src/dfx/assets/new_project_motoko_files/src/__project_name___backend/main.mo b/src/dfx/assets/project_templates/motoko/src/__project_name___backend/main.mo similarity index 100% rename from src/dfx/assets/new_project_motoko_files/src/__project_name___backend/main.mo rename to src/dfx/assets/project_templates/motoko/src/__project_name___backend/main.mo diff --git a/src/dfx/assets/new_project_node_files/dfx.json-patch b/src/dfx/assets/project_templates/react/dfx.json-patch similarity index 68% rename from src/dfx/assets/new_project_node_files/dfx.json-patch rename to src/dfx/assets/project_templates/react/dfx.json-patch index 933faf7453..fa6e3bd3ed 100644 --- a/src/dfx/assets/new_project_node_files/dfx.json-patch +++ b/src/dfx/assets/project_templates/react/dfx.json-patch @@ -5,11 +5,12 @@ "value": { "type": "assets", "source": [ - "src/{project_name}_frontend/assets" + "src/{project_name}_frontend/dist" ], "dependencies": [ "{project_name}_backend" - ] + ], + "workspace": "{project_name}_frontend" } } ] diff --git a/src/dfx/assets/project_templates/react/package.json-patch b/src/dfx/assets/project_templates/react/package.json-patch new file mode 100644 index 0000000000..c74a9e93c7 --- /dev/null +++ b/src/dfx/assets/project_templates/react/package.json-patch @@ -0,0 +1,7 @@ +[ + { + "op": "add", + "path": "/workspaces/-", + "value": "src/{project_name}_frontend" + } +] diff --git a/src/dfx/assets/project_templates/react/src/__project_name___frontend/index.html b/src/dfx/assets/project_templates/react/src/__project_name___frontend/index.html new file mode 100644 index 0000000000..a8f14f76b8 --- /dev/null +++ b/src/dfx/assets/project_templates/react/src/__project_name___frontend/index.html @@ -0,0 +1,17 @@ + + + + + + + IC Hello Starter + + + + + +
+ + + + \ No newline at end of file diff --git a/src/dfx/assets/project_templates/react/src/__project_name___frontend/package.json b/src/dfx/assets/project_templates/react/src/__project_name___frontend/package.json new file mode 100644 index 0000000000..111f329b2c --- /dev/null +++ b/src/dfx/assets/project_templates/react/src/__project_name___frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "{project_name}_frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "setup": "npm i && dfx canister create {project_name}_backend && dfx generate {project_name}_backend && dfx deploy", + "start": "vite --port 3000", + "prebuild": "dfx generate", + "build": "tsc && vite build", + "format": "prettier --write \"src/**/*.{json,js,jsx,ts,tsx,css,scss}\"" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "@dfinity/agent": "^0.15.7", + "@dfinity/candid": "^0.15.7", + "@dfinity/principal": "^0.15.7" + }, + "devDependencies": { + "@types/react": "^18.2.14", + "@types/react-dom": "^18.2.6", + "@vitejs/plugin-react": "^4.0.1", + "dotenv": "^16.3.1", + "sass": "^1.63.6", + "typescript": "^5.1.3", + "vite": "^4.3.9", + "vite-plugin-environment": "^1.1.3" + } +} diff --git a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/src/.ic-assets.json5 b/src/dfx/assets/project_templates/react/src/__project_name___frontend/public/.ic-assets.json5 similarity index 96% rename from src/dfx/assets/new_project_node_files/src/__project_name___frontend/src/.ic-assets.json5 rename to src/dfx/assets/project_templates/react/src/__project_name___frontend/public/.ic-assets.json5 index b58d2e1fc6..394b81c66e 100644 --- a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/src/.ic-assets.json5 +++ b/src/dfx/assets/project_templates/react/src/__project_name___frontend/public/.ic-assets.json5 @@ -50,8 +50,7 @@ // See: https://owasp.org/www-community/attacks/xss/ "X-XSS-Protection": "1; mode=block" }, - // Set the allow_raw_access field to false to redirect all requests from .raw.icp0.io to .icp0.io - // The default behavior is to allow raw access. - // "allow_raw_access": true + // Uncomment to redirect all requests from .raw.icp0.io to .icp0.io + // "allow_raw_access": false }, ] diff --git a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/favicon.ico b/src/dfx/assets/project_templates/react/src/__project_name___frontend/public/favicon.ico similarity index 100% rename from src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/favicon.ico rename to src/dfx/assets/project_templates/react/src/__project_name___frontend/public/favicon.ico diff --git a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/logo2.svg b/src/dfx/assets/project_templates/react/src/__project_name___frontend/public/logo2.svg similarity index 100% rename from src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/logo2.svg rename to src/dfx/assets/project_templates/react/src/__project_name___frontend/public/logo2.svg diff --git a/src/dfx/assets/project_templates/react/src/__project_name___frontend/src/App.jsx b/src/dfx/assets/project_templates/react/src/__project_name___frontend/src/App.jsx new file mode 100644 index 0000000000..48e6545c81 --- /dev/null +++ b/src/dfx/assets/project_templates/react/src/__project_name___frontend/src/App.jsx @@ -0,0 +1,31 @@ +import { useState } from 'react'; +import { {project_name}_backend } from 'declarations/{project_name}_backend'; + +function App() { + const [greeting, setGreeting] = useState(''); + + function handleSubmit(event) { + event.preventDefault(); + const name = event.target.elements.name.value; + {project_name}_backend.greet(name).then((greeting) => { + setGreeting(greeting); + }); + return false; + } + + return ( +
+ DFINITY logo +
+
+
+ + + +
+
{greeting}
+
+ ); +} + +export default App; diff --git a/src/dfx/assets/project_templates/react/src/__project_name___frontend/src/index.scss b/src/dfx/assets/project_templates/react/src/__project_name___frontend/src/index.scss new file mode 100644 index 0000000000..c1c320834c --- /dev/null +++ b/src/dfx/assets/project_templates/react/src/__project_name___frontend/src/index.scss @@ -0,0 +1,37 @@ +body { + font-family: sans-serif; + font-size: 1.5rem; +} + +img { + max-width: 50vw; + max-height: 25vw; + display: block; + margin: auto; +} + +form { + display: flex; + justify-content: center; + gap: 0.5em; + flex-flow: row wrap; + max-width: 40vw; + margin: auto; + align-items: baseline; +} + +button[type="submit"] { + padding: 5px 20px; + margin: 10px auto; + float: right; +} + +#greeting { + margin: 10px auto; + padding: 10px 60px; + border: 1px solid #222; +} + +#greeting:empty { + display: none; +} diff --git a/src/dfx/assets/project_templates/react/src/__project_name___frontend/src/main.jsx b/src/dfx/assets/project_templates/react/src/__project_name___frontend/src/main.jsx new file mode 100644 index 0000000000..173e7221c5 --- /dev/null +++ b/src/dfx/assets/project_templates/react/src/__project_name___frontend/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.scss'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +); diff --git a/src/dfx/assets/project_templates/react/src/__project_name___frontend/src/vite-env.d.ts b/src/dfx/assets/project_templates/react/src/__project_name___frontend/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/src/dfx/assets/project_templates/react/src/__project_name___frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/dfx/assets/project_templates/react/src/__project_name___frontend/tsconfig.json b/src/dfx/assets/project_templates/react/src/__project_name___frontend/tsconfig.json new file mode 100644 index 0000000000..39a545e919 --- /dev/null +++ b/src/dfx/assets/project_templates/react/src/__project_name___frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/src/dfx/assets/project_templates/react/src/__project_name___frontend/vite.config.js b/src/dfx/assets/project_templates/react/src/__project_name___frontend/vite.config.js new file mode 100644 index 0000000000..670b0347b6 --- /dev/null +++ b/src/dfx/assets/project_templates/react/src/__project_name___frontend/vite.config.js @@ -0,0 +1,43 @@ +import { fileURLToPath, URL } from 'url'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; +import environment from 'vite-plugin-environment'; +import dotenv from 'dotenv'; + +dotenv.config({ path: '../../.env' }); + +export default defineConfig({ + build: { + emptyOutDir: true, + }, + optimizeDeps: { + esbuildOptions: { + define: { + global: "globalThis", + }, + }, + }, + server: { + proxy: { + "/api": { + target: "http://127.0.0.1:4943", + changeOrigin: true, + }, + }, + }, + plugins: [ + react(), + environment("all", { prefix: "CANISTER_" }), + environment("all", { prefix: "DFX_" }), + ], + resolve: { + alias: [ + { + find: "declarations", + replacement: fileURLToPath( + new URL("../declarations", import.meta.url) + ), + }, + ], + }, +}); diff --git a/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/package.json-patch b/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/package.json-patch new file mode 100644 index 0000000000..0695d69555 --- /dev/null +++ b/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/package.json-patch @@ -0,0 +1,32 @@ +[ + { + "op": "add", + "path": "/devDependencies/@testing-library~1jest-dom", + "value": "^5.16.5" + }, + { + "op": "add", + "path": "/devDependencies/@testing-library~1react", + "value": "^14.0.0" + }, + { + "op": "add", + "path": "/devDependencies/cross-fetch", + "value": "^3.1.6" + }, + { + "op": "add", + "path": "/devDependencies/vitest", + "value": "^0.32.2" + }, + { + "op": "add", + "path": "/devDependencies/jsdom", + "value": "^22.1.0" + }, + { + "op": "add", + "path": "/scripts/test", + "value": "vitest run" + } +] diff --git a/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/src/setupTests.js b/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/src/setupTests.js new file mode 100644 index 0000000000..c29162e04f --- /dev/null +++ b/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/src/setupTests.js @@ -0,0 +1,5 @@ +import matchers from '@testing-library/jest-dom/matchers'; +import 'cross-fetch/polyfill'; +import { expect } from 'vitest'; + +expect.extend(matchers); diff --git a/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/src/tests/App.test.jsx b/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/src/tests/App.test.jsx new file mode 100644 index 0000000000..9e54579365 --- /dev/null +++ b/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/src/tests/App.test.jsx @@ -0,0 +1,16 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import App from '../App'; +import { StrictMode } from 'react'; + +describe('App', () => { + it('renders as expected', () => { + render( + + + , + ); + expect(document.body.innerHTML).toMatchInlineSnapshot('"
\\"DFINITY

"'); + expect(1).toEqual(1); + }); +}); diff --git a/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/tsconfig.json-patch b/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/tsconfig.json-patch new file mode 100644 index 0000000000..b1501d3f56 --- /dev/null +++ b/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/tsconfig.json-patch @@ -0,0 +1,7 @@ +[ + { + "op": "add", + "path": "/compilerOptions/types/-", + "value": "@testing-library/jest-dom" + } +] diff --git a/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/vite.config.js.patch b/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/vite.config.js.patch new file mode 100644 index 0000000000..c2f6c8f6d3 --- /dev/null +++ b/src/dfx/assets/project_templates/react_tests/src/__project_name___frontend/vite.config.js.patch @@ -0,0 +1,11 @@ +--- a/src/{project_name}_frontend/vite.config.js ++++ b/src/{project_name}_frontend/vite.config.js +@@ -1,1 +1,2 @@ ++/// + import { fileURLToPath, URL } from 'url'; +@@ -33,1 +33,5 @@ ++ test: { ++ environment: 'jsdom', ++ setupFiles: 'src/setupTests.js', ++ }, + resolve: { diff --git a/src/dfx/assets/project_templates/rust/Cargo.toml b/src/dfx/assets/project_templates/rust/Cargo.toml new file mode 100644 index 0000000000..76f71f33d6 --- /dev/null +++ b/src/dfx/assets/project_templates/rust/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ + "src/{project_name}_backend" +] +resolver = "2" diff --git a/src/dfx/assets/new_project_rust_files/README.md.patch b/src/dfx/assets/project_templates/rust/README.md.patch similarity index 96% rename from src/dfx/assets/new_project_rust_files/README.md.patch rename to src/dfx/assets/project_templates/rust/README.md.patch index 04bf8c1da7..5d37f5183d 100644 --- a/src/dfx/assets/new_project_rust_files/README.md.patch +++ b/src/dfx/assets/project_templates/rust/README.md.patch @@ -1,5 +1,5 @@ --- a/README.md -+++ b/README.md ++++ b/README.md @@ -9,2 +9,6 @@ - [Quick Start](https://internetcomputer.org/docs/current/developer-docs/setup/deploy-locally) - [SDK Developer Tools](https://internetcomputer.org/docs/current/developer-docs/setup/install) diff --git a/src/dfx/assets/new_project_rust_files/dfx.json-patch b/src/dfx/assets/project_templates/rust/dfx.json-patch similarity index 100% rename from src/dfx/assets/new_project_rust_files/dfx.json-patch rename to src/dfx/assets/project_templates/rust/dfx.json-patch diff --git a/src/dfx/assets/new_project_rust_files/src/__project_name___backend/Cargo.toml b/src/dfx/assets/project_templates/rust/src/__project_name___backend/Cargo.toml similarity index 100% rename from src/dfx/assets/new_project_rust_files/src/__project_name___backend/Cargo.toml rename to src/dfx/assets/project_templates/rust/src/__project_name___backend/Cargo.toml diff --git a/src/dfx/assets/new_project_node_files/__dot__env b/src/dfx/assets/project_templates/rust/src/__project_name___backend/__project_name___backend.did similarity index 72% rename from src/dfx/assets/new_project_node_files/__dot__env rename to src/dfx/assets/project_templates/rust/src/__project_name___backend/__project_name___backend.did index dfbd8f71ed..aa419a250f 100644 --- a/src/dfx/assets/new_project_node_files/__dot__env +++ b/src/dfx/assets/project_templates/rust/src/__project_name___backend/__project_name___backend.did @@ -1 +1,3 @@ -# Do not commit this file to your public repos. +service : { + "greet": (text) -> (text) query; +} diff --git a/src/dfx/assets/new_project_rust_files/src/__project_name___backend/src/lib.rs b/src/dfx/assets/project_templates/rust/src/__project_name___backend/src/lib.rs similarity index 100% rename from src/dfx/assets/new_project_rust_files/src/__project_name___backend/src/lib.rs rename to src/dfx/assets/project_templates/rust/src/__project_name___backend/src/lib.rs diff --git a/src/dfx/assets/new_project_no_frontend_files/dfx.json-patch b/src/dfx/assets/project_templates/simple_assets/dfx.json-patch similarity index 100% rename from src/dfx/assets/new_project_no_frontend_files/dfx.json-patch rename to src/dfx/assets/project_templates/simple_assets/dfx.json-patch diff --git a/src/dfx/assets/project_templates/simple_assets/src/__project_name___frontend/assets/.ic-assets.json5 b/src/dfx/assets/project_templates/simple_assets/src/__project_name___frontend/assets/.ic-assets.json5 new file mode 100644 index 0000000000..887f555461 --- /dev/null +++ b/src/dfx/assets/project_templates/simple_assets/src/__project_name___frontend/assets/.ic-assets.json5 @@ -0,0 +1,56 @@ +[ + { + "match": "**/*", + "headers": { + // Security: The Content Security Policy (CSP) given below aims at working with many apps rather than providing maximal security. + // We recommend tightening the CSP for your specific application. Some recommendations are as follows: + // - Use the CSP Evaluator (https://csp-evaluator.withgoogle.com/) to validate the CSP you define. + // - Follow the “Strict CSP” recommendations (https://csp.withgoogle.com/docs/strict-csp.html). However, note that in the context of the IC, + // nonces cannot be used because the response bodies must be static to work well with HTTP asset certification. + // Thus, we recommend to include script hashes (in combination with strict-dynamic) in the CSP as described + // in https://csp.withgoogle.com/docs/faq.html in section “What if my site is static and I can't add nonces to scripts?”. + // See for example the II CSP (https://github.com/dfinity/internet-identity/blob/main/src/internet_identity/src/http.rs). + // - It is recommended to tighten the connect-src directive. With the current CSP configuration the browser can + // make requests to https://*.icp0.io, hence being able to call any canister via https://icp0.io/api/v2/canister/{canister-ID}. + // This could potentially be used in combination with another vulnerability (e.g. XSS) to exfiltrate private data. + // The developer can configure this policy to only allow requests to their specific canisters, + // e.g: connect-src 'self' https://icp-api.io/api/v2/canister/{my-canister-ID}, where {my-canister-ID} has the following format: aaaaa-aaaaa-aaaaa-aaaaa-aaa + // - It is recommended to configure style-src, style-src-elem and font-src directives with the resources your canister is going to use + // instead of using the wild card (*) option. Normally this will include 'self' but also other third party styles or fonts resources (e.g: https://fonts.googleapis.com or other CDNs) + + // Notes about the CSP below: + // - script-src 'unsafe-eval' is currently required because agent-js uses a WebAssembly module for the validation of bls signatures. + // There is currently no other way to allow execution of WebAssembly modules with CSP. + // See: https://github.com/WebAssembly/content-security-policy/blob/main/proposals/CSP.md. + // - We added img-src data: because data: images are used often. + // - frame-ancestors: none mitigates clickjacking attacks. See https://owasp.org/www-community/attacks/Clickjacking. + "Content-Security-Policy": "default-src 'self';script-src 'self' 'unsafe-eval' 'unsafe-inline';connect-src 'self' http://localhost:* https://icp0.io https://*.icp0.io https://icp-api.io;img-src 'self' data:;style-src * 'unsafe-inline';style-src-elem * 'unsafe-inline';font-src *;object-src 'none';base-uri 'self';frame-ancestors 'none';form-action 'self';upgrade-insecure-requests;", + + // Security: The permissions policy disables all features for security reasons. If your site needs such permissions, activate them. + // To configure permissions go here https://www.permissionspolicy.com/ + "Permissions-Policy": "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), window-placement=(), vertical-scroll=()", + + // Security: Mitigates clickjacking attacks. + // See: https://owasp.org/www-community/attacks/Clickjacking. + "X-Frame-Options": "DENY", + + // Security: Avoids forwarding referrer information to other origins. + // See: https://owasp.org/www-project-secure-headers/#referrer-policy. + "Referrer-Policy": "same-origin", + + // Security: Tells the user’s browser that it must always use HTTPS with your site. + // See: https://owasp.org/www-project-secure-headers/#http-strict-transport-security + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + + // Security: Prevents the browser from interpreting files as a different MIME type to what is specified in the Content-Type header. + // See: https://owasp.org/www-project-secure-headers/#x-content-type-options + "X-Content-Type-Options": "nosniff", + + // Security: Enables browser features to mitigate some of the XSS attacks. Note that it has to be in mode=block. + // See: https://owasp.org/www-community/attacks/xss/ + "X-XSS-Protection": "1; mode=block" + }, + // Uncomment to redirect all requests from .raw.icp0.io to .icp0.io + // "allow_raw_access": false + }, +] diff --git a/src/dfx/assets/new_project_no_frontend_files/src/__project_name___frontend/assets/sample-asset.txt b/src/dfx/assets/project_templates/simple_assets/src/__project_name___frontend/assets/sample-asset.txt similarity index 100% rename from src/dfx/assets/new_project_no_frontend_files/src/__project_name___frontend/assets/sample-asset.txt rename to src/dfx/assets/project_templates/simple_assets/src/__project_name___frontend/assets/sample-asset.txt diff --git a/src/dfx/assets/project_templates/svelte/dfx.json-patch b/src/dfx/assets/project_templates/svelte/dfx.json-patch new file mode 100644 index 0000000000..731bca83c1 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/dfx.json-patch @@ -0,0 +1,23 @@ +[ + { + "op": "add", + "path": "/canisters/{project_name}_frontend", + "value": { + "type": "assets", + "source": [ + "src/{project_name}_frontend/dist" + ], + "dependencies": [ + "{project_name}_backend" + ], + "workspace": "{project_name}_frontend" + } + }, + { + "op": "add", + "path": "/canisters/{project_name}_backend/declarations", + "value": { + "node_compatibility": true + } + } +] diff --git a/src/dfx/assets/project_templates/svelte/package.json-patch b/src/dfx/assets/project_templates/svelte/package.json-patch new file mode 100644 index 0000000000..c74a9e93c7 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/package.json-patch @@ -0,0 +1,7 @@ +[ + { + "op": "add", + "path": "/workspaces/-", + "value": "src/{project_name}_frontend" + } +] diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/package.json b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/package.json new file mode 100644 index 0000000000..5109a2622b --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "{project_name}_frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "setup": "npm i && dfx canister create {project_name}_backend && dfx generate {project_name}_backend && dfx deploy", + "start": "vite --port 3000", + "prebuild": "dfx generate", + "build": "tsc && vite build", + "format": "prettier --write \"src/**/*.{json,js,jsx,ts,tsx,css,scss}\"" + }, + "dependencies": { + "@dfinity/agent": "^0.20.0", + "@dfinity/candid": "^0.20.0", + "@dfinity/principal": "^0.20.0" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^2.0.0", + "@sveltejs/kit": "^1.21.0", + "@sveltejs/vite-plugin-svelte": "^2.4.2", + "dotenv": "^16.3.1", + "sass": "^1.63.6", + "svelte": "^4.0.1", + "svelte-check": "^3.4.4", + "typescript": "^5.1.3", + "vite": "^4.3.9", + "vite-plugin-environment": "^1.1.3" + } +} diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/app.d.ts b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/app.d.ts new file mode 100644 index 0000000000..899c7e8fca --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/app.html b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/app.html new file mode 100644 index 0000000000..dfae06cc73 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/app.html @@ -0,0 +1,16 @@ + + + + + + + + IC Hello Starter + %sveltekit.head% + + + +
%sveltekit.body%
+ + + \ No newline at end of file diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/index.scss b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/index.scss new file mode 100644 index 0000000000..c1c320834c --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/index.scss @@ -0,0 +1,37 @@ +body { + font-family: sans-serif; + font-size: 1.5rem; +} + +img { + max-width: 50vw; + max-height: 25vw; + display: block; + margin: auto; +} + +form { + display: flex; + justify-content: center; + gap: 0.5em; + flex-flow: row wrap; + max-width: 40vw; + margin: auto; + align-items: baseline; +} + +button[type="submit"] { + padding: 5px 20px; + margin: 10px auto; + float: right; +} + +#greeting { + margin: 10px auto; + padding: 10px 60px; + border: 1px solid #222; +} + +#greeting:empty { + display: none; +} diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/lib/canisters.js b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/lib/canisters.js new file mode 100644 index 0000000000..6030da0260 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/lib/canisters.js @@ -0,0 +1,12 @@ +import { createActor, canisterId } from 'declarations/{project_name}_backend'; +import { building } from '$app/environment'; + +function dummyActor() { + return new Proxy({}, { get() { throw new Error("Canister invoked while building"); } }); +} + +const buildingOrTesting = building || process.env.NODE_ENV === "test"; + +export const backend = buildingOrTesting + ? dummyActor() + : createActor(canisterId); diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/routes/+layout.js b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/routes/+layout.js new file mode 100644 index 0000000000..c8cacf0895 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/routes/+layout.js @@ -0,0 +1 @@ +export const prerender = true; \ No newline at end of file diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/routes/+page.svelte b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/routes/+page.svelte new file mode 100644 index 0000000000..472a02ea70 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/routes/+page.svelte @@ -0,0 +1,26 @@ + + +
+ DFINITY logo +
+
+
+ + + +
+
{greeting}
+
diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/vite-env.d.ts b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/static/.ic-assets.json5 b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/static/.ic-assets.json5 new file mode 100644 index 0000000000..887f555461 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/static/.ic-assets.json5 @@ -0,0 +1,56 @@ +[ + { + "match": "**/*", + "headers": { + // Security: The Content Security Policy (CSP) given below aims at working with many apps rather than providing maximal security. + // We recommend tightening the CSP for your specific application. Some recommendations are as follows: + // - Use the CSP Evaluator (https://csp-evaluator.withgoogle.com/) to validate the CSP you define. + // - Follow the “Strict CSP” recommendations (https://csp.withgoogle.com/docs/strict-csp.html). However, note that in the context of the IC, + // nonces cannot be used because the response bodies must be static to work well with HTTP asset certification. + // Thus, we recommend to include script hashes (in combination with strict-dynamic) in the CSP as described + // in https://csp.withgoogle.com/docs/faq.html in section “What if my site is static and I can't add nonces to scripts?”. + // See for example the II CSP (https://github.com/dfinity/internet-identity/blob/main/src/internet_identity/src/http.rs). + // - It is recommended to tighten the connect-src directive. With the current CSP configuration the browser can + // make requests to https://*.icp0.io, hence being able to call any canister via https://icp0.io/api/v2/canister/{canister-ID}. + // This could potentially be used in combination with another vulnerability (e.g. XSS) to exfiltrate private data. + // The developer can configure this policy to only allow requests to their specific canisters, + // e.g: connect-src 'self' https://icp-api.io/api/v2/canister/{my-canister-ID}, where {my-canister-ID} has the following format: aaaaa-aaaaa-aaaaa-aaaaa-aaa + // - It is recommended to configure style-src, style-src-elem and font-src directives with the resources your canister is going to use + // instead of using the wild card (*) option. Normally this will include 'self' but also other third party styles or fonts resources (e.g: https://fonts.googleapis.com or other CDNs) + + // Notes about the CSP below: + // - script-src 'unsafe-eval' is currently required because agent-js uses a WebAssembly module for the validation of bls signatures. + // There is currently no other way to allow execution of WebAssembly modules with CSP. + // See: https://github.com/WebAssembly/content-security-policy/blob/main/proposals/CSP.md. + // - We added img-src data: because data: images are used often. + // - frame-ancestors: none mitigates clickjacking attacks. See https://owasp.org/www-community/attacks/Clickjacking. + "Content-Security-Policy": "default-src 'self';script-src 'self' 'unsafe-eval' 'unsafe-inline';connect-src 'self' http://localhost:* https://icp0.io https://*.icp0.io https://icp-api.io;img-src 'self' data:;style-src * 'unsafe-inline';style-src-elem * 'unsafe-inline';font-src *;object-src 'none';base-uri 'self';frame-ancestors 'none';form-action 'self';upgrade-insecure-requests;", + + // Security: The permissions policy disables all features for security reasons. If your site needs such permissions, activate them. + // To configure permissions go here https://www.permissionspolicy.com/ + "Permissions-Policy": "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), window-placement=(), vertical-scroll=()", + + // Security: Mitigates clickjacking attacks. + // See: https://owasp.org/www-community/attacks/Clickjacking. + "X-Frame-Options": "DENY", + + // Security: Avoids forwarding referrer information to other origins. + // See: https://owasp.org/www-project-secure-headers/#referrer-policy. + "Referrer-Policy": "same-origin", + + // Security: Tells the user’s browser that it must always use HTTPS with your site. + // See: https://owasp.org/www-project-secure-headers/#http-strict-transport-security + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + + // Security: Prevents the browser from interpreting files as a different MIME type to what is specified in the Content-Type header. + // See: https://owasp.org/www-project-secure-headers/#x-content-type-options + "X-Content-Type-Options": "nosniff", + + // Security: Enables browser features to mitigate some of the XSS attacks. Note that it has to be in mode=block. + // See: https://owasp.org/www-community/attacks/xss/ + "X-XSS-Protection": "1; mode=block" + }, + // Uncomment to redirect all requests from .raw.icp0.io to .icp0.io + // "allow_raw_access": false + }, +] diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/static/favicon.ico b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/static/favicon.ico new file mode 100644 index 0000000000..338fbf34cd Binary files /dev/null and b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/static/favicon.ico differ diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/static/logo2.svg b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/static/logo2.svg new file mode 100644 index 0000000000..74bc67e39d --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/static/logo2.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/svelte.config.js b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/svelte.config.js new file mode 100644 index 0000000000..0d8c6762d0 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/svelte.config.js @@ -0,0 +1,19 @@ +import adapter from '@sveltejs/adapter-static'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter({ + pages: 'dist', + assets: 'dist', + fallback: undefined, + precompress: false, + strict: true, + }), + }, +}; + +export default config; diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/tsconfig.json b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/tsconfig.json new file mode 100644 index 0000000000..efe61e1a02 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/vite.config.js b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/vite.config.js new file mode 100644 index 0000000000..51010df640 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte/src/__project_name___frontend/vite.config.js @@ -0,0 +1,43 @@ +import { fileURLToPath, URL } from 'url'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; +import environment from 'vite-plugin-environment'; +import dotenv from 'dotenv'; + +dotenv.config({ path: '../../.env' }); + +export default defineConfig({ + build: { + emptyOutDir: true, + }, + optimizeDeps: { + esbuildOptions: { + define: { + global: "globalThis", + }, + }, + }, + server: { + proxy: { + "/api": { + target: "http://127.0.0.1:4943", + changeOrigin: true, + }, + }, + }, + plugins: [ + sveltekit(), + environment("all", { prefix: "CANISTER_" }), + environment("all", { prefix: "DFX_" }), + ], + resolve: { + alias: [ + { + find: "declarations", + replacement: fileURLToPath( + new URL("../declarations", import.meta.url) + ), + }, + ], + }, +}); diff --git a/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/package.json-patch b/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/package.json-patch new file mode 100644 index 0000000000..b6d3f53c0c --- /dev/null +++ b/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/package.json-patch @@ -0,0 +1,27 @@ +[ + { + "op": "add", + "path": "/devDependencies/@testing-library~1jest-dom", + "value": "^5.16.5" + }, + { + "op": "add", + "path": "/devDependencies/cross-fetch", + "value": "^3.1.6" + }, + { + "op": "add", + "path": "/devDependencies/jsdom", + "value": "^22.1.0" + }, + { + "op": "add", + "path": "/devDependencies/vitest", + "value": "^0.32.2" + }, + { + "op": "add", + "path": "/scripts/test", + "value": "vitest run" + } +] \ No newline at end of file diff --git a/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/src/setupTests.js b/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/src/setupTests.js new file mode 100644 index 0000000000..c29162e04f --- /dev/null +++ b/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/src/setupTests.js @@ -0,0 +1,5 @@ +import matchers from '@testing-library/jest-dom/matchers'; +import 'cross-fetch/polyfill'; +import { expect } from 'vitest'; + +expect.extend(matchers); diff --git a/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/src/tests/App.test.js b/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/src/tests/App.test.js new file mode 100644 index 0000000000..0d5b86dc99 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/src/tests/App.test.js @@ -0,0 +1,19 @@ +import { test, expect, afterEach } from 'vitest'; +import App from '../routes/+page.svelte'; + +let host; + +afterEach(() => { + host.remove(); +}); + +test('mount component', async () => { + host = document.createElement('div'); + host.setAttribute('id', 'host'); + document.body.appendChild(host); + const instance = new App({ target: host, props: {} }); + expect(instance).toBeTruthy(); + expect(host.innerHTML).toMatchInlineSnapshot( + '"
\\"DFINITY

"', + ); +}); diff --git a/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/tsconfig.json-patch b/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/tsconfig.json-patch new file mode 100644 index 0000000000..b1501d3f56 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/tsconfig.json-patch @@ -0,0 +1,7 @@ +[ + { + "op": "add", + "path": "/compilerOptions/types/-", + "value": "@testing-library/jest-dom" + } +] diff --git a/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/vite.config.js.patch b/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/vite.config.js.patch new file mode 100644 index 0000000000..c2f6c8f6d3 --- /dev/null +++ b/src/dfx/assets/project_templates/svelte_tests/src/__project_name___frontend/vite.config.js.patch @@ -0,0 +1,11 @@ +--- a/src/{project_name}_frontend/vite.config.js ++++ b/src/{project_name}_frontend/vite.config.js +@@ -1,1 +1,2 @@ ++/// + import { fileURLToPath, URL } from 'url'; +@@ -33,1 +33,5 @@ ++ test: { ++ environment: 'jsdom', ++ setupFiles: 'src/setupTests.js', ++ }, + resolve: { diff --git a/src/dfx/assets/project_templates/vanilla_js/dfx.json-patch b/src/dfx/assets/project_templates/vanilla_js/dfx.json-patch new file mode 100644 index 0000000000..fa6e3bd3ed --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js/dfx.json-patch @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/canisters/{project_name}_frontend", + "value": { + "type": "assets", + "source": [ + "src/{project_name}_frontend/dist" + ], + "dependencies": [ + "{project_name}_backend" + ], + "workspace": "{project_name}_frontend" + } + } +] diff --git a/src/dfx/assets/project_templates/vanilla_js/package.json-patch b/src/dfx/assets/project_templates/vanilla_js/package.json-patch new file mode 100644 index 0000000000..c74a9e93c7 --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js/package.json-patch @@ -0,0 +1,7 @@ +[ + { + "op": "add", + "path": "/workspaces/-", + "value": "src/{project_name}_frontend" + } +] diff --git a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/.ic-assets.json5 b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/assets/.ic-assets.json5 similarity index 96% rename from src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/.ic-assets.json5 rename to src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/assets/.ic-assets.json5 index b58d2e1fc6..394b81c66e 100644 --- a/src/dfx/assets/new_project_node_files/src/__project_name___frontend/assets/.ic-assets.json5 +++ b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/assets/.ic-assets.json5 @@ -50,8 +50,7 @@ // See: https://owasp.org/www-community/attacks/xss/ "X-XSS-Protection": "1; mode=block" }, - // Set the allow_raw_access field to false to redirect all requests from .raw.icp0.io to .icp0.io - // The default behavior is to allow raw access. - // "allow_raw_access": true + // Uncomment to redirect all requests from .raw.icp0.io to .icp0.io + // "allow_raw_access": false }, ] diff --git a/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/assets/favicon.ico b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/assets/favicon.ico new file mode 100644 index 0000000000..338fbf34cd Binary files /dev/null and b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/assets/favicon.ico differ diff --git a/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/index.html b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/index.html new file mode 100644 index 0000000000..6c1db68a54 --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + IC Hello Starter + + + +
+ + + + \ No newline at end of file diff --git a/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/package.json b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/package.json new file mode 100644 index 0000000000..9c7a8d6c8d --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "{project_name}_frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "setup": "npm i && dfx canister create {project_name}_backend && dfx generate {project_name}_backend && dfx deploy", + "start": "vite --port 3000", + "prebuild": "dfx generate", + "build": "tsc && vite build", + "format": "prettier --write \"src/**/*.{json,js,jsx,ts,tsx,css,scss}\"" + }, + "devDependencies": { + "@testing-library/jest-dom": "^5.16.5", + "cross-fetch": "^3.1.6", + "dotenv": "^16.3.1", + "sass": "^1.63.6", + "typescript": "^5.1.3", + "vite": "^4.3.9", + "vite-plugin-environment": "^1.1.3", + "vitest": "^0.32.2" + }, + "dependencies": { + "@dfinity/agent": "^0.20.0", + "@dfinity/candid": "^0.20.0", + "@dfinity/principal": "^0.20.0", + "lit-html": "^2.8.0" + } +} diff --git a/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/App.js b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/App.js new file mode 100644 index 0000000000..1cac8128fb --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/App.js @@ -0,0 +1,40 @@ +import { html, render } from 'lit-html'; +import { {project_name}_backend } from 'declarations/{project_name}_backend'; +import logo from './logo2.svg'; + +class App { + greeting = ''; + + constructor() { + this.#render(); + } + + #handleSubmit = async (e) => { + e.preventDefault(); + const name = document.getElementById('name').value; + this.greeting = await {project_name}_backend.greet(name); + this.#render(); + }; + + #render() { + let body = html` +
+ DFINITY logo +
+
+
+ + + +
+
${this.greeting}
+
+ `; + render(body, document.getElementById('root')); + document + .querySelector('form') + .addEventListener('submit', this.#handleSubmit); + } +} + +export default App; diff --git a/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/index.scss b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/index.scss new file mode 100644 index 0000000000..c1c320834c --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/index.scss @@ -0,0 +1,37 @@ +body { + font-family: sans-serif; + font-size: 1.5rem; +} + +img { + max-width: 50vw; + max-height: 25vw; + display: block; + margin: auto; +} + +form { + display: flex; + justify-content: center; + gap: 0.5em; + flex-flow: row wrap; + max-width: 40vw; + margin: auto; + align-items: baseline; +} + +button[type="submit"] { + padding: 5px 20px; + margin: 10px auto; + float: right; +} + +#greeting { + margin: 10px auto; + padding: 10px 60px; + border: 1px solid #222; +} + +#greeting:empty { + display: none; +} diff --git a/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/logo2.svg b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/logo2.svg new file mode 100644 index 0000000000..74bc67e39d --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/logo2.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/main.js b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/main.js new file mode 100644 index 0000000000..16c07d59d0 --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/main.js @@ -0,0 +1,4 @@ +import App from './App'; +import './index.scss'; + +const app = new App(); diff --git a/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/vite-env.d.ts b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/tsconfig.json b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/tsconfig.json new file mode 100644 index 0000000000..39a545e919 --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/vite.config.js b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/vite.config.js new file mode 100644 index 0000000000..18ceb500c7 --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js/src/__project_name___frontend/vite.config.js @@ -0,0 +1,42 @@ +import { defineConfig } from 'vite'; +import { fileURLToPath, URL } from 'url'; +import environment from 'vite-plugin-environment'; +import dotenv from 'dotenv'; + +dotenv.config({ path: '../../.env' }); + +export default defineConfig({ + build: { + emptyOutDir: true, + }, + optimizeDeps: { + esbuildOptions: { + define: { + global: "globalThis", + }, + }, + }, + server: { + proxy: { + "/api": { + target: "http://127.0.0.1:4943", + changeOrigin: true, + }, + }, + }, + publicDir: "assets", + plugins: [ + environment("all", { prefix: "CANISTER_" }), + environment("all", { prefix: "DFX_" }), + ], + resolve: { + alias: [ + { + find: "declarations", + replacement: fileURLToPath( + new URL("../declarations", import.meta.url) + ), + }, + ], + }, +}); diff --git a/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/package.json-patch b/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/package.json-patch new file mode 100644 index 0000000000..b24159f32b --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/package.json-patch @@ -0,0 +1,27 @@ +[ + { + "op": "add", + "path": "/devDependencies/@testing-library~1jest-dom", + "value": "^5.16.5" + }, + { + "op": "add", + "path": "/devDependencies/cross-fetch", + "value": "^3.1.6" + }, + { + "op": "add", + "path": "/devDependencies/jsdom", + "value": "^22.1.0" + }, + { + "op": "add", + "path": "/devDependencies/vitest", + "value": "^0.32.2" + }, + { + "op": "add", + "path": "/scripts/test", + "value": "vitest run" + } +] diff --git a/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/src/setupTests.js b/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/src/setupTests.js new file mode 100644 index 0000000000..c29162e04f --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/src/setupTests.js @@ -0,0 +1,5 @@ +import matchers from '@testing-library/jest-dom/matchers'; +import 'cross-fetch/polyfill'; +import { expect } from 'vitest'; + +expect.extend(matchers); diff --git a/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/src/tests/App.test.js b/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/src/tests/App.test.js new file mode 100644 index 0000000000..14502a8907 --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/src/tests/App.test.js @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; +import App from '../App'; + +describe('App', () => { + it('renders as expected', () => { + const root = document.createElement('div'); + root.id = 'root'; + document.body.appendChild(root); + new App(); + + expect(root.querySelector('main')).toBeTruthy(); + }); +}); diff --git a/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/tsconfig.json-patch b/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/tsconfig.json-patch new file mode 100644 index 0000000000..b1501d3f56 --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/tsconfig.json-patch @@ -0,0 +1,7 @@ +[ + { + "op": "add", + "path": "/compilerOptions/types/-", + "value": "@testing-library/jest-dom" + } +] diff --git a/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/vite.config.js.patch b/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/vite.config.js.patch new file mode 100644 index 0000000000..dce9050f78 --- /dev/null +++ b/src/dfx/assets/project_templates/vanilla_js_tests/src/__project_name___frontend/vite.config.js.patch @@ -0,0 +1,11 @@ +--- a/src/{project_name}_frontend/vite.config.js ++++ b/src/{project_name}_frontend/vite.config.js +@@ -1,1 +1,2 @@ ++/// + import { defineConfig } from 'vite'; +@@ -32,1 +32,5 @@ ++ test: { ++ environment: 'jsdom', ++ setupFiles: 'src/setupTests.js', ++ }, + resolve: { diff --git a/src/dfx/assets/project_templates/vue/dfx.json-patch b/src/dfx/assets/project_templates/vue/dfx.json-patch new file mode 100644 index 0000000000..a608dc8351 --- /dev/null +++ b/src/dfx/assets/project_templates/vue/dfx.json-patch @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/canisters/{project_name}_frontend", + "value": { + "type": "assets", + "dependencies": [ + "{project_name}_backend" + ], + "source": [ + "src/{project_name}_frontend/dist" + ], + "workspace": "{project_name}_frontend" + } + } +] diff --git a/src/dfx/assets/project_templates/vue/package.json-patch b/src/dfx/assets/project_templates/vue/package.json-patch new file mode 100644 index 0000000000..c74a9e93c7 --- /dev/null +++ b/src/dfx/assets/project_templates/vue/package.json-patch @@ -0,0 +1,7 @@ +[ + { + "op": "add", + "path": "/workspaces/-", + "value": "src/{project_name}_frontend" + } +] diff --git a/src/dfx/assets/project_templates/vue/src/__project_name___frontend/index.html b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/index.html new file mode 100644 index 0000000000..6e80f94528 --- /dev/null +++ b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/index.html @@ -0,0 +1,21 @@ + + + + + + + + + IC Hello Starter + + + + +
+ + + + \ No newline at end of file diff --git a/src/dfx/assets/project_templates/vue/src/__project_name___frontend/package.json b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/package.json new file mode 100644 index 0000000000..8f7ebedadb --- /dev/null +++ b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "{project_name}_frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "setup": "npm i && dfx canister create {project_name}_backend && dfx generate {project_name}_backend && dfx deploy", + "start": "vite --port 3000", + "prebuild": "dfx generate", + "build": "tsc && vite build", + "format": "prettier --write \"src/**/*.{json,js,jsx,ts,tsx,css,scss}\"" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.2.3", + "@vue/tsconfig": "^0.4.0", + "dotenv": "^16.3.1", + "prettier": "^2.8.8", + "sass": "^1.63.6", + "typescript": "^5.1.3", + "vite": "^4.3.9", + "vite-plugin-environment": "^1.1.3" + }, + "dependencies": { + "pinia": "^2.1.6", + "vue": "^3.3.4", + "@dfinity/agent": "^0.20.0", + "@dfinity/candid": "^0.20.0", + "@dfinity/principal": "^0.20.0" + } +} diff --git a/src/dfx/assets/project_templates/vue/src/__project_name___frontend/public/.ic-assets.json5 b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/public/.ic-assets.json5 new file mode 100644 index 0000000000..394b81c66e --- /dev/null +++ b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/public/.ic-assets.json5 @@ -0,0 +1,56 @@ +[ + { + "match": "**/*", + "headers": { + // Security: The Content Security Policy (CSP) given below aims at working with many apps rather than providing maximal security. + // We recommend tightening the CSP for your specific application. Some recommendations are as follows: + // - Use the CSP Evaluator (https://csp-evaluator.withgoogle.com/) to validate the CSP you define. + // - Follow the “Strict CSP” recommendations (https://csp.withgoogle.com/docs/strict-csp.html). However, note that in the context of the IC, + // nonces cannot be used because the response bodies must be static to work well with HTTP asset certification. + // Thus, we recommend to include script hashes (in combination with strict-dynamic) in the CSP as described + // in https://csp.withgoogle.com/docs/faq.html in section “What if my site is static and I can't add nonces to scripts?”. + // See for example the II CSP (https://github.com/dfinity/internet-identity/blob/main/src/internet_identity/src/http.rs). + // - It is recommended to tighten the connect-src directive. With the current CSP configuration the browser can + // make requests to https://*.icp0.io, hence being able to call any canister via https://icp0.io/api/v2/canister/{canister-ID}. + // This could potentially be used in combination with another vulnerability (e.g. XSS) to exfiltrate private data. + // The developer can configure this policy to only allow requests to their specific canisters, + // e.g: connect-src 'self' https://icp-api.io/api/v2/canister/{my-canister-ID}, where {my-canister-ID} has the following format: aaaaa-aaaaa-aaaaa-aaaaa-aaa + // - It is recommended to configure style-src, style-src-elem and font-src directives with the resources your canister is going to use + // instead of using the wild card (*) option. Normally this will include 'self' but also other third party styles or fonts resources (e.g: https://fonts.googleapis.com or other CDNs) + + // Notes about the CSP below: + // - script-src 'unsafe-eval' is currently required because agent-js uses a WebAssembly module for the validation of bls signatures. + // There is currently no other way to allow execution of WebAssembly modules with CSP. + // See: https://github.com/WebAssembly/content-security-policy/blob/main/proposals/CSP.md. + // - We added img-src data: because data: images are used often. + // - frame-ancestors: none mitigates clickjacking attacks. See https://owasp.org/www-community/attacks/Clickjacking. + "Content-Security-Policy": "default-src 'self';script-src 'self' 'unsafe-eval';connect-src 'self' http://localhost:* https://icp0.io https://*.icp0.io https://icp-api.io;img-src 'self' data:;style-src * 'unsafe-inline';style-src-elem * 'unsafe-inline';font-src *;object-src 'none';base-uri 'self';frame-ancestors 'none';form-action 'self';upgrade-insecure-requests;", + + // Security: The permissions policy disables all features for security reasons. If your site needs such permissions, activate them. + // To configure permissions go here https://www.permissionspolicy.com/ + "Permissions-Policy": "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), window-placement=(), vertical-scroll=()", + + // Security: Mitigates clickjacking attacks. + // See: https://owasp.org/www-community/attacks/Clickjacking. + "X-Frame-Options": "DENY", + + // Security: Avoids forwarding referrer information to other origins. + // See: https://owasp.org/www-project-secure-headers/#referrer-policy. + "Referrer-Policy": "same-origin", + + // Security: Tells the user’s browser that it must always use HTTPS with your site. + // See: https://owasp.org/www-project-secure-headers/#http-strict-transport-security + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + + // Security: Prevents the browser from interpreting files as a different MIME type to what is specified in the Content-Type header. + // See: https://owasp.org/www-project-secure-headers/#x-content-type-options + "X-Content-Type-Options": "nosniff", + + // Security: Enables browser features to mitigate some of the XSS attacks. Note that it has to be in mode=block. + // See: https://owasp.org/www-community/attacks/xss/ + "X-XSS-Protection": "1; mode=block" + }, + // Uncomment to redirect all requests from .raw.icp0.io to .icp0.io + // "allow_raw_access": false + }, +] diff --git a/src/dfx/assets/project_templates/vue/src/__project_name___frontend/public/favicon.ico b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/public/favicon.ico new file mode 100644 index 0000000000..338fbf34cd Binary files /dev/null and b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/public/favicon.ico differ diff --git a/src/dfx/assets/project_templates/vue/src/__project_name___frontend/public/logo2.svg b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/public/logo2.svg new file mode 100644 index 0000000000..74bc67e39d --- /dev/null +++ b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/public/logo2.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dfx/assets/project_templates/vue/src/__project_name___frontend/src/App.vue b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/src/App.vue new file mode 100644 index 0000000000..72b5c2bb85 --- /dev/null +++ b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/src/App.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/dfx/assets/project_templates/vue/src/__project_name___frontend/src/index.scss b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/src/index.scss new file mode 100644 index 0000000000..c1c320834c --- /dev/null +++ b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/src/index.scss @@ -0,0 +1,37 @@ +body { + font-family: sans-serif; + font-size: 1.5rem; +} + +img { + max-width: 50vw; + max-height: 25vw; + display: block; + margin: auto; +} + +form { + display: flex; + justify-content: center; + gap: 0.5em; + flex-flow: row wrap; + max-width: 40vw; + margin: auto; + align-items: baseline; +} + +button[type="submit"] { + padding: 5px 20px; + margin: 10px auto; + float: right; +} + +#greeting { + margin: 10px auto; + padding: 10px 60px; + border: 1px solid #222; +} + +#greeting:empty { + display: none; +} diff --git a/src/dfx/assets/project_templates/vue/src/__project_name___frontend/src/main.js b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/src/main.js new file mode 100644 index 0000000000..e9655ff5f7 --- /dev/null +++ b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/src/main.js @@ -0,0 +1,6 @@ +import { createPinia } from 'pinia'; +import { createApp } from 'vue'; +import './index.scss'; +import App from './App.vue'; + +createApp(App).use(createPinia()).mount('#app'); diff --git a/src/dfx/assets/project_templates/vue/src/__project_name___frontend/src/vite-env.d.ts b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/dfx/assets/project_templates/vue/src/__project_name___frontend/tsconfig.json b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/tsconfig.json new file mode 100644 index 0000000000..6dc43ddca4 --- /dev/null +++ b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@vue/tsconfig/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/src/dfx/assets/project_templates/vue/src/__project_name___frontend/vite.config.js b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/vite.config.js new file mode 100644 index 0000000000..014c37999a --- /dev/null +++ b/src/dfx/assets/project_templates/vue/src/__project_name___frontend/vite.config.js @@ -0,0 +1,39 @@ +import { fileURLToPath, URL } from 'url'; +import { defineConfig } from 'vite'; +import environment from 'vite-plugin-environment'; +import vue from '@vitejs/plugin-vue'; +import dotenv from 'dotenv'; + +dotenv.config({ path: '../../.env' }); + +export default defineConfig({ + build: { + emptyOutDir: true, + }, + optimizeDeps: { + esbuildOptions: { + define: { + global: 'globalThis', + }, + }, + }, + server: { + proxy: { + '/api': { + target: 'http://127.0.0.1:4943', + changeOrigin: true, + }, + }, + }, + plugins: [ + vue(), + environment('all', { prefix: 'CANISTER_' }), + environment('all', { prefix: 'DFX_' }), + ], + resolve: { + alias: [ + { find: 'declarations', replacement: fileURLToPath(new URL('../declarations', import.meta.url)) }, + { find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) }, + ] + } +}); diff --git a/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/package.json-patch b/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/package.json-patch new file mode 100644 index 0000000000..897f6feefa --- /dev/null +++ b/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/package.json-patch @@ -0,0 +1,32 @@ +[ + { + "op": "add", + "path": "/devDependencies/@testing-library~1jest-dom", + "value": "^5.16.5" + }, + { + "op": "add", + "path": "/devDependencies/@vue~1test-utils", + "value": "^2.4.1" + }, + { + "op": "add", + "path": "/devDependencies/cross-fetch", + "value": "^3.1.6" + }, + { + "op": "add", + "path": "/devDependencies/jsdom", + "value": "^22.1.0" + }, + { + "op": "add", + "path": "/devDependencies/vitest", + "value": "^0.32.2" + }, + { + "op": "add", + "path": "/scripts/test", + "value": "vitest run" + } +] diff --git a/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/src/setupTests.js b/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/src/setupTests.js new file mode 100644 index 0000000000..c29162e04f --- /dev/null +++ b/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/src/setupTests.js @@ -0,0 +1,5 @@ +import matchers from '@testing-library/jest-dom/matchers'; +import 'cross-fetch/polyfill'; +import { expect } from 'vitest'; + +expect.extend(matchers); diff --git a/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/src/tests/App.test.js b/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/src/tests/App.test.js new file mode 100644 index 0000000000..539ec485b5 --- /dev/null +++ b/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/src/tests/App.test.js @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import App from '../App.vue'; +import { mount } from '@vue/test-utils'; + +describe('App', () => { + it('renders as expected', () => { + const root = document.createElement('div'); + root.id = 'root'; + document.body.appendChild(root); + mount(App, { attachTo: root }); + + expect(root.querySelector('main')).toBeTruthy(); + }); +}); diff --git a/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/tsconfig.json-patch b/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/tsconfig.json-patch new file mode 100644 index 0000000000..b1501d3f56 --- /dev/null +++ b/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/tsconfig.json-patch @@ -0,0 +1,7 @@ +[ + { + "op": "add", + "path": "/compilerOptions/types/-", + "value": "@testing-library/jest-dom" + } +] diff --git a/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/vite.config.js.patch b/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/vite.config.js.patch new file mode 100644 index 0000000000..c2f6c8f6d3 --- /dev/null +++ b/src/dfx/assets/project_templates/vue_tests/src/__project_name___frontend/vite.config.js.patch @@ -0,0 +1,11 @@ +--- a/src/{project_name}_frontend/vite.config.js ++++ b/src/{project_name}_frontend/vite.config.js +@@ -1,1 +1,2 @@ ++/// + import { fileURLToPath, URL } from 'url'; +@@ -33,1 +33,5 @@ ++ test: { ++ environment: 'jsdom', ++ setupFiles: 'src/setupTests.js', ++ }, + resolve: { diff --git a/src/dfx/src/commands/new.rs b/src/dfx/src/commands/new.rs index 98569b85b2..4e1fbe04c8 100644 --- a/src/dfx/src/commands/new.rs +++ b/src/dfx/src/commands/new.rs @@ -1,22 +1,24 @@ use crate::config::cache::DiskBasedCache; use crate::lib::environment::Environment; use crate::lib::error::{DfxError, DfxResult}; +use crate::lib::info::replica_rev; use crate::lib::manifest::{get_latest_version, is_upgrade_necessary}; use crate::lib::program; use crate::util::assets; use crate::util::clap::parsers::project_name_parser; use anyhow::{anyhow, bail, ensure, Context}; -use clap::Parser; +use clap::{Parser, ValueEnum}; use console::{style, Style}; -use dfx_core::config::model::dfinity::CONFIG_FILE_NAME; use dfx_core::json::{load_json_file, save_json_file}; +use dialoguer::theme::ColorfulTheme; +use dialoguer::{FuzzySelect, MultiSelect}; use fn_error_context::context; use indicatif::HumanBytes; use semver::Version; -use serde_json::Value; use slog::{info, warn, Logger}; use std::collections::BTreeMap; -use std::io::Read; +use std::fmt::{self, Display, Formatter}; +use std::io::{self, IsTerminal, Read}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::time::Duration; @@ -43,17 +45,17 @@ pub struct NewOpts { #[arg(value_parser = project_name_parser)] project_name: String, - /// Choose the type of canister in the starter project. Default to be motoko. - #[arg(long, value_parser = ["motoko", "rust"], default_value = "motoko")] - r#type: String, + /// Choose the type of canister in the starter project. + #[arg(long, value_enum)] + r#type: Option, /// Provides a preview the directories and files to be created without adding them to the file system. #[arg(long)] dry_run: bool, - /// Installs the frontend code example for the default canister. This defaults to true if Node is installed, or false if it isn't. - #[arg(long)] - frontend: bool, + /// Choose the type of frontend in the starter project. Defaults to vanilla. + #[arg(long, value_enum, default_missing_value = "vanilla")] + frontend: Option, /// Skip installing the frontend code example. #[arg(long, conflicts_with = "frontend")] @@ -63,6 +65,78 @@ pub struct NewOpts { /// NPM to decide. #[arg(long, requires("frontend"))] agent_version: Option, + + #[arg(long, value_enum)] + extras: Vec, +} + +#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq)] +enum BackendType { + Motoko, + Rust, + Azle, + Kybra, +} + +impl Display for BackendType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Motoko => "Motoko", + Self::Rust => "Rust", + Self::Azle => "TypeScript (Azle)", + Self::Kybra => "Python (Kybra)", + } + .fmt(f) + } +} + +#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq)] +enum FrontendType { + #[value(name = "sveltekit")] + SvelteKit, + Vanilla, + Vue, + React, + SimpleAssets, + None, +} + +impl Display for FrontendType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::SvelteKit => "SvelteKit", + Self::Vanilla => "Vanilla JS", + Self::Vue => "Vue", + Self::React => "React", + Self::SimpleAssets => "No JS template", + Self::None => "No frontend canister", + } + .fmt(f) + } +} + +impl FrontendType { + fn has_js(&self) -> bool { + !matches!(self, Self::None | Self::SimpleAssets) + } +} + +#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq)] +enum Extra { + InternetIdentity, + Bitcoin, + FrontendTests, +} + +impl Display for Extra { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::InternetIdentity => "Internet Identity", + Self::Bitcoin => "Bitcoin (Regtest)", + Self::FrontendTests => "Frontend tests", + } + .fmt(f) + } } enum Status<'a> { @@ -250,6 +324,8 @@ fn npm_install(location: &Path) -> DfxResult { .arg("install") .arg("--quiet") .arg("--no-progress") + .arg("--workspaces") + .arg("--if-present") .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .current_dir(location) @@ -262,8 +338,8 @@ fn scaffold_frontend_code( env: &dyn Environment, dry_run: bool, project_name: &Path, - arg_no_frontend: bool, - arg_frontend: bool, + frontend: FrontendType, + extras: &[Extra], agent_version: &Option, variables: &BTreeMap, ) -> DfxResult { @@ -277,7 +353,7 @@ fn scaffold_frontend_code( .to_str() .ok_or_else(|| anyhow!("Invalid argument: project_name"))?; - if (node_installed && !arg_no_frontend) || arg_frontend { + if node_installed { // Check if node is available, and if it is create the files for the frontend build. let js_agent_version = if let Some(v) = agent_version { v.clone() @@ -293,56 +369,37 @@ fn scaffold_frontend_code( project_name_str.to_uppercase(), ); - let mut new_project_node_files = assets::new_project_node_files()?; + let mut new_project_files = match frontend { + FrontendType::Vanilla => assets::new_project_vanillajs_files(), + FrontendType::SvelteKit => assets::new_project_svelte_files(), + FrontendType::Vue => assets::new_project_vue_files(), + FrontendType::React => assets::new_project_react_files(), + FrontendType::SimpleAssets => assets::new_project_assets_files(), + FrontendType::None => unreachable!(), + }?; write_files_from_entries( log, - &mut new_project_node_files, + &mut new_project_files, project_name, dry_run, &variables, )?; + if extras.contains(&Extra::FrontendTests) { + let mut test_files = match frontend { + FrontendType::SvelteKit => assets::new_project_svelte_test_files(), + FrontendType::React => assets::new_project_react_test_files(), + FrontendType::Vue => assets::new_project_vue_test_files(), + FrontendType::Vanilla => assets::new_project_vanillajs_test_files(), + FrontendType::SimpleAssets => { + bail!("Cannot add frontend tests to --frontend-type simple-assets") + } + FrontendType::None => bail!("Cannot add frontend tests to --no-frontend"), + }?; + write_files_from_entries(log, &mut test_files, project_name, dry_run, &variables)?; + } - let dfx_path = project_name.join(CONFIG_FILE_NAME); - let content = - std::fs::read(&dfx_path).with_context(|| format!("Failed to read {:?}.", &dfx_path))?; - let mut config_json: Value = serde_json::from_slice(&content) - .map_err(std::io::Error::from) - .with_context(|| format!("Failed to parse {}.", dfx_path.to_string_lossy()))?; - - let frontend_value: serde_json::Map = [( - "entrypoint".to_string(), - ("src/".to_owned() + project_name_str + "_frontend/src/index.html").into(), - )] - .iter() - .cloned() - .collect(); - - // Only update the dfx.json and install node dependencies if we're not running in dry run. - if !dry_run { - let assets_canister_json = config_json - .pointer_mut(("/canisters/".to_owned() + project_name_str + "_frontend").as_str()) - .unwrap(); - assets_canister_json - .as_object_mut() - .unwrap() - .insert("frontend".to_string(), Value::from(frontend_value)); - - assets_canister_json - .as_object_mut() - .unwrap() - .get_mut("source") - .unwrap() - .as_array_mut() - .unwrap() - .push(Value::from( - "dist/".to_owned() + project_name_str + "_frontend/", - )); - - let pretty = serde_json::to_string_pretty(&config_json) - .context("Invalid data: Cannot serialize configuration file.")?; - std::fs::write(&dfx_path, pretty) - .with_context(|| format!("Failed to write to {}.", dfx_path.to_string_lossy()))?; - + // Only install node dependencies if we're not running in dry run. + if !dry_run && frontend != FrontendType::SimpleAssets { // Install node modules. Error is not blocking, we just show a message instead. if node_installed { let b = env.new_spinner("Installing node dependencies...".into()); @@ -357,7 +414,7 @@ fn scaffold_frontend_code( } } } else { - if !arg_frontend && !node_installed { + if !node_installed { warn!( log, "Node could not be found. Skipping installing the frontend example code." @@ -369,7 +426,7 @@ fn scaffold_frontend_code( } write_files_from_entries( log, - &mut assets::new_project_no_frontend_files()?, + &mut assets::new_project_assets_files()?, project_name, dry_run, variables, @@ -399,11 +456,21 @@ fn get_agent_js_version_from_npm(dist_tag: &str) -> DfxResult { }) } -pub fn exec(env: &dyn Environment, opts: NewOpts) -> DfxResult { +pub fn exec(env: &dyn Environment, mut opts: NewOpts) -> DfxResult { + use BackendType::*; let log = env.get_logger(); let dry_run = opts.dry_run; - let project_name = Path::new(opts.project_name.as_str()); + let r#type = if let Some(r#type) = opts.r#type { + r#type + } else if opts.frontend.is_none() && opts.extras.is_empty() && io::stdout().is_terminal() { + opts = get_opts_interactively(opts)?; + opts.r#type.unwrap() + } else { + Motoko + }; + + let project_name = Path::new(opts.project_name.as_str()); if project_name.exists() { bail!("Cannot create a new project because the directory already exists."); } @@ -441,6 +508,7 @@ pub fn exec(env: &dyn Environment, opts: NewOpts) -> DfxResult { ("project_name".to_string(), project_name_str.to_string()), ("dfx_version".to_string(), version_str.clone()), ("dot".to_string(), ".".to_string()), + ("ic_commit".to_string(), replica_rev().to_string()), ] .iter() .cloned() @@ -454,11 +522,28 @@ pub fn exec(env: &dyn Environment, opts: NewOpts) -> DfxResult { &variables, )?; + let frontend = if opts.no_frontend { + FrontendType::None + } else { + opts.frontend.unwrap_or(FrontendType::Vanilla) + }; + + if r#type == Azle || frontend.has_js() { + write_files_from_entries( + log, + &mut assets::new_project_js_files().context("Failed to get JS config archive.")?, + project_name, + dry_run, + &variables, + )?; + } + // Default to start with motoko - let mut new_project_files = match opts.r#type.as_str() { - "rust" => assets::new_project_rust_files().context("Failed to get rust archive.")?, - "motoko" => assets::new_project_motoko_files().context("Failed to get motoko archive.")?, - t => bail!("Unsupported canister type: {}", t), + let mut new_project_files = match r#type { + Rust => assets::new_project_rust_files().context("Failed to get rust archive.")?, + Motoko => assets::new_project_motoko_files().context("Failed to get motoko archive.")?, + Azle => assets::new_project_azle_files().context("Failed to get azle archive.")?, + Kybra => assets::new_project_kybra_files().context("Failed to get kybra archive.")?, }; write_files_from_entries( log, @@ -468,15 +553,35 @@ pub fn exec(env: &dyn Environment, opts: NewOpts) -> DfxResult { &variables, )?; - scaffold_frontend_code( - env, - dry_run, - project_name, - opts.no_frontend, - opts.frontend, - &opts.agent_version, - &variables, - )?; + if opts.extras.contains(&Extra::InternetIdentity) { + write_files_from_entries( + log, + &mut assets::new_project_internet_identity_files()?, + project_name, + dry_run, + &variables, + )?; + } + if opts.extras.contains(&Extra::Bitcoin) { + write_files_from_entries( + log, + &mut assets::new_project_bitcoin_files()?, + project_name, + dry_run, + &variables, + )?; + } + if frontend != FrontendType::None { + scaffold_frontend_code( + env, + dry_run, + project_name, + frontend, + &opts.extras, + &opts.agent_version, + &variables, + )?; + } if !dry_run { // If on mac, we should validate that XCode toolchain was installed. @@ -508,7 +613,7 @@ pub fn exec(env: &dyn Environment, opts: NewOpts) -> DfxResult { init_git(log, project_name)?; } - if opts.r#type == "rust" { + if r#type == Rust { // dfx build will use --locked, so update the lockfile beforehand const MSG: &str = "You will need to run it yourself (or a similar command like `cargo vendor`), because `dfx build` will use the --locked flag with Cargo."; if let Ok(code) = Command::new("cargo") @@ -545,6 +650,45 @@ pub fn exec(env: &dyn Environment, opts: NewOpts) -> DfxResult { Ok(()) } +fn get_opts_interactively(opts: NewOpts) -> DfxResult { + use BackendType::*; + use Extra::*; + use FrontendType::*; + let theme = ColorfulTheme::default(); + let backends_list = [Motoko, Rust, Azle, Kybra]; + let backend = FuzzySelect::with_theme(&theme) + .items(&backends_list) + .default(0) + .with_prompt("Select a backend language:") + .interact()?; + let backend = backends_list[backend]; + let frontends_list = [SvelteKit, React, Vue, Vanilla, SimpleAssets, None]; + let frontend = FuzzySelect::with_theme(&theme) + .items(&frontends_list) + .default(0) + .with_prompt("Select a frontend framework:") + .interact()?; + let frontend = frontends_list[frontend]; + let mut extras_list = vec![InternetIdentity, Bitcoin]; + if frontend != None && frontend != SimpleAssets { + extras_list.push(FrontendTests); + } + let extras = MultiSelect::with_theme(&theme) + .items(&extras_list) + .with_prompt("Add extra features (space to select, enter to confirm)") + .interact()?; + + let extras = extras.into_iter().map(|x| extras_list[x]).collect(); + + let opts = NewOpts { + extras, + frontend: Some(frontend), + r#type: Some(backend), + ..opts + }; + Ok(opts) +} + fn warn_upgrade(log: &Logger, latest_version: Option<&Version>, current_version: &Version) { warn!(log, "You seem to be running an outdated version of dfx."); diff --git a/src/dfx/src/lib/builders/assets.rs b/src/dfx/src/lib/builders/assets.rs index eff29745ac..aab01faea8 100644 --- a/src/dfx/src/lib/builders/assets.rs +++ b/src/dfx/src/lib/builders/assets.rs @@ -27,6 +27,8 @@ struct AssetsBuilderExtra { /// the canister does not have a frontend or can be built using the default /// `npm run build` command. build: Vec, + /// The NPM workspace that the project is located in. + workspace: Option, } impl AssetsBuilderExtra { @@ -45,10 +47,12 @@ impl AssetsBuilderExtra { .collect::>>().with_context( || format!("Failed to collect dependencies (canister ids) of canister {}.", info.get_name()))?; let info = info.as_info::()?; let build = info.get_build_tasks().to_owned(); + let workspace = info.get_npm_workspace().map(str::to_owned); Ok(AssetsBuilderExtra { dependencies, build, + workspace, }) } } @@ -109,6 +113,7 @@ impl CanisterBuilder for AssetsBuilder { let AssetsBuilderExtra { build, dependencies, + workspace, } = AssetsBuilderExtra::try_from(info, pool)?; let vars = super::get_and_write_environment_variables( @@ -125,6 +130,7 @@ impl CanisterBuilder for AssetsBuilder { &config.network_name, vars, &build, + workspace.as_deref(), )?; let assets_canister_info = info.as_info::()?; @@ -195,6 +201,7 @@ fn build_frontend( network_name: &str, vars: Vec>, build: &[String], + workspace: Option<&str>, ) -> DfxResult { let custom_build_frontend = !build.is_empty(); let build_frontend = project_root.join("package.json").exists(); @@ -228,6 +235,10 @@ fn build_frontend( cmd.arg("run").arg("build"); + if let Some(workspace) = workspace { + cmd.arg("--workspace").arg(workspace); + } + if NetworkDescriptor::is_ic(network_name, &vec![]) { cmd.env("NODE_ENV", "production"); } diff --git a/src/dfx/src/lib/canister_info.rs b/src/dfx/src/lib/canister_info.rs index eec28cf406..0e1e4bf173 100644 --- a/src/dfx/src/lib/canister_info.rs +++ b/src/dfx/src/lib/canister_info.rs @@ -123,7 +123,7 @@ impl CanisterInfo { let declarations_config = CanisterDeclarationsConfig { output: declarations_config_pre .output - .or_else(|| Some(PathBuf::from("src/declarations").join(name))), + .or_else(|| Some(workspace_root.join("src/declarations").join(name))), bindings: declarations_config_pre .bindings .or_else(|| Some(vec!["js".to_string(), "ts".to_string(), "did".to_string()])), diff --git a/src/dfx/src/lib/canister_info/assets.rs b/src/dfx/src/lib/canister_info/assets.rs index e00e675cbf..f66601ca15 100644 --- a/src/dfx/src/lib/canister_info/assets.rs +++ b/src/dfx/src/lib/canister_info/assets.rs @@ -12,6 +12,7 @@ pub struct AssetsCanisterInfo { output_wasm_path: PathBuf, output_idl_path: PathBuf, build: Vec, + workspace: Option, } impl AssetsCanisterInfo { @@ -27,6 +28,9 @@ impl AssetsCanisterInfo { pub fn get_build_tasks(&self) -> &[String] { &self.build } + pub fn get_npm_workspace(&self) -> Option<&str> { + self.workspace.as_deref() + } #[context("Failed to assert source paths.")] pub fn assert_source_paths(&self) -> DfxResult<()> { @@ -55,15 +59,19 @@ impl CanisterInfoFactory for AssetsCanisterInfo { fn create(info: &CanisterInfo) -> DfxResult { let input_root = info.get_workspace_root().to_path_buf(); // If there are no "source" field, we just ignore this. - let (source_paths, build) = - if let CanisterTypeProperties::Assets { source, build } = info.type_specific.clone() { - (source, build.into_vec()) - } else { - bail!( - "Attempted to construct an assets canister from a type:{} canister config", - info.type_specific.name() - ) - }; + let (source_paths, build, workspace) = if let CanisterTypeProperties::Assets { + source, + build, + workspace, + } = info.type_specific.clone() + { + (source, build.into_vec(), workspace) + } else { + bail!( + "Attempted to construct an assets canister from a type:{} canister config", + info.type_specific.name() + ) + }; let output_root = info.get_output_root(); @@ -76,6 +84,7 @@ impl CanisterInfoFactory for AssetsCanisterInfo { output_wasm_path, output_idl_path, build, + workspace, }) } }