Skip to content

Commit

Permalink
feat: Add headless template (#267)
Browse files Browse the repository at this point in the history
* feat: add initial headless template

* include headless template in ui framework choices

* add web-app instructions and prefer explicit hc commands in all package.json templates

* rename instructions template

* fix indentation in instructions

* update instructions

* remove gitkeep

* refactor UiFramework struct

* refactor tempalte config access

* remove assertion

* revert renaming

* revert integrity zome name

* choose non vanilla framework for non hello world examples

* simplify ui framework prompts; update instructions

* update instructions

* Update scripts and instructions

* color code ui frameworks

* templatify web-happ manifest

* fix bug writing invalid template config

* remove unnecessary print statement

* remove unnecessary return statement

* update ui framework try from filetree implementation

* refactor: refactor reserved words check

* update ansi colors for lit and svelte

* Add build happ instruction
  • Loading branch information
c12i committed May 14, 2024
1 parent a999323 commit 30bb0da
Show file tree
Hide file tree
Showing 23 changed files with 837 additions and 80 deletions.
56 changes: 24 additions & 32 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,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 @@ -203,7 +203,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 @@ -216,44 +215,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 @@ -266,7 +258,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';

0 comments on commit 30bb0da

Please sign in to comment.