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: Add headless template #267

Merged
merged 27 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1c2ddb3
feat: add initial headless template
c12i Apr 18, 2024
bf16c3e
include headless template in ui framework choices
c12i Apr 18, 2024
d2a7aed
add web-app instructions and prefer explicit hc commands in all packa…
c12i Apr 18, 2024
f8a84e2
rename instructions template
c12i Apr 18, 2024
cf6e605
fix indentation in instructions
c12i Apr 19, 2024
651e351
update instructions
c12i Apr 19, 2024
a6e592e
remove gitkeep
c12i Apr 19, 2024
533a520
refactor UiFramework struct
c12i Apr 19, 2024
8fdc7ea
refactor tempalte config access
c12i Apr 19, 2024
65d69af
Merge remote-tracking branch 'holochain/develop' into add-headless-te…
c12i Apr 19, 2024
20907bd
remove assertion
c12i Apr 19, 2024
c942bd4
revert renaming
c12i Apr 19, 2024
08b0297
revert integrity zome name
c12i Apr 19, 2024
aa47171
choose non vanilla framework for non hello world examples
c12i Apr 19, 2024
76f6965
simplify ui framework prompts; update instructions
c12i Apr 22, 2024
730e540
update instructions
c12i Apr 22, 2024
e72fead
Update scripts and instructions
c12i Apr 23, 2024
14aacd7
temp
c12i May 10, 2024
1708cdc
color code ui frameworks
c12i May 10, 2024
4035dc0
templatify web-happ manifest
c12i May 10, 2024
7cc4e26
fix bug writing invalid template config
c12i May 12, 2024
06f9cf3
remove unnecessary print statement
c12i May 12, 2024
4152c81
remove unnecessary return statement
c12i May 12, 2024
721be67
update ui framework try from filetree implementation
c12i May 12, 2024
6137e41
refactor: refactor reserved words check
c12i May 12, 2024
9d90554
update ansi colors for lit and svelte
c12i May 13, 2024
1e2c1d1
Add build happ instruction
c12i May 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 24 additions & 32 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ use structopt::StructOpt;
pub struct HcScaffold {
#[structopt(short, long)]
/// The template to use for the scaffold command
/// Can either be an option from the built-in templates: "vanilla", "vue", "lit", "svelte"
/// Can either be an option from the built-in templates: "vanilla", "vue", "lit", "svelte", "headless"
/// Or a path to a custom template
template: Option<String>,

Expand Down Expand Up @@ -204,7 +204,6 @@ pub enum HcScaffoldCommand {
/// Skips UI generation for this collection.
no_ui: bool,
},

Example {
/// Name of the example to scaffold. One of ['hello-world', 'forum'].
example: Option<Example>,
Expand All @@ -217,44 +216,37 @@ pub enum HcScaffoldCommand {
impl HcScaffold {
pub async fn run(self) -> anyhow::Result<()> {
let current_dir = std::env::current_dir()?;
let template_config = if let Some(t) = &self.template {
// Only read from config if the template is inbuilt and not a path
if Path::new(t).exists() {
None
} else {
get_template_config(&current_dir)?
}
} else {
None
};
let template_config = get_template_config(&current_dir)?;
let template = match (&template_config, &self.template) {
(Some(config), Some(template)) if &config.template != template => {
return Err(ScaffoldError::InvalidArguments(format!(
"The value {template} passed with `--template` does not match the template the web-app was scaffolded with: {}",
config.template
"The value {} passed with `--template` does not match the template the web-app was scaffolded with: {}",
template.italic(),
config.template.italic(),
)).into())
}
(Some(config), _) => Some(&config.template),
// Only read from config if the template is inbuilt and not a path
(Some(config), _) if !Path::new(&config.template).exists() => Some(&config.template),
(_, t) => t.as_ref(),
};

// Given a template either passed via the --template flag or retreived via the hcScaffold config,
// get the template file tree and the ui framework name or custom template path
let (template, template_file_tree) = match template {
Some(template) => {
let template_name_or_path;
let file_tree = match template.as_str() {
"lit" | "svelte" | "vanilla" | "vue" => {
let ui_framework = UiFramework::from_str(template)?;
template_name_or_path = ui_framework.to_string();
ui_framework.template_filetree()?
}
custom_template_path => {
template_name_or_path = custom_template_path.to_string();
let templates_dir = current_dir.join(PathBuf::from(custom_template_path));
load_directory_into_memory(&templates_dir)?
}
};
(template_name_or_path.to_owned(), file_tree)
}
Some(template) => match template.to_lowercase().as_str() {
"lit" | "svelte" | "vanilla" | "vue" | "headless" => {
let ui_framework = UiFramework::from_str(template)?;
(ui_framework.name(), ui_framework.template_filetree()?)
}
custom_template_path if Path::new(custom_template_path).exists() => {
let templates_dir = current_dir.join(PathBuf::from(custom_template_path));
(
custom_template_path.to_string(),
load_directory_into_memory(&templates_dir)?,
)
}
path => return Err(ScaffoldError::PathNotFound(PathBuf::from(path)).into()),
},
None => {
let ui_framework = match self.command {
HcScaffoldCommand::WebApp { .. } => UiFramework::choose()?,
Expand All @@ -267,7 +259,7 @@ impl HcScaffold {
UiFramework::try_from(&file_tree)?
}
};
(ui_framework.to_string(), ui_framework.template_filetree()?)
(ui_framework.name(), ui_framework.template_filetree()?)
}
};

Expand Down
19 changes: 8 additions & 11 deletions src/reserved_words.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use convert_case::{Case, Casing};

use crate::error::{ScaffoldError, ScaffoldResult};

const RESERVED_WORDS: [&str; 27] = [
Expand Down Expand Up @@ -32,16 +30,15 @@ const RESERVED_WORDS: [&str; 27] = [
"Call",
];

// Returns an error if the given string is invalid due to it being a reserved word
/// Returns an error if the given string is invalid due to it being a reserved word
pub fn check_for_reserved_words(string_to_check: &str) -> ScaffoldResult<()> {
for w in RESERVED_WORDS {
if string_to_check
.to_case(Case::Lower)
.eq(&w.to_string().to_case(Case::Lower))
{
return Err(ScaffoldError::InvalidReservedWord(w.to_string()));
}
if RESERVED_WORDS
.iter()
.any(|w| string_to_check.eq_ignore_ascii_case(w))
{
return Err(ScaffoldError::InvalidReservedWord(
string_to_check.to_string(),
));
}

Ok(())
}
5 changes: 2 additions & 3 deletions src/scaffold/entry_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,10 @@ pub fn scaffold_entry_type(
.iter()
.map(|s| s.to_os_string())
.collect();
let empty_dir = dir! {};
choose_fields(
name,
&zome_file_tree,
template_file_tree.path(&mut v.iter()).unwrap_or(&empty_dir),
template_file_tree.path(&mut v.iter()).unwrap_or(&dir! {}),
no_ui,
)?
}
Expand Down Expand Up @@ -145,7 +144,7 @@ pub fn scaffold_entry_type(
.filter_map(|f| f.linked_from.clone())
.collect();

for l in linked_from.clone() {
for l in linked_from {
zome_file_tree = add_link_type_to_integrity_zome(
zome_file_tree,
&link_type_name(&l, &entry_def.referenceable()),
Expand Down
8 changes: 1 addition & 7 deletions src/scaffold/web_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,9 @@ fn web_app_skeleton(
.insert(OsString::from("flake.nix"), flake_nix(holo_enabled));
}

let mut scaffold_template_result =
let scaffold_template_result =
scaffold_web_app_template(app_file_tree, template_file_tree, &app_name, holo_enabled)?;

scaffold_template_result
.file_tree
.dir_content_mut()
.unwrap()
.insert(OsString::from("dnas"), dir! {});

Ok(scaffold_template_result)
}

Expand Down
52 changes: 36 additions & 16 deletions src/scaffold/web_app/uis.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, Select};
use include_dir::{include_dir, Dir};
use std::{ffi::OsString, path::PathBuf, str::FromStr};
Expand All @@ -11,22 +12,37 @@ static LIT_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates
static SVELTE_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/svelte");
static VUE_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/vue");
static VANILLA_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/vanilla");
static HEADLESS_TEMPLATE: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/headless");

#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub enum UiFramework {
Vanilla,
Lit,
Svelte,
Vue,
Headless,
}

impl UiFramework {
/// Gets the non-ANSI escaped name of the ui framework
pub fn name(&self) -> String {
let name = match self {
UiFramework::Vanilla => "vanilla",
UiFramework::Lit => "lit",
UiFramework::Svelte => "svelte",
UiFramework::Vue => "vue",
UiFramework::Headless => "headless",
};
name.to_string()
}

pub fn template_filetree(&self) -> ScaffoldResult<FileTree> {
let dir = match self {
UiFramework::Lit => &LIT_TEMPLATES,
UiFramework::Vanilla => &VANILLA_TEMPLATES,
UiFramework::Svelte => &SVELTE_TEMPLATES,
UiFramework::Vue => &VUE_TEMPLATES,
UiFramework::Headless => &HEADLESS_TEMPLATE,
};
dir_to_file_tree(dir)
}
Expand All @@ -37,29 +53,32 @@ impl UiFramework {
UiFramework::Svelte,
UiFramework::Vue,
UiFramework::Vanilla,
UiFramework::Headless,
];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Choose UI framework:")
.with_prompt("Choose UI framework: (Use arrow-keys. Return to submit)")
.default(0)
.items(&frameworks[..])
.interact()?;
Ok(frameworks[selection])
Ok(frameworks[selection].clone())
}

pub fn choose_non_vanilla() -> ScaffoldResult<UiFramework> {
let frameworks = [UiFramework::Lit, UiFramework::Svelte, UiFramework::Vue];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Choose UI framework:")
.with_prompt("Choose UI framework: (Use arrow-keys. Return to submit)")
.default(0)
.items(&frameworks[..])
.interact()?;
Ok(frameworks[selection])
Ok(frameworks[selection].clone())
}
}

impl TryFrom<&FileTree> for UiFramework {
type Error = ScaffoldError;

/// Try to get ui framework from app file tree, if the ui framework cannot be inferred, then
/// the user will be prompted to choose one via `UiFramework::choose`
fn try_from(app_file_tree: &FileTree) -> Result<Self, Self::Error> {
let ui_package_json_path = PathBuf::from("ui/package.json");
if file_exists(app_file_tree, &ui_package_json_path) {
Expand All @@ -71,8 +90,8 @@ impl TryFrom<&FileTree> for UiFramework {
.path(&mut v.iter())
.ok_or(ScaffoldError::PathNotFound(ui_package_json_path.clone()))?
.file_content()
.ok_or(ScaffoldError::PathNotFound(ui_package_json_path.clone()))?
.clone();
.map(|c| c.to_owned())
.ok_or(ScaffoldError::PathNotFound(ui_package_json_path.clone()))?;
if ui_package_json.contains("lit") {
return Ok(UiFramework::Lit);
} else if ui_package_json.contains("svelte") {
Expand All @@ -90,10 +109,11 @@ impl TryFrom<&FileTree> for UiFramework {
impl std::fmt::Display for UiFramework {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
UiFramework::Vanilla => "vanilla",
UiFramework::Lit => "lit",
UiFramework::Svelte => "svelte",
UiFramework::Vue => "vue",
UiFramework::Vanilla => "vanilla".yellow(),
UiFramework::Lit => "lit".bright_blue(),
UiFramework::Svelte => "svelte".bright_red(),
UiFramework::Vue => "vue".green(),
UiFramework::Headless => "headless (no ui)".italic(),
};
write!(f, "{str}")
}
Expand All @@ -103,15 +123,15 @@ impl FromStr for UiFramework {
type Err = ScaffoldError;

fn from_str(s: &str) -> ScaffoldResult<UiFramework> {
match s {
match s.to_ascii_lowercase().as_str() {
"vanilla" => Ok(UiFramework::Vanilla),
"svelte" => Ok(UiFramework::Svelte),
"vue" => Ok(UiFramework::Vue),
"lit" => Ok(UiFramework::Lit),
_ => Err(ScaffoldError::InvalidUiFramework(
s.to_string(),
"vanilla, lit, svelte, vue".to_string(),
)),
"headless" => Ok(UiFramework::Headless),
value => Err(ScaffoldError::MalformedTemplate(format!(
"Invalid value: {value}, expected vanilla, svelte, vue, lit or headless"
))),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
],
"scripts": {
"start": "AGENTS=2 BOOTSTRAP_PORT=$(port) SIGNAL_PORT=$(port) npm run network",
"network": "hc s clean && npm run build:happ && UI_PORT=8888 concurrently \"npm start -w ui\" \"npm run launch:happ\" \"holochain-playground\"",
"network": "hc sandbox clean && npm run build:happ && UI_PORT=8888 concurrently \"npm start -w ui\" \"npm run launch:happ\" \"holochain-playground\"",
"test": "npm run build:zomes && hc app pack workdir --recursive && npm t -w tests",
"launch:happ": "hc-spin -n $AGENTS --ui-port $UI_PORT workdir/{{app_name}}.happ",
"start:tauri": "AGENTS=2 BOOTSTRAP_PORT=$(port) SIGNAL_PORT=$(port) npm run network:tauri",
"network:tauri": "hc s clean && npm run build:happ && UI_PORT=8888 concurrently \"npm start -w ui\" \"npm run launch:tauri\" \"holochain-playground\"",
"network:tauri": "hc sandbox clean && npm run build:happ && UI_PORT=8888 concurrently \"npm start -w ui\" \"npm run launch:tauri\" \"holochain-playground\"",
"launch:tauri": "concurrently \"hc run-local-services --bootstrap-port $BOOTSTRAP_PORT --signal-port $SIGNAL_PORT\" \"echo pass | RUST_LOG=warn hc launch --piped -n $AGENTS workdir/{{app_name}}.happ --ui-port $UI_PORT network --bootstrap http://127.0.0.1:\"$BOOTSTRAP_PORT\" webrtc ws://127.0.0.1:\"$SIGNAL_PORT\"\"",
{{#if holo_enabled}}
"start:holo": "AGENTS=2 npm run network:holo",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { assert, test } from "vitest";

import { runScenario, dhtSync, CallableCell } from '@holochain/tryorama';
import {
NewEntryAction,
ActionHash,
Record,
Link,
AppBundleSource,
fakeActionHash,
fakeAgentPubKey,
fakeEntryHash
} from '@holochain/client';
import { decode } from '@msgpack/msgpack';

import { create{{pascal_case referenceable.name}} } from './common.js';

test('create a {{pascal_case referenceable.name}} and get {{lower_case collection_name}}', async () => {
await runScenario(async scenario => {
// Construct proper paths for your app.
// This assumes app bundle created by the `hc app pack` command.
const testAppPath = process.cwd() + '/../workdir/{{app_name}}.happ';

// Set up the app to be installed
const appSource = { appBundleSource: { path: testAppPath } };

// Add 2 players with the test app to the Scenario. The returned players
// can be destructured.
const [alice, bob] = await scenario.addPlayersWithApps([appSource, appSource]);

// Shortcut peer discovery through gossip and register all agents in every
// conductor of the scenario.
await scenario.shareAllAgents();

// Bob gets {{lower_case collection_name}}
let collectionOutput: Link[] = await bob.cells[0].callZome({
zome_name: "{{coordinator_zome_manifest.name}}",
fn_name: "get_{{snake_case collection_name}}",
payload: {{#if (eq collection_type.type "Global")}}null{{else}}alice.agentPubKey{{/if}}
});
assert.equal(collectionOutput.length, 0);

// Alice creates a {{pascal_case referenceable.name}}
const createRecord: Record = await create{{pascal_case referenceable.name}}(alice.cells[0]);
assert.ok(createRecord);

await dhtSync([alice, bob], alice.cells[0].cell_id[0]);

// Bob gets {{lower_case collection_name}} again
collectionOutput = await bob.cells[0].callZome({
zome_name: "{{coordinator_zome_manifest.name}}",
fn_name: "get_{{snake_case collection_name}}",
payload: {{#if (eq collection_type.type "Global")}}null{{else}}alice.agentPubKey{{/if}}
});
assert.equal(collectionOutput.length, 1);
assert.deepEqual({{#if (eq referenceable.hash_type "EntryHash")}}(createRecord.signed_action.hashed.content as NewEntryAction).entry_hash{{else}}createRecord.signed_action.hashed.hash{{/if}}, collectionOutput[0].target);
{{#if (and deletable (eq referenceable.hash_type "ActionHash"))}}

// Alice deletes the {{pascal_case referenceable.name}}
await alice.cells[0].callZome({
zome_name: "{{coordinator_zome_manifest.name}}",
fn_name: "delete_{{snake_case referenceable.name}}",
payload: createRecord.signed_action.hashed.hash
});

await dhtSync([alice, bob], alice.cells[0].cell_id[0]);

// Bob gets {{lower_case collection_name}} again
collectionOutput = await bob.cells[0].callZome({
zome_name: "{{coordinator_zome_manifest.name}}",
fn_name: "get_{{snake_case collection_name}}",
payload: {{#if (eq collection_type.type "Global")}}null{{else}}alice.agentPubKey{{/if}}
});
assert.equal(collectionOutput.length, 0);
{{/if}}
});
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CallableCell } from '@holochain/tryorama';
import { NewEntryAction, ActionHash, Record, AppBundleSource, fakeActionHash, fakeAgentPubKey, fakeEntryHash, fakeDnaHash } from '@holochain/client';