Skip to content

Commit e05c888

Browse files
feat: extension defined project templates (#4012)
1 parent 1acb8fb commit e05c888

File tree

12 files changed

+459
-14
lines changed

12 files changed

+459
-14
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ Added a flag `--replica` to `dfx start`. This flag currently has no effect.
2020
Once PocketIC becomes the default for `dfx start` this flag will start the replica instead.
2121
You can use the `--replica` flag already to write scripts that anticipate that change.
2222

23+
### feat: extensions can define project templates
24+
25+
An extension can define one or more project templates for `dfx new` to use.
26+
These can be new templates or replace the built-in project templates.
27+
2328
# 0.24.3
2429

2530
### feat: Bitcoin support in PocketIC
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Extension-Defined Project Templates
2+
3+
## Overview
4+
5+
An extension can define one or more project templates for `dfx new` to use.
6+
7+
A project template is a set of files that `dfx new` copies or patches into a new project.
8+
9+
For examples of project template files, see the [project_templates] directory in the SDK repository.
10+
11+
# Specification
12+
13+
The `project_templates` field in an extension's `extension.json` defines the project templates
14+
included in the extension. It is an object field mapping `project template name -> project template properties`.
15+
These are the properties of a project template:
16+
17+
| Field | Type | Description |
18+
|------------------------------|---------------------------|------------------------------------------------------------------------------------------------------|
19+
| `display` | String | Display name of the project template |
20+
| `category` | String | Category for inclusion in `--backend` and `--frontend` CLI options, as well as interactive selection |
21+
| `requirements` | Array of String | Required project templates |
22+
| `post_create` | String or Array of String | Command(s) to run after adding the canister to the project |
23+
| `post_create_spinner_message` | String | Message to display while running the post_create command |
24+
| `post_create_failure_warning` | String | Warning to display if the post_create command fails |
25+
26+
Within the files distributed with the extension, the project template files are
27+
located in the `project_templates/{project template name}` directory.
28+
29+
## The `display` field
30+
31+
The `display` field is a string that describes the project template.
32+
`dfx new` will use this value for interactive selection of project templates.
33+
34+
## The `category` field
35+
36+
The `category` field is an array of strings that categorize the project template.
37+
`dfx new` uses this field to determine whether to include this project template
38+
as an option for the `--backend` and `-frontend` flags, as well as in interactive setup.
39+
40+
Valid values for the field:
41+
- `frontend`
42+
- `backend`
43+
- `extra`
44+
- `frontend-test`
45+
- `support`
46+
47+
## The `requirements` field
48+
49+
The `requirements` field lists any project templates that `dfx new` must apply before this project template.
50+
For example, many of the frontend templates depend on the `dfx_js_base` template, which adds
51+
package.json and tsconfig.json to the project.
52+
53+
## The `post_create` field
54+
55+
The `post_create` field specifies a command or commands to run after adding the project template files to the project.
56+
For example, the rust project template runs `cargo update` after adding the files.
57+
58+
## The `post_create_spinner_message` field
59+
60+
If this field is set, `dfx new` will display a spinner with this message while running the `post_create` command.
61+
62+
## The `post_create_failure_warning` field
63+
64+
If this field is present and the `post_create` command fails, `dfx new` will display this warning but won't stop creating the project.
65+
66+
[project_templates]: https://github.com/dfinity/sdk/tree/master/src/dfx/assets/project_templates

docs/concepts/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22

33
- [Asset Canister Interface](../design/asset-canister-interface.md)
44
- [Canister metadata](./canister-metadata.md)
5+
- [Extension-Defined Canister Types](./extension-defined-canister-types.md)
6+
- [Extension-Defined Project Templates](./extension-defined-project-templates.md)

docs/extension-manifest-schema.json

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@
7070
"name": {
7171
"type": "string"
7272
},
73+
"project_templates": {
74+
"type": [
75+
"object",
76+
"null"
77+
],
78+
"additionalProperties": {
79+
"$ref": "#/definitions/ExtensionProjectTemplate"
80+
}
81+
},
7382
"subcommands": {
7483
"anyOf": [
7584
{
@@ -155,6 +164,58 @@
155164
}
156165
]
157166
},
167+
"ExtensionProjectTemplate": {
168+
"type": "object",
169+
"required": [
170+
"category",
171+
"display",
172+
"post_create",
173+
"requirements"
174+
],
175+
"properties": {
176+
"category": {
177+
"description": "Used to determine which CLI group (`--type`, `--backend`, `--frontend`) as well as for interactive selection",
178+
"allOf": [
179+
{
180+
"$ref": "#/definitions/ProjectTemplateCategory"
181+
}
182+
]
183+
},
184+
"display": {
185+
"description": "The name used for display and sorting",
186+
"type": "string"
187+
},
188+
"post_create": {
189+
"description": "Run a command after adding the canister to dfx.json",
190+
"allOf": [
191+
{
192+
"$ref": "#/definitions/SerdeVec_for_String"
193+
}
194+
]
195+
},
196+
"post_create_failure_warning": {
197+
"description": "If the post-create command fails, display this warning but don't fail",
198+
"type": [
199+
"string",
200+
"null"
201+
]
202+
},
203+
"post_create_spinner_message": {
204+
"description": "If set, display a spinner while this command runs",
205+
"type": [
206+
"string",
207+
"null"
208+
]
209+
},
210+
"requirements": {
211+
"description": "Other project templates to patch in alongside this one",
212+
"type": "array",
213+
"items": {
214+
"type": "string"
215+
}
216+
}
217+
}
218+
},
158219
"ExtensionSubcommandArgOpts": {
159220
"type": "object",
160221
"properties": {
@@ -231,6 +292,16 @@
231292
"$ref": "#/definitions/ExtensionSubcommandOpts"
232293
}
233294
},
295+
"ProjectTemplateCategory": {
296+
"type": "string",
297+
"enum": [
298+
"backend",
299+
"frontend",
300+
"frontend-test",
301+
"extra",
302+
"support"
303+
]
304+
},
234305
"Range_of_uint": {
235306
"type": "object",
236307
"required": [
@@ -249,6 +320,19 @@
249320
"minimum": 0.0
250321
}
251322
}
323+
},
324+
"SerdeVec_for_String": {
325+
"anyOf": [
326+
{
327+
"type": "string"
328+
},
329+
{
330+
"type": "array",
331+
"items": {
332+
"type": "string"
333+
}
334+
}
335+
]
252336
}
253337
}
254338
}

e2e/tests-dfx/extension.bash

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,133 @@ teardown() {
1414
standard_teardown
1515
}
1616

17+
@test "extension-defined project template" {
18+
start_webserver --directory www
19+
EXTENSION_URL="http://localhost:$E2E_WEB_SERVER_PORT/arbitrary/extension.json"
20+
mkdir -p www/arbitrary/downloads www/arbitrary/project_templates/a-template
21+
22+
cat > www/arbitrary/extension.json <<EOF
23+
{
24+
"name": "an-extension",
25+
"version": "0.1.0",
26+
"homepage": "https://github.com/dfinity/dfx-extensions",
27+
"authors": "DFINITY",
28+
"summary": "Test extension for e2e purposes.",
29+
"categories": [],
30+
"keywords": [],
31+
"project_templates": {
32+
"rust-by-extension": {
33+
"category": "backend",
34+
"display": "rust by extension",
35+
"requirements": [],
36+
"post_create": "cargo update",
37+
"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."
38+
}
39+
},
40+
"download_url_template": "http://localhost:$E2E_WEB_SERVER_PORT/arbitrary/downloads/{{tag}}.{{archive-format}}"
41+
}
42+
EOF
43+
44+
cat > www/arbitrary/dependencies.json <<EOF
45+
{
46+
"0.1.0": {
47+
"dfx": {
48+
"version": ">=0.8.0"
49+
}
50+
}
51+
}
52+
EOF
53+
54+
cp -R "${BATS_TEST_DIRNAME}/../../src/dfx/assets/project_templates/rust" www/arbitrary/project_templates/rust-by-extension
55+
56+
ARCHIVE_BASENAME="an-extension-v0.1.0"
57+
58+
mkdir "$ARCHIVE_BASENAME"
59+
cp www/arbitrary/extension.json "$ARCHIVE_BASENAME"
60+
cp -R www/arbitrary/project_templates "$ARCHIVE_BASENAME"
61+
tar -czf "$ARCHIVE_BASENAME".tar.gz "$ARCHIVE_BASENAME"
62+
rm -rf "$ARCHIVE_BASENAME"
63+
64+
mv "$ARCHIVE_BASENAME".tar.gz www/arbitrary/downloads/
65+
66+
assert_command dfx extension install "$EXTENSION_URL"
67+
68+
setup_rust
69+
70+
dfx new rbe --type rust-by-extension --no-frontend
71+
cd rbe || exit
72+
73+
dfx_start
74+
assert_command dfx deploy
75+
assert_command dfx canister call rbe_backend greet '("Rust By Extension")'
76+
assert_contains "Hello, Rust By Extension!"
77+
}
78+
79+
@test "extension-defined project template replaces built-in type" {
80+
start_webserver --directory www
81+
EXTENSION_URL="http://localhost:$E2E_WEB_SERVER_PORT/arbitrary/extension.json"
82+
mkdir -p www/arbitrary/downloads www/arbitrary/project_templates/a-template
83+
84+
cat > www/arbitrary/extension.json <<EOF
85+
{
86+
"name": "an-extension",
87+
"version": "0.1.0",
88+
"homepage": "https://github.com/dfinity/dfx-extensions",
89+
"authors": "DFINITY",
90+
"summary": "Test extension for e2e purposes.",
91+
"categories": [],
92+
"keywords": [],
93+
"project_templates": {
94+
"rust": {
95+
"category": "backend",
96+
"display": "rust by extension",
97+
"requirements": [],
98+
"post_create": "cargo update"
99+
}
100+
},
101+
"download_url_template": "http://localhost:$E2E_WEB_SERVER_PORT/arbitrary/downloads/{{tag}}.{{archive-format}}"
102+
}
103+
EOF
104+
105+
cat > www/arbitrary/dependencies.json <<EOF
106+
{
107+
"0.1.0": {
108+
"dfx": {
109+
"version": ">=0.8.0"
110+
}
111+
}
112+
}
113+
EOF
114+
115+
cp -R "${BATS_TEST_DIRNAME}/../../src/dfx/assets/project_templates/rust" www/arbitrary/project_templates/rust
116+
echo "just-proves-it-used-the-project-template" > www/arbitrary/project_templates/rust/proof.txt
117+
118+
ARCHIVE_BASENAME="an-extension-v0.1.0"
119+
120+
mkdir "$ARCHIVE_BASENAME"
121+
cp www/arbitrary/extension.json "$ARCHIVE_BASENAME"
122+
cp -R www/arbitrary/project_templates "$ARCHIVE_BASENAME"
123+
tar -czf "$ARCHIVE_BASENAME".tar.gz "$ARCHIVE_BASENAME"
124+
rm -rf "$ARCHIVE_BASENAME"
125+
126+
mv "$ARCHIVE_BASENAME".tar.gz www/arbitrary/downloads/
127+
128+
assert_command dfx extension install "$EXTENSION_URL"
129+
130+
setup_rust
131+
132+
dfx new rbe --type rust --no-frontend
133+
assert_command cat rbe/proof.txt
134+
assert_eq "just-proves-it-used-the-project-template"
135+
136+
cd rbe || exit
137+
138+
dfx_start
139+
assert_command dfx deploy
140+
assert_command dfx canister call rbe_backend greet '("Rust By Extension")'
141+
assert_contains "Hello, Rust By Extension!"
142+
}
143+
17144
@test "run an extension command with a canister type defined by another extension" {
18145
install_shared_asset subnet_type/shared_network_settings/system
19146
dfx_start_for_nns_install

e2e/utils/_.bash

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,14 @@ dfx_new() {
8282
echo PWD: "$(pwd)" >&2
8383
}
8484

85-
dfx_new_rust() {
86-
local project_name=${1:-e2e_project}
85+
setup_rust() {
8786
rustup default stable
8887
rustup target add wasm32-unknown-unknown
88+
}
89+
90+
dfx_new_rust() {
91+
local project_name=${1:-e2e_project}
92+
setup_rust
8993
dfx new "${project_name}" --type=rust --no-frontend
9094
test -d "${project_name}"
9195
test -f "${project_name}/dfx.json"

src/dfx-core/src/config/model/project_template.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
#[derive(Debug, Clone, Eq, PartialEq)]
1+
use schemars::JsonSchema;
2+
use serde::{Deserialize, Serialize};
3+
4+
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
5+
#[serde(rename_all = "lowercase")]
26
pub enum ProjectTemplateCategory {
37
Backend,
48
Frontend,
9+
#[serde(rename = "frontend-test")]
510
FrontendTest,
611
Extra,
712
Support,

src/dfx-core/src/config/project_templates.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use itertools::Itertools;
33
use std::collections::BTreeMap;
44
use std::fmt::Display;
55
use std::io;
6+
use std::path::PathBuf;
67
use std::sync::OnceLock;
78

89
type GetArchiveFn = fn() -> Result<tar::Archive<flate2::read::GzDecoder<&'static [u8]>>, io::Error>;
@@ -11,6 +12,9 @@ type GetArchiveFn = fn() -> Result<tar::Archive<flate2::read::GzDecoder<&'static
1112
pub enum ResourceLocation {
1213
/// The template's assets are compiled into the dfx binary
1314
Bundled { get_archive_fn: GetArchiveFn },
15+
16+
/// The templates assets are in a directory on the filesystem
17+
Directory { path: PathBuf },
1418
}
1519

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

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

77-
pub fn populate(builtin_templates: Vec<ProjectTemplate>) {
78-
let templates = builtin_templates
79-
.iter()
80-
.map(|t| (t.name.clone(), t.clone()))
81+
pub fn populate(builtin_templates: Vec<ProjectTemplate>, loaded_templates: Vec<ProjectTemplate>) {
82+
let templates: ProjectTemplates = builtin_templates
83+
.into_iter()
84+
.map(|t| (t.name.clone(), t))
85+
.chain(loaded_templates.into_iter().map(|t| (t.name.clone(), t)))
8186
.collect();
8287

8388
PROJECT_TEMPLATES.set(templates).unwrap();

0 commit comments

Comments
 (0)