diff --git a/Cargo.lock b/Cargo.lock index 684bd7b..dd7063b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,6 +580,7 @@ dependencies = [ "logger", "metacall", "metassr-build", + "metassr-create", "metassr-server", "metassr-utils", "nu-ansi-term 0.50.0", @@ -617,6 +618,7 @@ dependencies = [ "logger", "metacall", "metassr-build", + "metassr-create", "metassr-server", "tokio", "tower-http", @@ -628,6 +630,7 @@ dependencies = [ name = "metassr-create" version = "0.1.0" dependencies = [ + "anyhow", "walkdir", ] diff --git a/Cargo.toml b/Cargo.toml index 3545bc1..e929ed5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ html-generator = { path = "crates/html-generator" } serde = "1.0.207" tower-layer = "0.3.3" tower-service = "0.3.3" +metassr-create = { version = "0.1.0", path = "crates/metassr-create" } [workspace] members = [ diff --git a/crates/metassr-create/.gitignore b/crates/metassr-create/.gitignore new file mode 100644 index 0000000..2b29f27 --- /dev/null +++ b/crates/metassr-create/.gitignore @@ -0,0 +1 @@ +tests diff --git a/crates/metassr-create/Cargo.toml b/crates/metassr-create/Cargo.toml index eff17fc..29b4e12 100644 --- a/crates/metassr-create/Cargo.toml +++ b/crates/metassr-create/Cargo.toml @@ -8,3 +8,6 @@ build = "build.rs" [build-dependencies] walkdir = "2.5.0" + +[dependencies] +anyhow = "1.0.86" diff --git a/crates/metassr-create/build.rs b/crates/metassr-create/build.rs index 5f8779f..f95d221 100644 --- a/crates/metassr-create/build.rs +++ b/crates/metassr-create/build.rs @@ -12,7 +12,7 @@ fn main() { generated_code.push_str("use std::collections::HashMap;\n\n"); generated_code - .push_str("pub fn load_templates() -> HashMap> {\n"); + .push_str("pub fn load_templates() -> HashMap>> {\n"); generated_code.push_str(" let mut templates = HashMap::new();\n"); for entry in WalkDir::new(templates_dir) @@ -30,10 +30,10 @@ fn main() { .join("/"); generated_code.push_str(&format!( - " templates.entry(\"{}\".to_string()).or_insert_with(HashMap::new).insert(\"{}\".to_string(), include_str!(r#\"{}\"#).to_string());\n", + " templates.entry(\"{}\".to_string()).or_insert_with(HashMap::new).insert(\"{}\".to_string(), include_bytes!(r#\"{}\"#).to_vec());\n", template_name, file_name, - path.display() + path.canonicalize().unwrap().display() )); } diff --git a/crates/metassr-create/src/lib.rs b/crates/metassr-create/src/lib.rs index 7d12d9a..16e4be4 100644 --- a/crates/metassr-create/src/lib.rs +++ b/crates/metassr-create/src/lib.rs @@ -1,14 +1,114 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right +include!(concat!(env!("OUT_DIR"), "/templates.rs")); + +use std::{ + fs::{create_dir_all, File}, + io::Write, + path::PathBuf, +}; + +use anyhow::{anyhow, Result}; +use templates::Template; + +mod templates; + +pub mod tags { + pub const VERSION: &str = "%VER%"; + pub const NAME: &str = "%NAME%"; + pub const DESC: &str = "%DESC%"; +} + +pub struct Creator { + project_name: String, + version: String, + description: String, + template: Template, +} +impl Creator { + pub fn new(project_name: &str, version: &str, desc: &str, template: &str) -> Self { + Self { + project_name: project_name.to_string(), + version: version.to_string(), + description: desc.to_string(), + template: Template::from(template), + } + } + pub fn generate(&self) -> Result<()> { + let template = self.template.load(&self)?; + let root = PathBuf::from(&self.project_name); + + if root.exists() { + return Err(anyhow!("Path already exists.")); + } + for (file, buf) in template { + let path = root.join(&file); + create_dir_all(path.parent().unwrap())?; + + let _ = File::create(path)?.write(&buf)?; + } + + Ok(()) + } } #[cfg(test)] -mod tests { - use super::*; +mod test { + use crate::{tags, Creator}; + use anyhow::Result; + use std::{ + env::set_current_dir, + fs::create_dir, + path::Path, + str::from_utf8, + time::{SystemTime, UNIX_EPOCH}, + }; + + fn init_test_dir() -> Result<()> { + let path = Path::new("tests"); + if !path.exists() { + create_dir(path)?; + } + set_current_dir(path)?; + Ok(()) + } + include!(concat!(env!("OUT_DIR"), "/templates.rs")); + #[test] + fn load_template() { + dbg!(&from_utf8( + load_templates() + .get("typescript") + .unwrap() + .get("src/_head.tsx") + .unwrap() + ) + .unwrap() + .replace(tags::VERSION, "1.0.0") + .replace(tags::NAME, "MetaSSR")); + } #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + fn generate_templates() -> Result<()> { + init_test_dir()?; + let project_name = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + .to_string(); + dbg!(&project_name); + Creator::new( + &format!("{}-javascript", project_name), + "1.0.0", + "Hello World!", + "js", + ) + .generate()?; + Creator::new( + &format!("{}-typescript", project_name), + "1.0.0", + "Hello World!", + "ts", + ) + .generate()?; + + Ok(()) } } diff --git a/crates/metassr-create/src/templates.rs b/crates/metassr-create/src/templates.rs new file mode 100644 index 0000000..b620309 --- /dev/null +++ b/crates/metassr-create/src/templates.rs @@ -0,0 +1,55 @@ +use crate::{load_templates, tags, Creator}; +use anyhow::Result; +use std::{collections::HashMap, str::from_utf8}; + +pub enum Template { + Javascript, + Typescript, +} + +impl From<&str> for Template { + fn from(value: &str) -> Self { + match value.to_lowercase().as_str() { + "javascript" | "js" => Self::Javascript, + "typescript" | "ts" => Self::Typescript, + _ => unreachable!("Template isn't detected."), + } + } +} + +impl ToString for Template { + fn to_string(&self) -> String { + match *self { + Self::Javascript => "javascript", + Self::Typescript => "typescript", + } + .to_string() + } +} + +impl Template { + pub fn load(&self, creator: &Creator) -> Result>> { + let template = load_templates(); + let template = template.get(&self.to_string()).unwrap(); + let mut template = template.clone(); + let package_json = from_utf8(template.get("package.json").unwrap())? + .replace(tags::NAME, &creator.project_name) + .replace(tags::VERSION, &creator.version) + .replace(tags::DESC, &creator.description) + .as_bytes() + .to_vec(); + template.insert("package.json".to_string(), package_json); + let ext = match *self { + Template::Javascript => "jsx", + Template::Typescript => "tsx", + }; + let head = from_utf8(template.get(&format!("src/_head.{ext}")).unwrap())? + .replace(tags::NAME, &creator.project_name) + .replace(tags::VERSION, &creator.version) + .as_bytes() + .to_vec(); + template.insert(format!("src/_head.{ext}"), head); + + Ok(template) + } +} diff --git a/crates/metassr-create/templates/javascript/build.js b/crates/metassr-create/templates/javascript/build.js deleted file mode 100644 index f4990f6..0000000 --- a/crates/metassr-create/templates/javascript/build.js +++ /dev/null @@ -1,237 +0,0 @@ -const { rspack } = require('@rspack/core'); -const path = require('path'); - -const config = { - entry: { - index: './src/pages/index.tsx', - // client: './client/index.js' - }, - output: { - path: path.resolve(__dirname, 'dist'), - filename: '[name].js' - }, - target: 'web', - output: { - filename: "[name].js", - library: { - type: "commonjs2", - }, - publicPath: "" - }, - resolve: { - extensions: ['.js', '.jsx', '.tsx', '.ts'] - }, - optimization: { - minimize: false, - }, - module: { - rules: [ - { - test: /\.(jsx|js)$/, - exclude: /node_modules/, - use: { - loader: 'builtin:swc-loader', - options: { - sourceMap: true, - jsc: { - parser: { - syntax: 'ecmascript', - jsx: true, - }, - externalHelpers: false, - preserveAllComments: false, - transform: { - react: { - runtime: "automatic", - throwIfNamespace: true, - useBuiltins: false, - }, - }, - }, - }, - - }, - type: 'javascript/auto', - }, - { - test: /\.(tsx|ts)$/, - exclude: /node_modules/, - use: { - loader: 'builtin:swc-loader', - options: { - jsc: { - parser: { - syntax: 'typescript', - tsx: true, - }, - transform: { - react: { - runtime: "automatic", - throwIfNamespace: true, - useBuiltins: false, - }, - }, - }, - }, - }, - type: 'javascript/auto', - }, - { - test: /\.(png|svg|jpg)$/, - type: 'asset/inline', - }, - ], - - }, - devServer: { - port: 1111, - onListening: function (devServer) { - if (!devServer) { - throw new Error('@rspack/dev-server is not defined'); - } - - const port = devServer.server.address().port; - console.log('Listening on port:', port); - }, - compress: true, - port: 9000, - hot: true, // Enable hot module replacement - open: true // Open the browser after the server starts - }, - watch: true -}; - -import { rspack } from '@rspack/core'; -import { RspackDevServer } from '@rspack/dev-server'; -import path from 'path'; -import fs from 'fs'; -import config from '../rspack.config'; -import { normalizeAssets, requireFromString, hash } from './util'; - -async function main() { - const compiler = rspack([ - { - ...config, - name: 'Client', - entry: { - client: './src/entry-client.jsx', - }, - mode: 'development', - devtool: 'cheap-module-source-map', - builtins: { noEmitAssets: false }, - stats: { preset: 'errors-warnings', timings: true, colors: true }, - target: 'web', - }, - { - ...config, - name: 'Server', - entry: { - server: './src/entry-server.jsx', - }, - mode: 'development', - devtool: 'cheap-module-source-map', - builtins: { noEmitAssets: false }, - stats: { preset: 'errors-warnings', timings: true, colors: true }, - target: 'node', - output: { - library: { - type: 'commonjs-module', - }, - }, - }, - ]); - const devServer = new RspackDevServer( - { - ...(config.devServer ?? {}), - devMiddleware: { serverSideRender: true }, - hot: true, - client: { overlay: { errors: true, warnings: false } }, - // Allow CodeSandbox to access dev server - allowedHosts: 'all', - setupMiddlewares(middlewares, devServer) { - if (!devServer) { - throw new Error('webpack-dev-server is not defined'); - } - - middlewares.push(async (req, res) => { - const { devMiddleware } = res.locals.webpack; - const outputFileSystem = devMiddleware.outputFileSystem; - const jsonWebpackStats = devMiddleware.stats.toJson(); - const jsonWebpackStatsClient = jsonWebpackStats.children[0]; - const jsonWebpackStatsServer = jsonWebpackStats.children[1]; - const { assetsByChunkName, outputPath } = jsonWebpackStatsClient; - - if (req.originalUrl === '/') { - let render = () => ''; - - const serverChunkPath = path.join( - jsonWebpackStatsServer.outputPath, - jsonWebpackStatsServer.assetsByChunkName.server[ - jsonWebpackStatsServer.assetsByChunkName.server.length - 1 - ], - ); - const serverChunkString = outputFileSystem - .readFileSync(serverChunkPath) - .toString(); - if (!serverChunkString) { - throw new Error('Server entry compilation result is null!'); - } - try { - // TODO: get chunk hash from rspack directly - render = - requireFromString( - serverChunkString, - `${serverChunkPath}?hash=${hash(serverChunkString)}`, - ).render || render; - } catch (e) { - throw new Error( - 'Load server entry compilation result failed', - // @ts-ignore - e?.message, - ); - } - - // Then use `assetsByChunkName` for server-side rendering - // For example, if you have only one main chunk: - res.send( - ` - - - - My App - - - - ${normalizeAssets(jsonWebpackStatsServer.assetsByChunkName.server) - .filter((path) => path.endsWith('.css')) - .map((path) => ``) - .join('\n')} - ${normalizeAssets(assetsByChunkName.client) - .filter((path) => path.endsWith('.css')) - .map((path) => ``) - .join('\n')} - ${normalizeAssets(assetsByChunkName.client) - .filter((path) => path.endsWith('.js')) - .map((path) => ``) - .join('\n')} - - -
${await render()}
- - - `.trim(), - ); - } - }); - - return middlewares; - }, - // Make sure server entry modification will trigger re-render normally - watchFiles: ['./src'], - }, - compiler, - ); - - await devServer.start(); -} - diff --git a/crates/metassr-create/templates/javascript/package.json b/crates/metassr-create/templates/javascript/package.json index a1a7db0..9b496c0 100644 --- a/crates/metassr-create/templates/javascript/package.json +++ b/crates/metassr-create/templates/javascript/package.json @@ -1,5 +1,5 @@ { - "name": "NAME", + "name": "%NAME%", "version": "%VER%", "description": "%DESC%", "scripts": { diff --git a/crates/metassr-create/templates/typescript/build.js b/crates/metassr-create/templates/typescript/build.js deleted file mode 100644 index f4990f6..0000000 --- a/crates/metassr-create/templates/typescript/build.js +++ /dev/null @@ -1,237 +0,0 @@ -const { rspack } = require('@rspack/core'); -const path = require('path'); - -const config = { - entry: { - index: './src/pages/index.tsx', - // client: './client/index.js' - }, - output: { - path: path.resolve(__dirname, 'dist'), - filename: '[name].js' - }, - target: 'web', - output: { - filename: "[name].js", - library: { - type: "commonjs2", - }, - publicPath: "" - }, - resolve: { - extensions: ['.js', '.jsx', '.tsx', '.ts'] - }, - optimization: { - minimize: false, - }, - module: { - rules: [ - { - test: /\.(jsx|js)$/, - exclude: /node_modules/, - use: { - loader: 'builtin:swc-loader', - options: { - sourceMap: true, - jsc: { - parser: { - syntax: 'ecmascript', - jsx: true, - }, - externalHelpers: false, - preserveAllComments: false, - transform: { - react: { - runtime: "automatic", - throwIfNamespace: true, - useBuiltins: false, - }, - }, - }, - }, - - }, - type: 'javascript/auto', - }, - { - test: /\.(tsx|ts)$/, - exclude: /node_modules/, - use: { - loader: 'builtin:swc-loader', - options: { - jsc: { - parser: { - syntax: 'typescript', - tsx: true, - }, - transform: { - react: { - runtime: "automatic", - throwIfNamespace: true, - useBuiltins: false, - }, - }, - }, - }, - }, - type: 'javascript/auto', - }, - { - test: /\.(png|svg|jpg)$/, - type: 'asset/inline', - }, - ], - - }, - devServer: { - port: 1111, - onListening: function (devServer) { - if (!devServer) { - throw new Error('@rspack/dev-server is not defined'); - } - - const port = devServer.server.address().port; - console.log('Listening on port:', port); - }, - compress: true, - port: 9000, - hot: true, // Enable hot module replacement - open: true // Open the browser after the server starts - }, - watch: true -}; - -import { rspack } from '@rspack/core'; -import { RspackDevServer } from '@rspack/dev-server'; -import path from 'path'; -import fs from 'fs'; -import config from '../rspack.config'; -import { normalizeAssets, requireFromString, hash } from './util'; - -async function main() { - const compiler = rspack([ - { - ...config, - name: 'Client', - entry: { - client: './src/entry-client.jsx', - }, - mode: 'development', - devtool: 'cheap-module-source-map', - builtins: { noEmitAssets: false }, - stats: { preset: 'errors-warnings', timings: true, colors: true }, - target: 'web', - }, - { - ...config, - name: 'Server', - entry: { - server: './src/entry-server.jsx', - }, - mode: 'development', - devtool: 'cheap-module-source-map', - builtins: { noEmitAssets: false }, - stats: { preset: 'errors-warnings', timings: true, colors: true }, - target: 'node', - output: { - library: { - type: 'commonjs-module', - }, - }, - }, - ]); - const devServer = new RspackDevServer( - { - ...(config.devServer ?? {}), - devMiddleware: { serverSideRender: true }, - hot: true, - client: { overlay: { errors: true, warnings: false } }, - // Allow CodeSandbox to access dev server - allowedHosts: 'all', - setupMiddlewares(middlewares, devServer) { - if (!devServer) { - throw new Error('webpack-dev-server is not defined'); - } - - middlewares.push(async (req, res) => { - const { devMiddleware } = res.locals.webpack; - const outputFileSystem = devMiddleware.outputFileSystem; - const jsonWebpackStats = devMiddleware.stats.toJson(); - const jsonWebpackStatsClient = jsonWebpackStats.children[0]; - const jsonWebpackStatsServer = jsonWebpackStats.children[1]; - const { assetsByChunkName, outputPath } = jsonWebpackStatsClient; - - if (req.originalUrl === '/') { - let render = () => ''; - - const serverChunkPath = path.join( - jsonWebpackStatsServer.outputPath, - jsonWebpackStatsServer.assetsByChunkName.server[ - jsonWebpackStatsServer.assetsByChunkName.server.length - 1 - ], - ); - const serverChunkString = outputFileSystem - .readFileSync(serverChunkPath) - .toString(); - if (!serverChunkString) { - throw new Error('Server entry compilation result is null!'); - } - try { - // TODO: get chunk hash from rspack directly - render = - requireFromString( - serverChunkString, - `${serverChunkPath}?hash=${hash(serverChunkString)}`, - ).render || render; - } catch (e) { - throw new Error( - 'Load server entry compilation result failed', - // @ts-ignore - e?.message, - ); - } - - // Then use `assetsByChunkName` for server-side rendering - // For example, if you have only one main chunk: - res.send( - ` - - - - My App - - - - ${normalizeAssets(jsonWebpackStatsServer.assetsByChunkName.server) - .filter((path) => path.endsWith('.css')) - .map((path) => ``) - .join('\n')} - ${normalizeAssets(assetsByChunkName.client) - .filter((path) => path.endsWith('.css')) - .map((path) => ``) - .join('\n')} - ${normalizeAssets(assetsByChunkName.client) - .filter((path) => path.endsWith('.js')) - .map((path) => ``) - .join('\n')} - - -
${await render()}
- - - `.trim(), - ); - } - }); - - return middlewares; - }, - // Make sure server entry modification will trigger re-render normally - watchFiles: ['./src'], - }, - compiler, - ); - - await devServer.start(); -} - diff --git a/crates/metassr-create/templates/typescript/package.json b/crates/metassr-create/templates/typescript/package.json index a1a7db0..9b496c0 100644 --- a/crates/metassr-create/templates/typescript/package.json +++ b/crates/metassr-create/templates/typescript/package.json @@ -1,5 +1,5 @@ { - "name": "NAME", + "name": "%NAME%", "version": "%VER%", "description": "%DESC%", "scripts": { diff --git a/metassr-cli/Cargo.toml b/metassr-cli/Cargo.toml index 234089a..997d6a8 100644 --- a/metassr-cli/Cargo.toml +++ b/metassr-cli/Cargo.toml @@ -19,3 +19,4 @@ tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] } logger = { path = "../crates/logger" } metassr-server = { path = "../crates/metassr-server" } metassr-build = { path = "../crates/metassr-build" } +metassr-create = { version = "0.1.0", path = "../crates/metassr-create" } diff --git a/metassr-cli/src/cli/builder.rs b/metassr-cli/src/cli/builder.rs new file mode 100644 index 0000000..7b5aa92 --- /dev/null +++ b/metassr-cli/src/cli/builder.rs @@ -0,0 +1,98 @@ +use std::{fmt::Display, str::FromStr}; + +use super::traits::Exec; +use anyhow::{anyhow, Result}; +use clap::ValueEnum; +use metacall::switch; +use metassr_build::server; + +use metassr_build::{client::ClientBuilder, server::ServerSideBuilder, traits::Build}; + +use std::{ + thread::sleep, + time::{Duration, Instant}, +}; + +use tracing::{debug, error}; + +pub struct Builder { + out_dir: String, + _type: BuildingType, +} + +impl Builder { + pub fn new(_type: BuildingType, out_dir: String) -> Self { + Self { out_dir, _type } + } +} + +impl Exec for Builder { + fn exec(&self) -> anyhow::Result<()> { + let _metacall = switch::initialize().unwrap(); + let instant = Instant::now(); + if let Err(e) = ClientBuilder::new("", &self.out_dir)?.build() { + error!( + target = "builder", + message = format!("Couldn't build for the client side: {e}"), + ); + return Err(anyhow!("Couldn't continue building process.")); + } + + // TODO: find a solution to remove this + sleep(Duration::from_secs(1)); + + if let Err(e) = ServerSideBuilder::new("", &self.out_dir, self._type.into())?.build() { + error!( + target = "builder", + message = format!("Couldn't build for the server side: {e}"), + ); + return Err(anyhow!("Couldn't continue building process.")); + } + + if (_metacall.0)() == 0 { + debug!( + target = "builder", + message = "Building is completed", + time = format!("{}ms", instant.elapsed().as_millis()) + ); + } + Ok(()) + } +} + +#[derive(Debug, ValueEnum, PartialEq, Eq, Clone, Copy)] +pub enum BuildingType { + /// Static-Site Generation. + SSG, + /// Server-Side Rendering. + SSR, +} + +impl Into for BuildingType { + fn into(self) -> server::BuildingType { + match self { + Self::SSG => server::BuildingType::StaticSiteGeneration, + Self::SSR => server::BuildingType::ServerSideRendering, + } + } +} + +impl Display for BuildingType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match *self { + Self::SSG => "ssg", + Self::SSR => "ssr", + }) + } +} + +impl FromStr for BuildingType { + type Err = String; + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "ssr" | "server-side rendering" => Ok(BuildingType::SSR), + "ssg" | "static-site generation" => Ok(BuildingType::SSG), + _ => Err("unsupported option.".to_string()), + } + } +} diff --git a/metassr-cli/src/cli/creator.rs b/metassr-cli/src/cli/creator.rs new file mode 100644 index 0000000..20ad465 --- /dev/null +++ b/metassr-cli/src/cli/creator.rs @@ -0,0 +1,72 @@ +use clap::ValueEnum; +use metassr_create; +use std::{fmt::Display, str::FromStr}; +use tracing::{error, info}; + +use super::traits::Exec; + +pub struct Creator { + project_name: String, + version: String, + description: String, + template: Template, +} + +impl Creator { + pub fn new( + project_name: String, + version: String, + description: String, + template: Template, + ) -> Self { + Self { + project_name, + version, + description, + template, + } + } +} + +impl Exec for Creator { + fn exec(&self) -> anyhow::Result<()> { + match metassr_create::Creator::new( + &self.project_name, + &self.version, + &self.description, + &self.template.to_string(), + ) + .generate() + { + Ok(_) => info!("Project has been created."), + Err(e) => error!("Couldn't create your project: {e}"), + }; + Ok(()) + } +} + +#[derive(Debug, ValueEnum, PartialEq, Eq, Clone, Copy)] +pub enum Template { + Javascript, + Typescript, +} + +impl Display for Template { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match *self { + Self::Javascript => "javascript", + Self::Typescript => "typescript", + }) + } +} + +impl FromStr for Template { + type Err = String; + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "js" | "javascript" => Ok(Self::Javascript), + "ts" | "typescript" => Ok(Self::Typescript), + _ => unreachable!("Template isn't found."), + } + } +} diff --git a/metassr-cli/src/cli.rs b/metassr-cli/src/cli/mod.rs similarity index 53% rename from metassr-cli/src/cli.rs rename to metassr-cli/src/cli/mod.rs index 22fd78d..537523e 100644 --- a/metassr-cli/src/cli.rs +++ b/metassr-cli/src/cli/mod.rs @@ -1,7 +1,14 @@ -use std::{fmt::Display, str::FromStr}; +mod builder; +mod creator; +mod runner; +pub mod traits; + +pub use builder::*; +pub use creator::*; +pub use runner::*; use clap::{command, Parser, Subcommand, ValueEnum}; -use metassr_build::server; + #[derive(Parser, Debug)] #[command( author, @@ -37,7 +44,7 @@ pub enum DebugMode { Http, } -#[derive(Debug, Subcommand)] +#[derive(Debug, Subcommand, PartialEq, Eq)] pub enum Commands { /// Building your web application. Build { @@ -55,43 +62,23 @@ pub enum Commands { #[arg(long)] serve: bool, }, -} -#[derive(Debug, ValueEnum, PartialEq, Eq, Clone)] -pub enum BuildingType { - /// Static-Site Generation. - SSG, - /// Server-Side Rendering. - SSR, -} + /// Create a new metassr project. + Create { + /// The name of project + #[arg(index = 1)] + project_name: String, -impl Into for BuildingType { - fn into(self) -> server::BuildingType { - match self { - Self::SSG => server::BuildingType::StaticSiteGeneration, - Self::SSR => server::BuildingType::ServerSideRendering, - } - } -} + /// The version of your web application + #[arg(long, short, default_value_t = String::from("1.0.0"))] + version: String, -impl Display for BuildingType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match *self { - Self::SSG => "ssg", - Self::SSR => "ssr", - }) - } -} + /// The description of your web application + #[arg(long, short, default_value_t = String::from("A web application built with MetaSSR framework."))] + description: String, -impl FromStr for BuildingType { - type Err = String; - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "ssr" => Ok(BuildingType::SSR), - "server-side rendering" => Ok(BuildingType::SSR), - "ssg" => Ok(BuildingType::SSG), - "static-site generation" => Ok(BuildingType::SSG), - _ => Err("unsupported option.".to_string()), - } - } + /// Template of your new project + #[arg(long, short, default_value_t = Template::Javascript)] + template: Template, + }, } diff --git a/metassr-cli/src/cli/runner.rs b/metassr-cli/src/cli/runner.rs new file mode 100644 index 0000000..2856d9a --- /dev/null +++ b/metassr-cli/src/cli/runner.rs @@ -0,0 +1,44 @@ +use anyhow::Result; +use metacall::switch; +use metassr_server::{RunningType, Server, ServerConfigs}; +use std::env::current_dir; +use tracing::info; + +use super::traits::AsyncExec; + +pub struct Runner { + port: u16, + is_served: bool, + allow_http_debug: bool, +} + +impl Runner { + pub fn new(port: u16, is_served: bool, allow_http_debug: bool) -> Self { + Self { + port, + is_served, + allow_http_debug, + } + } +} +impl AsyncExec for Runner { + async fn exec(&self) -> Result<()> { + let _metacall = switch::initialize().unwrap(); + let running_type = match self.is_served { + true => RunningType::SSG, + false => RunningType::SSR, + }; + + let server_configs = ServerConfigs { + port: self.port, + _enable_http_logging: self.allow_http_debug, + root_path: current_dir()?, + running_type, + }; + + info!("Running your web application on {:?} mode", running_type); + + Server::new(server_configs).run().await?; + Ok(()) + } +} diff --git a/metassr-cli/src/cli/traits.rs b/metassr-cli/src/cli/traits.rs new file mode 100644 index 0000000..05b3d76 --- /dev/null +++ b/metassr-cli/src/cli/traits.rs @@ -0,0 +1,8 @@ +use anyhow::Result; + +pub trait Exec { + fn exec(&self) -> Result<()>; +} +pub trait AsyncExec { + async fn exec(&self) -> Result<()>; +} diff --git a/metassr-cli/src/main.rs b/metassr-cli/src/main.rs index fce2312..c4b0ac6 100644 --- a/metassr-cli/src/main.rs +++ b/metassr-cli/src/main.rs @@ -1,98 +1,70 @@ mod cli; use clap::Parser; -use cli::{Args, Commands, DebugMode}; +use cli::{ + traits::{AsyncExec, Exec}, + Args, Commands, DebugMode, +}; use logger::LoggingLayer; -use anyhow::{anyhow, Result}; -use metacall::switch; - -use metassr_build::{client::ClientBuilder, server::ServerSideBuilder, traits::Build}; -use metassr_server::{RunningType, Server, ServerConfigs}; +use anyhow::Result; use std::{ - env::{current_dir, set_current_dir, set_var}, + env::{set_current_dir, set_var}, path::Path, - thread::sleep, - time::{Duration, Instant}, }; -use tracing::{debug, error, info}; use tracing_subscriber::{prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt}; #[tokio::main] async fn main() -> Result<()> { let args = Args::parse(); + let allow_metacall_debug = [Some(DebugMode::All), Some(DebugMode::Metacall)].contains(&args.debug_mode); let allow_http_debug = [Some(DebugMode::All), Some(DebugMode::Http)].contains(&args.debug_mode); - tracing_subscriber::registry() - .with(LoggingLayer { - logfile: args.log_file, - }) - .init(); - - let project_root = Path::new(&args.root); - - set_current_dir(project_root) - .map_err(|err| eprintln!("Cannot chdir: {err}")) - .unwrap(); - - if allow_metacall_debug { - set_var("METACALL_DEBUG", "1"); + if let Some(Commands::Create { .. }) = args.commands { + tracing_subscriber::fmt() + .with_target(false) + .without_time() + .compact() + .init(); + } else { + tracing_subscriber::registry() + .with(LoggingLayer { + logfile: args.log_file, + }) + .init(); + let project_root = Path::new(&args.root); + + set_current_dir(project_root) + .map_err(|err| eprintln!("Cannot chdir: {err}")) + .unwrap(); + + if allow_metacall_debug { + set_var("METACALL_DEBUG", "1"); + } } - let _metacall = switch::initialize().unwrap(); - match args.commands { Some(Commands::Build { out_dir, build_type, }) => { - let instant = Instant::now(); - if let Err(e) = ClientBuilder::new("", &out_dir)?.build() { - error!( - target = "builder", - message = format!("Couldn't build for the client side: {e}"), - ); - return Err(anyhow!("Couldn't continue building process.")); - } - - // TODO: find a solution to remove this - sleep(Duration::from_secs(1)); - - if let Err(e) = ServerSideBuilder::new("", &out_dir, build_type.into())?.build() { - error!( - target = "builder", - message = format!("Couldn't build for the server side: {e}"), - ); - return Err(anyhow!("Couldn't continue building process.")); - } - - if (_metacall.0)() == 0 { - debug!( - target = "builder", - message = "Building is completed", - time = format!("{}ms", instant.elapsed().as_millis()) - ); - } + cli::Builder::new(build_type, out_dir).exec()?; } Some(Commands::Run { port, serve }) => { - let running_type = match serve { - true => RunningType::SSG, - false => RunningType::SSR, - }; - - let server_configs = ServerConfigs { - port, - _enable_http_logging: allow_http_debug, - root_path: current_dir()?, - running_type, - }; - - info!("Running your web application on {:?} mode", running_type); - - Server::new(server_configs).run().await?; + cli::Runner::new(port, serve, allow_http_debug) + .exec() + .await?; + } + Some(Commands::Create { + project_name, + version, + description, + template, + }) => { + cli::Creator::new(project_name, version, description, template).exec()?; } _ => {} };