Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: extension defined project templates #4012

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ Added a flag `--replica` to `dfx start`. This flag currently has no effect.
Once PocketIC becomes the default for `dfx start` this flag will start the replica instead.
You can use the `--replica` flag already to write scripts that anticipate that change.

### feat: extensions can define project templates

An extension can define one or more project templates for `dfx new` to use.
These can be new templates or replace the built-in project templates.

# 0.24.3

### feat: Bitcoin support in PocketIC
Expand Down
66 changes: 66 additions & 0 deletions docs/concepts/extension-defined-project-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Extension-Defined Project Templates

## Overview

An extension can define one or more project templates for `dfx new` to use.

A project template is a set of files that `dfx new` copies or patches into a new project.

For examples of project template files, see the [project_templates] directory in the SDK repository.

# Specification

The `project_templates` field in an extension's `extension.json` defines the project templates
included in the extension. It is an object field mapping `project template name -> project template properties`.
These are the properties of a project template:

| Field | Type | Description |
|------------------------------|---------------------------|------------------------------------------------------------------------------------------------------|
| `display` | String | Display name of the project template |
| `category` | String | Category for inclusion in `--backend` and `--frontend` CLI options, as well as interactive selection |
| `requirements` | Array of String | Required project templates |
| `post_create` | String or Array of String | Command(s) to run after adding the canister to the project |
| `post_create_spinner_message` | String | Message to display while running the post_create command |
| `post_create_failure_warning` | String | Warning to display if the post_create command fails |

Within the files distributed with the extension, the project template files are
located in the `project_templates/{project template name}` directory.

## The `display` field

The `display` field is a string that describes the project template.
`dfx new` will use this value for interactive selection of project templates.

## The `category` field

The `category` field is an array of strings that categorize the project template.
`dfx new` uses this field to determine whether to include this project template
as an option for the `--backend` and `-frontend` flags, as well as in interactive setup.

Valid values for the field:
- `frontend`
- `backend`
- `extra`
- `frontend-test`
- `support`

## The `requirements` field

The `requirements` field lists any project templates that `dfx new` must apply before this project template.
For example, many of the frontend templates depend on the `dfx_js_base` template, which adds
package.json and tsconfig.json to the project.

## The `post_create` field

The `post_create` field specifies a command or commands to run after adding the project template files to the project.
For example, the rust project template runs `cargo update` after adding the files.

## The `post_create_spinner_message` field

If this field is set, `dfx new` will display a spinner with this message while running the `post_create` command.

## The `post_create_failure_warning` field

If this field is present and the `post_create` command fails, `dfx new` will display this warning but won't stop creating the project.

[project_templates]: https://github.com/dfinity/sdk/tree/master/src/dfx/assets/project_templates
2 changes: 2 additions & 0 deletions docs/concepts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

- [Asset Canister Interface](../design/asset-canister-interface.md)
- [Canister metadata](./canister-metadata.md)
- [Extension-Defined Canister Types](./extension-defined-canister-types.md)
- [Extension-Defined Project Templates](./extension-defined-project-templates.md)
84 changes: 84 additions & 0 deletions docs/extension-manifest-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@
"name": {
"type": "string"
},
"project_templates": {
"type": [
"object",
"null"
],
"additionalProperties": {
"$ref": "#/definitions/ExtensionProjectTemplate"
}
},
"subcommands": {
"anyOf": [
{
Expand Down Expand Up @@ -155,6 +164,58 @@
}
]
},
"ExtensionProjectTemplate": {
"type": "object",
"required": [
"category",
"display",
"post_create",
"requirements"
],
"properties": {
"category": {
"description": "Used to determine which CLI group (`--type`, `--backend`, `--frontend`) as well as for interactive selection",
"allOf": [
{
"$ref": "#/definitions/ProjectTemplateCategory"
}
]
},
"display": {
"description": "The name used for display and sorting",
"type": "string"
},
"post_create": {
"description": "Run a command after adding the canister to dfx.json",
"allOf": [
{
"$ref": "#/definitions/SerdeVec_for_String"
}
]
},
"post_create_failure_warning": {
"description": "If the post-create command fails, display this warning but don't fail",
"type": [
"string",
"null"
]
},
"post_create_spinner_message": {
"description": "If set, display a spinner while this command runs",
"type": [
"string",
"null"
]
},
"requirements": {
"description": "Other project templates to patch in alongside this one",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"ExtensionSubcommandArgOpts": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -231,6 +292,16 @@
"$ref": "#/definitions/ExtensionSubcommandOpts"
}
},
"ProjectTemplateCategory": {
"type": "string",
"enum": [
"backend",
"frontend",
"frontend-test",
"extra",
"support"
]
},
"Range_of_uint": {
"type": "object",
"required": [
Expand All @@ -249,6 +320,19 @@
"minimum": 0.0
}
}
},
"SerdeVec_for_String": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
}
}
127 changes: 127 additions & 0 deletions e2e/tests-dfx/extension.bash
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,133 @@ teardown() {
standard_teardown
}

@test "extension-defined project template" {
start_webserver --directory www
EXTENSION_URL="http://localhost:$E2E_WEB_SERVER_PORT/arbitrary/extension.json"
mkdir -p www/arbitrary/downloads www/arbitrary/project_templates/a-template

cat > www/arbitrary/extension.json <<EOF
{
"name": "an-extension",
"version": "0.1.0",
"homepage": "https://github.com/dfinity/dfx-extensions",
"authors": "DFINITY",
"summary": "Test extension for e2e purposes.",
"categories": [],
"keywords": [],
"project_templates": {
"rust-by-extension": {
"category": "backend",
"display": "rust by extension",
"requirements": [],
"post_create": "cargo update",
"port_create_failure_warning": "You will need to run it yourself (or a similar command like 'cargo vendor'), because 'dfx build' will use the --locked flag with Cargo."
}
},
"download_url_template": "http://localhost:$E2E_WEB_SERVER_PORT/arbitrary/downloads/{{tag}}.{{archive-format}}"
}
EOF

cat > www/arbitrary/dependencies.json <<EOF
{
"0.1.0": {
"dfx": {
"version": ">=0.8.0"
}
}
}
EOF

cp -R "${BATS_TEST_DIRNAME}/../../src/dfx/assets/project_templates/rust" www/arbitrary/project_templates/rust-by-extension

ARCHIVE_BASENAME="an-extension-v0.1.0"

mkdir "$ARCHIVE_BASENAME"
cp www/arbitrary/extension.json "$ARCHIVE_BASENAME"
cp -R www/arbitrary/project_templates "$ARCHIVE_BASENAME"
tar -czf "$ARCHIVE_BASENAME".tar.gz "$ARCHIVE_BASENAME"
rm -rf "$ARCHIVE_BASENAME"

mv "$ARCHIVE_BASENAME".tar.gz www/arbitrary/downloads/

assert_command dfx extension install "$EXTENSION_URL"

setup_rust

dfx new rbe --type rust-by-extension --no-frontend
cd rbe || exit

dfx_start
assert_command dfx deploy
assert_command dfx canister call rbe_backend greet '("Rust By Extension")'
assert_contains "Hello, Rust By Extension!"
}

@test "extension-defined project template replaces built-in type" {
start_webserver --directory www
EXTENSION_URL="http://localhost:$E2E_WEB_SERVER_PORT/arbitrary/extension.json"
mkdir -p www/arbitrary/downloads www/arbitrary/project_templates/a-template

cat > www/arbitrary/extension.json <<EOF
{
"name": "an-extension",
"version": "0.1.0",
"homepage": "https://github.com/dfinity/dfx-extensions",
"authors": "DFINITY",
"summary": "Test extension for e2e purposes.",
"categories": [],
"keywords": [],
"project_templates": {
"rust": {
"category": "backend",
"display": "rust by extension",
"requirements": [],
"post_create": "cargo update"
}
},
"download_url_template": "http://localhost:$E2E_WEB_SERVER_PORT/arbitrary/downloads/{{tag}}.{{archive-format}}"
}
EOF

cat > www/arbitrary/dependencies.json <<EOF
{
"0.1.0": {
"dfx": {
"version": ">=0.8.0"
}
}
}
EOF

cp -R "${BATS_TEST_DIRNAME}/../../src/dfx/assets/project_templates/rust" www/arbitrary/project_templates/rust
echo "just-proves-it-used-the-project-template" > www/arbitrary/project_templates/rust/proof.txt

ARCHIVE_BASENAME="an-extension-v0.1.0"

mkdir "$ARCHIVE_BASENAME"
cp www/arbitrary/extension.json "$ARCHIVE_BASENAME"
cp -R www/arbitrary/project_templates "$ARCHIVE_BASENAME"
tar -czf "$ARCHIVE_BASENAME".tar.gz "$ARCHIVE_BASENAME"
rm -rf "$ARCHIVE_BASENAME"

mv "$ARCHIVE_BASENAME".tar.gz www/arbitrary/downloads/

assert_command dfx extension install "$EXTENSION_URL"

setup_rust

dfx new rbe --type rust --no-frontend
assert_command cat rbe/proof.txt
assert_eq "just-proves-it-used-the-project-template"

cd rbe || exit

dfx_start
assert_command dfx deploy
assert_command dfx canister call rbe_backend greet '("Rust By Extension")'
assert_contains "Hello, Rust By Extension!"
}

@test "run an extension command with a canister type defined by another extension" {
install_shared_asset subnet_type/shared_network_settings/system
dfx_start_for_nns_install
Expand Down
8 changes: 6 additions & 2 deletions e2e/utils/_.bash
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,14 @@ dfx_new() {
echo PWD: "$(pwd)" >&2
}

dfx_new_rust() {
local project_name=${1:-e2e_project}
setup_rust() {
rustup default stable
rustup target add wasm32-unknown-unknown
}

dfx_new_rust() {
local project_name=${1:-e2e_project}
setup_rust
dfx new "${project_name}" --type=rust --no-frontend
test -d "${project_name}"
test -f "${project_name}/dfx.json"
Expand Down
7 changes: 6 additions & 1 deletion src/dfx-core/src/config/model/project_template.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
#[derive(Debug, Clone, Eq, PartialEq)]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ProjectTemplateCategory {
Backend,
Frontend,
#[serde(rename = "frontend-test")]
FrontendTest,
Extra,
Support,
Expand Down
13 changes: 9 additions & 4 deletions src/dfx-core/src/config/project_templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use itertools::Itertools;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::io;
use std::path::PathBuf;
use std::sync::OnceLock;

type GetArchiveFn = fn() -> Result<tar::Archive<flate2::read::GzDecoder<&'static [u8]>>, io::Error>;
Expand All @@ -11,6 +12,9 @@ type GetArchiveFn = fn() -> Result<tar::Archive<flate2::read::GzDecoder<&'static
pub enum ResourceLocation {
/// The template's assets are compiled into the dfx binary
Bundled { get_archive_fn: GetArchiveFn },

/// The templates assets are in a directory on the filesystem
Directory { path: PathBuf },
}

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
Expand Down Expand Up @@ -74,10 +78,11 @@ type ProjectTemplates = BTreeMap<ProjectTemplateName, ProjectTemplate>;

static PROJECT_TEMPLATES: OnceLock<ProjectTemplates> = OnceLock::new();

pub fn populate(builtin_templates: Vec<ProjectTemplate>) {
let templates = builtin_templates
.iter()
.map(|t| (t.name.clone(), t.clone()))
pub fn populate(builtin_templates: Vec<ProjectTemplate>, loaded_templates: Vec<ProjectTemplate>) {
let templates: ProjectTemplates = builtin_templates
.into_iter()
.map(|t| (t.name.clone(), t))
.chain(loaded_templates.into_iter().map(|t| (t.name.clone(), t)))
.collect();

PROJECT_TEMPLATES.set(templates).unwrap();
Expand Down
Loading
Loading