@@ -15,6 +15,24 @@ use crate::commands::{PackageManager, PackageManagerSpec};
1515pub const TEMPLATES_DIR : & str = "templates" ;
1616/// The directory selected template is promoted to
1717pub const APP_DIR : & str = "app" ;
18+ /// Reserved `--template` value selecting the no-frontend layout. Not a
19+ /// directory under `templates/` — there is nothing to instantiate.
20+ pub const NO_FRONTEND : & str = "none" ;
21+
22+ /// List of every JS-related file in the monorepo root that will be removed
23+ /// by the no-template CLI option. Only Rust/Cargo workspace remains.
24+ const NO_FRONTEND_REMOVALS : & [ & str ] = & [
25+ TEMPLATES_DIR ,
26+ "app-lib" ,
27+ "e2e" ,
28+ "tests" ,
29+ "scripts" ,
30+ ".husky" ,
31+ "node_modules" ,
32+ "package.json" ,
33+ "package-lock.json" ,
34+ ".prettierignore" ,
35+ ] ;
1836
1937/// The declared workspaces for the instantiated project. `e2e/` is kept (its
2038/// self-adapting config targets the single `app/` post-init); `templates/*` is
@@ -39,6 +57,8 @@ pub enum Error {
3957 Io ( #[ from] std:: io:: Error ) ,
4058 #[ error( "malformed root package.json: {0}" ) ]
4159 PackageJson ( #[ from] serde_json:: Error ) ,
60+ #[ error( "malformed environments.toml: {0}" ) ]
61+ EnvToml ( #[ from] toml_edit:: TomlError ) ,
4262}
4363
4464/// How `--template` (or the interactive prompt) selected a source.
@@ -52,11 +72,15 @@ pub enum TemplateSource {
5272 Framework ( String ) ,
5373 /// Community repo shorthand, e.g. `"org/repo#ref"`.
5474 Community ( String ) ,
75+ /// The reserved `none` value: contracts only, no UI layer.
76+ NoFrontend ,
5577}
5678
57- /// Parse a `--template` value into official or community source
79+ /// Parse a `--template` value into official, community, or no-frontend source
5880pub fn parse_template_arg ( value : & str ) -> TemplateSource {
59- if value. contains ( '/' ) {
81+ if value == NO_FRONTEND {
82+ TemplateSource :: NoFrontend
83+ } else if value. contains ( '/' ) {
6084 TemplateSource :: Community ( value. to_string ( ) )
6185 } else {
6286 TemplateSource :: Framework ( value. to_string ( ) )
@@ -106,6 +130,50 @@ pub fn instantiate(root: &Path, framework: &str) -> Result<(), Error> {
106130 Ok ( ( ) )
107131}
108132
133+ /// Assemble the no-frontend layout: strip every JS workspace and node config
134+ /// from the acquired monorepo, leaving only the Cargo/contracts side, and set
135+ /// `client = false` on every contract in `environments.toml` so `build`/`watch`
136+ /// never generate (or deploy for) client packages there is no app to consume.
137+ pub fn instantiate_no_frontend ( root : & Path ) -> Result < ( ) , Error > {
138+ if !root. join ( TEMPLATES_DIR ) . is_dir ( ) {
139+ return Err ( Error :: NoTemplatesDir ) ;
140+ }
141+ for name in NO_FRONTEND_REMOVALS {
142+ let path = root. join ( name) ;
143+ if path. is_dir ( ) {
144+ fs:: remove_dir_all ( & path) ?;
145+ } else if path. exists ( ) {
146+ fs:: remove_file ( & path) ?;
147+ }
148+ }
149+ disable_clients_in_env_toml ( root)
150+ }
151+
152+ /// Set `client = false` on every contract entry in every environment of
153+ /// `environments.toml`, preserving formatting and comments. Contracts absent
154+ /// from the file default to `client = true` at build time, so only listed
155+ /// entries need rewriting — the shipped monorepo lists all of its contracts.
156+ fn disable_clients_in_env_toml ( root : & Path ) -> Result < ( ) , Error > {
157+ let path = root. join ( "environments.toml" ) ;
158+ let Ok ( contents) = fs:: read_to_string ( & path) else {
159+ return Ok ( ( ) ) ;
160+ } ;
161+ let mut doc: toml_edit:: DocumentMut = contents. parse ( ) ?;
162+ for ( _, env) in doc. iter_mut ( ) {
163+ let Some ( contracts) = env. get_mut ( "contracts" ) . and_then ( |c| c. as_table_like_mut ( ) ) else {
164+ continue ;
165+ } ;
166+ let names: Vec < String > = contracts. iter ( ) . map ( |( k, _) | k. to_string ( ) ) . collect ( ) ;
167+ for name in names {
168+ if let Some ( contract) = contracts. get_mut ( & name) . and_then ( |c| c. as_table_like_mut ( ) ) {
169+ contract. insert ( "client" , toml_edit:: value ( false ) ) ;
170+ }
171+ }
172+ }
173+ fs:: write ( & path, doc. to_string ( ) ) ?;
174+ Ok ( ( ) )
175+ }
176+
109177/// Remove the committed visual-snapshot baselines from the kept `e2e/` suite.
110178/// They are per-framework (e.g. `home-react-darwin.png`); after init only the
111179/// single `app/` remains, so the user regenerates their own on first run.
@@ -225,6 +293,11 @@ mod tests {
225293 ) ;
226294 }
227295
296+ #[ test]
297+ fn parse_template_arg_none_is_no_frontend ( ) {
298+ assert_eq ! ( parse_template_arg( "none" ) , TemplateSource :: NoFrontend ) ;
299+ }
300+
228301 #[ test]
229302 fn parse_template_arg_community_when_slash ( ) {
230303 assert_eq ! (
@@ -314,6 +387,107 @@ mod tests {
314387 assert ! ( dir. path( ) . join( "templates/react" ) . exists( ) ) ;
315388 }
316389
390+ /// Extend the base fixture with the Cargo side + env toml the no-frontend
391+ /// layout keeps, and the extra node config it strips.
392+ fn no_frontend_fixture ( ) -> tempfile:: TempDir {
393+ let dir = fixture ( ) ;
394+ let root = dir. path ( ) ;
395+ write ( & root. join ( "Cargo.toml" ) , "[workspace]\n " ) ;
396+ write ( & root. join ( "scaffold.yml" ) , "version: 1\n " ) ;
397+ write (
398+ & root. join ( "environments.toml" ) ,
399+ "# top comment\n \
400+ [development.contracts]\n \
401+ fungible = { client = true, constructor_args = \" --admin me\" }\n \
402+ \n \
403+ # section-style contract\n \
404+ [development.contracts.guess_the_number]\n \
405+ client = true\n \
406+ \n \
407+ [staging.contracts.other]\n \
408+ id = \" C123\" \n ",
409+ ) ;
410+ write ( & root. join ( "tests/e2e/smoke.spec.ts" ) , "// smoke" ) ;
411+ write ( & root. join ( "scripts/dev-guard.mjs" ) , "// guard" ) ;
412+ write ( & root. join ( ".husky/pre-commit" ) , "npm test" ) ;
413+ write ( & root. join ( ".prettierignore" ) , "target\n " ) ;
414+ dir
415+ }
416+
417+ #[ test]
418+ fn no_frontend_strips_js_and_keeps_cargo_side ( ) {
419+ let dir = no_frontend_fixture ( ) ;
420+ let root = dir. path ( ) ;
421+ instantiate_no_frontend ( root) . unwrap ( ) ;
422+
423+ for gone in [
424+ "templates" ,
425+ "app-lib" ,
426+ "e2e" ,
427+ "tests" ,
428+ "scripts" ,
429+ ".husky" ,
430+ "package.json" ,
431+ ".prettierignore" ,
432+ ] {
433+ assert ! ( !root. join( gone) . exists( ) , "{gone} removed" ) ;
434+ }
435+ for kept in [
436+ "Cargo.toml" ,
437+ "scaffold.yml" ,
438+ "environments.toml" ,
439+ "contracts" ,
440+ ] {
441+ assert ! ( root. join( kept) . exists( ) , "{kept} kept" ) ;
442+ }
443+ assert ! ( !root. join( APP_DIR ) . exists( ) , "no app/ promoted" ) ;
444+ }
445+
446+ #[ test]
447+ fn no_frontend_disables_clients_in_env_toml ( ) {
448+ let dir = no_frontend_fixture ( ) ;
449+ let root = dir. path ( ) ;
450+ instantiate_no_frontend ( root) . unwrap ( ) ;
451+
452+ let contents = fs:: read_to_string ( root. join ( "environments.toml" ) ) . unwrap ( ) ;
453+ assert ! ( contents. contains( "# top comment" ) , "comments preserved" ) ;
454+ assert ! ( !contents. contains( "client = true" ) ) ;
455+
456+ let parsed: toml:: Value = toml:: from_str ( & contents) . unwrap ( ) ;
457+ for ( env, name) in [
458+ ( "development" , "fungible" ) ,
459+ ( "development" , "guess_the_number" ) ,
460+ ( "staging" , "other" ) ,
461+ ] {
462+ assert_eq ! (
463+ parsed[ env] [ "contracts" ] [ name] [ "client" ] ,
464+ toml:: Value :: Boolean ( false ) ,
465+ "{env}.{name} client disabled"
466+ ) ;
467+ }
468+ assert_eq ! (
469+ parsed[ "development" ] [ "contracts" ] [ "fungible" ] [ "constructor_args" ] ,
470+ toml:: Value :: String ( "--admin me" . into( ) ) ,
471+ "other settings untouched"
472+ ) ;
473+ }
474+
475+ #[ test]
476+ fn no_frontend_errors_without_templates_dir ( ) {
477+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
478+ assert ! ( matches!(
479+ instantiate_no_frontend( dir. path( ) ) ,
480+ Err ( Error :: NoTemplatesDir )
481+ ) ) ;
482+ }
483+
484+ #[ test]
485+ fn no_frontend_ok_without_environments_toml ( ) {
486+ let dir = fixture ( ) ;
487+ instantiate_no_frontend ( dir. path ( ) ) . unwrap ( ) ;
488+ assert ! ( !dir. path( ) . join( "templates" ) . exists( ) ) ;
489+ }
490+
317491 fn spec ( kind : PackageManager ) -> PackageManagerSpec {
318492 PackageManagerSpec {
319493 kind,
0 commit comments