Compare commits
7 Commits
plugin-ref
...
aa1c2422ef
| Author | SHA1 | Date | |
|---|---|---|---|
| aa1c2422ef | |||
| 5698b504e9 | |||
| e2b2c82ff7 | |||
| 7badcc333b | |||
| bf27a344e1 | |||
| 577d61ed42 | |||
| 777b0fccc8 |
@@ -1,4 +1,5 @@
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.Commands;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using MycroForge.Core;
|
||||
|
||||
@@ -6,6 +7,8 @@ namespace MycroForge.CLI.CodeGen;
|
||||
|
||||
public class CrudRouterGenerator
|
||||
{
|
||||
#region Templates
|
||||
|
||||
private static readonly string[] Template =
|
||||
[
|
||||
"from typing import Annotated",
|
||||
@@ -79,6 +82,8 @@ public class CrudRouterGenerator
|
||||
"\t\treturn JSONResponse(status_code=500, content=str(ex))",
|
||||
];
|
||||
|
||||
#endregion
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public CrudRouterGenerator(ProjectContext context)
|
||||
@@ -86,65 +91,49 @@ public class CrudRouterGenerator
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Generate(string path, string entity)
|
||||
public async Task Generate(FullyQualifiedName fqn)
|
||||
{
|
||||
var entitySnakeCaseName = entity.Underscore().ToLower();
|
||||
var entityClassName = entity.Pascalize();
|
||||
var serviceClassName = $"{entityClassName}Service";
|
||||
var entityRoutePrefix = entity.Kebaberize().Pluralize().ToLower();
|
||||
var serviceClassName = $"{fqn.PascalizedName}Service";
|
||||
var entityRoutePrefix = fqn.PascalizedName.Kebaberize().Pluralize().ToLower();
|
||||
|
||||
var servicesFolderPath = $"{Features.Api.FeatureName}/services/{path}";
|
||||
var serviceFilePath = $"{servicesFolderPath}/{entitySnakeCaseName}_service.py";
|
||||
var serviceImportPath = serviceFilePath
|
||||
.Replace('/', '.')
|
||||
.Replace('\\', '.')
|
||||
.Replace(".py", string.Empty)
|
||||
.DeduplicateDots()
|
||||
.Trim();
|
||||
var serviceFilePath = Path.Join(
|
||||
Features.Api.FeatureName, "services", fqn.FolderPath, $"{fqn.SnakeCasedName}_service"
|
||||
);
|
||||
|
||||
var serviceImportPath = serviceFilePath.SlashesToDots();
|
||||
var routerFolderPath = Path.Join(Features.Api.FeatureName, "routers", fqn.FolderPath);
|
||||
var routerFilePath = Path.Join(routerFolderPath, $"{fqn.SnakeCasedName}");
|
||||
var routerImportPath = routerFolderPath.SlashesToDots();
|
||||
var requestsFolderPath = Path.Join(Features.Api.FeatureName, "requests", fqn.FolderPath);
|
||||
|
||||
var routersFolderPath = $"{Features.Api.FeatureName}/routers/{path}";
|
||||
var routerFilePath = $"{routersFolderPath}/{entitySnakeCaseName}.py";
|
||||
var routerImportPath = routersFolderPath
|
||||
.Replace('/', '.')
|
||||
.Replace('\\', '.')
|
||||
.Replace(".py", "")
|
||||
.DeduplicateDots()
|
||||
.Trim();
|
||||
|
||||
var requestsFolderPath = $"{Features.Api.FeatureName}/requests/{path}";
|
||||
|
||||
var createRequestImportPath = $"{requestsFolderPath}/Create{entityClassName}Request"
|
||||
.Replace('/', '.')
|
||||
.Replace('\\', '.')
|
||||
.DeduplicateDots()
|
||||
var createRequestImportPath = Path.Join(requestsFolderPath, $"Create{fqn.PascalizedName}Request")
|
||||
.SlashesToDots()
|
||||
.Underscore()
|
||||
.ToLower();
|
||||
var createRequestClassName = $"Create{entityClassName}Request";
|
||||
var createRequestClassName = $"Create{fqn.PascalizedName}Request";
|
||||
|
||||
var updateRequestImportPath = $"{requestsFolderPath}/Update{entityClassName}Request"
|
||||
.Replace('/', '.')
|
||||
.Replace('\\', '.')
|
||||
.DeduplicateDots()
|
||||
var updateRequestImportPath = Path.Join(requestsFolderPath, $"Update{fqn.PascalizedName}Request")
|
||||
.SlashesToDots()
|
||||
.Underscore()
|
||||
.ToLower();
|
||||
var updateRequestClassName = $"Update{entityClassName}Request";
|
||||
var updateRequestClassName = $"Update{fqn.PascalizedName}Request";
|
||||
|
||||
var router = string.Join("\n", Template)
|
||||
.Replace("%service_import_path%", serviceImportPath)
|
||||
.Replace("%entity_class_name%", entityClassName)
|
||||
.Replace("%entity_class_name%", fqn.PascalizedName)
|
||||
.Replace("%service_class_name%", serviceClassName)
|
||||
.Replace("%create_entity_request_import_path%", createRequestImportPath)
|
||||
.Replace("%create_entity_request_class_name%", createRequestClassName)
|
||||
.Replace("%update_entity_request_import_path%", updateRequestImportPath)
|
||||
.Replace("%update_entity_request_class_name%", updateRequestClassName);
|
||||
|
||||
await _context.CreateFile(routerFilePath, router);
|
||||
await _context.CreateFile($"{routerFilePath}.py", router);
|
||||
|
||||
var main = await _context.ReadFile("main.py");
|
||||
|
||||
main = new MainModifier(main).Initialize()
|
||||
.Import(from: routerImportPath, import: entitySnakeCaseName)
|
||||
.IncludeRouter(prefix: entityRoutePrefix, router: entitySnakeCaseName)
|
||||
.Import(from: routerImportPath, import: fqn.SnakeCasedName)
|
||||
.IncludeRouter(prefix: entityRoutePrefix, router: fqn.SnakeCasedName)
|
||||
.Rewrite();
|
||||
|
||||
await _context.WriteFile("main.py", main);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using MycroForge.CLI.Commands;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.CodeGen;
|
||||
@@ -65,26 +64,15 @@ public class CrudServiceGenerator
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Generate(string path, string entity)
|
||||
public async Task Generate(FullyQualifiedName fqn)
|
||||
{
|
||||
var entitySnakeCaseName = entity.Underscore().ToLower();
|
||||
var entityClassName = entity.Pascalize();
|
||||
var entityImportPath = fqn.GetImportPath(root: [Features.Db.FeatureName, "entities"]);
|
||||
|
||||
var entitiesFolderPath = $"{Features.Db.FeatureName}/entities/{path}";
|
||||
var entityFilePath = $"{entitiesFolderPath}/{entitySnakeCaseName}.py";
|
||||
var entityImportPath = entityFilePath
|
||||
.Replace('/', '.')
|
||||
.Replace('\\', '.')
|
||||
.Replace(".py", string.Empty)
|
||||
.DeduplicateDots()
|
||||
.Trim();
|
||||
|
||||
var servicesFolderPath = $"{Features.Api.FeatureName}/services/{path}";
|
||||
var serviceFilePath = $"{servicesFolderPath}/{entity.Underscore().ToLower()}_service.py";
|
||||
var serviceFilePath = Path.Join(Features.Api.FeatureName, "services", $"{fqn.FilePath}_service.py");
|
||||
|
||||
var service = string.Join("\n", Template)
|
||||
.Replace("%entity_import_path%", entityImportPath)
|
||||
.Replace("%entity_class_name%", entityClassName)
|
||||
.Replace("%entity_class_name%", fqn.PascalizedName)
|
||||
;
|
||||
|
||||
await _context.CreateFile(serviceFilePath, service);
|
||||
|
||||
@@ -148,7 +148,7 @@ public partial class EntityLinker
|
||||
var path = $"{Features.Db.FeatureName}/entities";
|
||||
|
||||
if (fqn.HasPath)
|
||||
path = Path.Combine(path, fqn.Path);
|
||||
path = Path.Combine(path, fqn.FolderPath);
|
||||
|
||||
path = Path.Combine(path, $"{fqn.SnakeCasedName}.py");
|
||||
var entity = new EntityModel(fqn.PascalizedName, path, await _context.ReadFile(path));
|
||||
|
||||
@@ -38,12 +38,14 @@ public class MainModifier
|
||||
|
||||
public MainModifier IncludeRouter(string prefix, string router)
|
||||
{
|
||||
_routerIncludeBuffer.Add($"\napp.include_router(prefix=\"/{prefix}\", router={router}.router)\n");
|
||||
_routerIncludeBuffer.Add($"app.include_router(prefix=\"/{prefix}\", router={router}.router)");
|
||||
return this;
|
||||
}
|
||||
|
||||
public string Rewrite()
|
||||
{
|
||||
// Make sure to insert the includes before the imports, if done the other way around,
|
||||
// the insertions of the includes will change the indexes of the imports.
|
||||
InsertIncludes();
|
||||
|
||||
InsertImports();
|
||||
@@ -54,7 +56,7 @@ public class MainModifier
|
||||
private void InsertImports()
|
||||
{
|
||||
if (_importsBuffer.Count == 0) return;
|
||||
|
||||
|
||||
if (_lastImport is not null)
|
||||
{
|
||||
_source.InsertMultiLine(_lastImport.EndIndex, _importsBuffer.ToArray());
|
||||
@@ -69,15 +71,17 @@ public class MainModifier
|
||||
{
|
||||
if (_routerIncludeBuffer.Count == 0) return;
|
||||
|
||||
// Prepend an empty string to the router include buffer,
|
||||
// this will ensure that the new entries are all on separate lines.
|
||||
var content = _routerIncludeBuffer.Prepend(string.Empty).ToArray();
|
||||
|
||||
if (_lastRouterInclude is not null)
|
||||
{
|
||||
_source.InsertMultiLine(
|
||||
_lastRouterInclude.EndIndex, _routerIncludeBuffer.ToArray()
|
||||
);
|
||||
_source.InsertMultiLine(_lastRouterInclude.EndIndex, content);
|
||||
}
|
||||
else
|
||||
{
|
||||
_source.InsertMultiLineAtEnd(_routerIncludeBuffer.ToArray());
|
||||
_source.InsertMultiLineAtEnd(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.Commands;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.CodeGen;
|
||||
@@ -40,29 +41,30 @@ public class RequestClassGenerator
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Generate(string path, string entity, Type type)
|
||||
public async Task Generate(FullyQualifiedName fqn, Type type)
|
||||
{
|
||||
var entitySnakeCaseName = entity.Underscore().ToLower();
|
||||
var entityClassName = entity.Pascalize();
|
||||
var entitiesFolderPath = $"{Features.Db.FeatureName}/entities/{path}";
|
||||
var entityFilePath = $"{entitiesFolderPath}/{entitySnakeCaseName}.py";
|
||||
var entityFilePath = Path.Join(Features.Db.FeatureName, "entities", $"{fqn.FilePath}.py");
|
||||
var entitySource = await _context.ReadFile(entityFilePath);
|
||||
|
||||
var fieldInfo = ReadFields(entitySource);
|
||||
var fields = string.Join('\n', fieldInfo.Select(x => ToFieldString(x, type)));
|
||||
|
||||
var requestsFolderPath = $"{Features.Api.FeatureName}/requests/{path}";
|
||||
var updateRequestFilePath =
|
||||
$"{requestsFolderPath}/{type.ToString().ToLower()}_{entitySnakeCaseName}_request.py";
|
||||
|
||||
var requestFilePath = Path.Join(
|
||||
Features.Api.FeatureName,
|
||||
"requests",
|
||||
fqn.FolderPath,
|
||||
// requestsFolderPath,
|
||||
$"{type.ToString().ToLower()}_{fqn.SnakeCasedName}_request.py"
|
||||
);
|
||||
|
||||
var service = string.Join("\n", Template)
|
||||
.Replace("%imports%", GetImportString(entitySource, fieldInfo, type))
|
||||
.Replace("%request_type%", type.ToString().Pascalize())
|
||||
.Replace("%entity_class_name%", entityClassName)
|
||||
.Replace("%entity_class_name%", fqn.PascalizedName)
|
||||
// .Replace("%entity_class_name%", entityClassName)
|
||||
.Replace("%fields%", fields)
|
||||
;
|
||||
|
||||
await _context.CreateFile(updateRequestFilePath, service);
|
||||
await _context.CreateFile(requestFilePath, service);
|
||||
}
|
||||
|
||||
private string ToFieldString(Field field, Type type)
|
||||
@@ -104,7 +106,7 @@ public class RequestClassGenerator
|
||||
.Replace("]", "")
|
||||
.Replace(" ", "")
|
||||
.Split();
|
||||
|
||||
|
||||
foreach (var dissectedType in dissectedTypes)
|
||||
{
|
||||
if (imports.FirstOrDefault(i => i.Match(dissectedType)) is Import import)
|
||||
|
||||
@@ -1,28 +1,48 @@
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.Extensions;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
||||
public class FullyQualifiedName
|
||||
{
|
||||
public string Path { get; }
|
||||
public string FolderPath { get; }
|
||||
public string PascalizedName { get; }
|
||||
public string SnakeCasedName { get; }
|
||||
|
||||
public bool HasPath => Path.Length > 0;
|
||||
public string FilePath =>
|
||||
string.IsNullOrEmpty(FolderPath.Trim())
|
||||
? SnakeCasedName
|
||||
: Path.Join(FolderPath, SnakeCasedName);
|
||||
|
||||
public bool HasPath => FolderPath.Length > 0;
|
||||
|
||||
|
||||
|
||||
public FullyQualifiedName(string name)
|
||||
{
|
||||
var path = string.Empty;
|
||||
|
||||
|
||||
if (name.Split(':').Select(s => s.Trim()).ToArray() is { Length: 2 } fullName)
|
||||
{
|
||||
path = fullName[0];
|
||||
name = fullName[1];
|
||||
}
|
||||
|
||||
Path = path;
|
||||
|
||||
FolderPath = path;
|
||||
PascalizedName = name.Pascalize();
|
||||
SnakeCasedName = name.Underscore().ToLower();
|
||||
SnakeCasedName = SnakeCase(name);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetImportPath(params string[] root)
|
||||
{
|
||||
if (root.Length == 0)
|
||||
return string.Join('.', FilePath).SlashesToDots();
|
||||
|
||||
var importRoot = string.Join('.', root);
|
||||
|
||||
return string.Join('.', SnakeCase(importRoot), FilePath).SlashesToDots();
|
||||
}
|
||||
|
||||
private static string SnakeCase(string value) => value.Underscore().ToLower();
|
||||
|
||||
// private static string SlashesToDots(string value) => value.Replace('\\', '.').Replace('/', '.');
|
||||
}
|
||||
@@ -21,6 +21,11 @@ public partial class MycroForge
|
||||
description: "The database UI port"
|
||||
);
|
||||
|
||||
private static readonly Option<ProjectConfig.DbConfig.DbuPlatformOptions> DbuPlatformOption = new(
|
||||
aliases: ["--database-ui-platform", "--dbu-platform"],
|
||||
description: "The docker platform for the PhpMyAdmin image"
|
||||
);
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
private readonly OptionsContainer _optionsContainer;
|
||||
private readonly List<IFeature> _features;
|
||||
@@ -34,12 +39,22 @@ public partial class MycroForge
|
||||
|
||||
AddOption(DbhPortOption);
|
||||
AddOption(DbuPortOption);
|
||||
this.SetHandler(ExecuteAsync, DbhPortOption, DbuPortOption);
|
||||
AddOption(DbuPlatformOption);
|
||||
this.SetHandler(ExecuteAsync, DbhPortOption, DbuPortOption, DbuPlatformOption);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync(int dbhPort, int dbuPort)
|
||||
private async Task ExecuteAsync(
|
||||
int dbhPort,
|
||||
int dbuPort,
|
||||
ProjectConfig.DbConfig.DbuPlatformOptions dbuPlatform
|
||||
)
|
||||
{
|
||||
_optionsContainer.Set(new Features.Db.Options { DbhPort = dbhPort, DbuPort = dbuPort });
|
||||
_optionsContainer.Set(new Features.Db.Options
|
||||
{
|
||||
DbhPort = dbhPort,
|
||||
DbuPort = dbuPort,
|
||||
DbuPlatform = dbuPlatform
|
||||
});
|
||||
var feature = _features.First(f => f.Name == Features.Db.FeatureName);
|
||||
await feature.ExecuteAsync(_context);
|
||||
}
|
||||
|
||||
@@ -29,11 +29,10 @@ public partial class MycroForge
|
||||
private async Task ExecuteAsync(string entity)
|
||||
{
|
||||
var fqn = new FullyQualifiedName(entity);
|
||||
|
||||
await new CrudServiceGenerator(_context).Generate(fqn.Path, fqn.PascalizedName);
|
||||
await new RequestClassGenerator(_context).Generate(fqn.Path, fqn.PascalizedName, RequestClassGenerator.Type.Create);
|
||||
await new RequestClassGenerator(_context).Generate(fqn.Path, fqn.PascalizedName, RequestClassGenerator.Type.Update);
|
||||
await new CrudRouterGenerator(_context).Generate(fqn.Path, fqn.PascalizedName);
|
||||
await new CrudServiceGenerator(_context).Generate(fqn);
|
||||
await new RequestClassGenerator(_context).Generate(fqn, RequestClassGenerator.Type.Create);
|
||||
await new RequestClassGenerator(_context).Generate(fqn, RequestClassGenerator.Type.Update);
|
||||
await new CrudRouterGenerator(_context).Generate(fqn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public partial class MycroForge
|
||||
_context.AssertDirectoryExists(folderPath);
|
||||
|
||||
if (fqn.HasPath)
|
||||
folderPath = Path.Combine(folderPath, fqn.Path);
|
||||
folderPath = Path.Combine(folderPath, fqn.FolderPath);
|
||||
|
||||
var fileName = $"{fqn.SnakeCasedName}.py";
|
||||
var filePath = Path.Combine(folderPath, fileName);
|
||||
|
||||
@@ -7,11 +7,11 @@ public partial class MycroForge
|
||||
{
|
||||
public partial class Api : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
public Api(IEnumerable<ISubCommandOf<Api>> subCommands) :
|
||||
public Api(IEnumerable<ISubCommandOf<Api>> commands) :
|
||||
base("api", "API related commands")
|
||||
{
|
||||
foreach (var subCommandOf in subCommands)
|
||||
AddCommand((subCommandOf as Command)!);
|
||||
foreach (var command in commands)
|
||||
AddCommand((command as Command)!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ public partial class MycroForge
|
||||
_context.AssertDirectoryExists(Features.Db.FeatureName);
|
||||
|
||||
if (fqn.HasPath)
|
||||
folderPath = Path.Combine(folderPath, fqn.Path);
|
||||
folderPath = Path.Combine(folderPath, fqn.FolderPath);
|
||||
|
||||
var _columns = GetColumnDefinitions(columns.ToArray());
|
||||
var typeImports = string.Join(", ", _columns.Select(c => c.OrmType.Split('(').First()).Distinct());
|
||||
|
||||
@@ -23,7 +23,8 @@ public partial class MycroForge
|
||||
{
|
||||
var config = await _context.LoadConfig();
|
||||
var env = $"DBH_PORT={config.Db.DbhPort} DBU_PORT={config.Db.DbuPort}";
|
||||
await _context.Bash($"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml up -d");
|
||||
var command = $"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml up -d";
|
||||
await _context.Bash(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,10 @@ public partial class MycroForge
|
||||
|
||||
private async Task ExecuteAsync()
|
||||
{
|
||||
var config = await _context.LoadConfig();
|
||||
var env = $"DB_PORT={config.Db.DbhPort} PMA_PORT={config.Db.DbuPort}";
|
||||
await _context.Bash($"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml down");
|
||||
await _context.Bash(
|
||||
// Set the log level to ERROR to prevent warnings concerning environment variables not being set.
|
||||
$"docker --log-level ERROR compose -f {Features.Db.FeatureName}.docker-compose.yml down"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public partial class MycroForge
|
||||
var folderPath = string.Empty;
|
||||
|
||||
if (fqn.HasPath)
|
||||
folderPath = Path.Combine(folderPath, fqn.Path);
|
||||
folderPath = Path.Combine(folderPath, fqn.FolderPath);
|
||||
|
||||
var filePath = Path.Combine(folderPath, $"{fqn.SnakeCasedName}.py");
|
||||
var template = withSession ? WithSessionTemplate : DefaultTemplate;
|
||||
|
||||
@@ -15,6 +15,7 @@ public partial class MycroForge
|
||||
ApiPort = ctx.ParseResult.GetValueForOption(ApiPortOption),
|
||||
DbhPort = ctx.ParseResult.GetValueForOption(DbhPortOption),
|
||||
DbuPort = ctx.ParseResult.GetValueForOption(DbuPortOption),
|
||||
DbuPlatform = ctx.ParseResult.GetValueForOption(DbuPlatformOption),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace MycroForge.CLI.Commands;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
@@ -11,6 +13,7 @@ public partial class MycroForge
|
||||
public int? ApiPort { get; set; }
|
||||
public int? DbhPort { get; set; }
|
||||
public int? DbuPort { get; set; }
|
||||
public ProjectConfig.DbConfig.DbuPlatformOptions DbuPlatform { get; set; }
|
||||
|
||||
public Features.Api.Options ApiOptions => new()
|
||||
{
|
||||
@@ -20,7 +23,8 @@ public partial class MycroForge
|
||||
public Features.Db.Options DbOptions => new()
|
||||
{
|
||||
DbhPort = DbhPort <= 0 ? 5050 : DbhPort,
|
||||
DbuPort = DbuPort <= 0 ? 5051 : DbhPort
|
||||
DbuPort = DbuPort <= 0 ? 5051 : DbhPort,
|
||||
DbuPlatform = DbuPlatform
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,11 @@ public partial class MycroForge
|
||||
description: "The database UI port"
|
||||
);
|
||||
|
||||
private static readonly Option<ProjectConfig.DbConfig.DbuPlatformOptions> DbuPlatformOption = new(
|
||||
aliases: ["--database-ui-platform", "--dbu-platform"],
|
||||
description: "The docker platform for the PhpMyAdmin image"
|
||||
);
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
private readonly List<IFeature> _features;
|
||||
private readonly OptionsContainer _optionsContainer;
|
||||
@@ -55,6 +60,7 @@ public partial class MycroForge
|
||||
AddOption(ApiPortOption);
|
||||
AddOption(DbhPortOption);
|
||||
AddOption(DbuPortOption);
|
||||
AddOption(DbuPlatformOption);
|
||||
|
||||
this.SetHandler(ExecuteAsync, new Binder());
|
||||
}
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static string DeduplicateDots(this string path)
|
||||
{
|
||||
while (path.Contains(".."))
|
||||
path = path.Replace("..", ".");
|
||||
|
||||
return path.Trim('.');
|
||||
}
|
||||
public static string SlashesToDots(this string path) =>
|
||||
path.Replace('/', '.')
|
||||
.Replace('\\', '.')
|
||||
.Trim();
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
namespace MycroForge.CLI.Features;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.Features;
|
||||
|
||||
public sealed partial class Db
|
||||
{
|
||||
public class Options
|
||||
{
|
||||
public int? DbhPort { get; set; }
|
||||
|
||||
public int? DbuPort { get; set; }
|
||||
|
||||
public ProjectConfig.DbConfig.DbuPlatformOptions DbuPlatform { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using MycroForge.CLI.CodeGen;
|
||||
using MycroForge.CLI.Commands;
|
||||
using MycroForge.Core;
|
||||
using MycroForge.Core.Extensions;
|
||||
|
||||
namespace MycroForge.CLI.Features;
|
||||
|
||||
@@ -37,7 +38,6 @@ public sealed partial class Db : IFeature
|
||||
|
||||
private static readonly string[] DockerCompose =
|
||||
[
|
||||
"version: '3.8'",
|
||||
"# Access the database UI at http://localhost:${DBU_PORT}.",
|
||||
"# Login: username = root & password = password",
|
||||
"",
|
||||
@@ -59,6 +59,7 @@ public sealed partial class Db : IFeature
|
||||
"",
|
||||
" %app_name%_phpmyadmin:",
|
||||
" image: phpmyadmin/phpmyadmin",
|
||||
" platform: %dbu_platform%",
|
||||
" container_name: %app_name%_phpmyadmin",
|
||||
" ports:",
|
||||
" - '${DBU_PORT}:80'",
|
||||
@@ -87,15 +88,16 @@ public sealed partial class Db : IFeature
|
||||
{
|
||||
_optionsContainer = optionsContainer;
|
||||
}
|
||||
|
||||
|
||||
public async Task ExecuteAsync(ProjectContext context)
|
||||
{
|
||||
var options = _optionsContainer.Get<Options>();
|
||||
var config = await context.LoadConfig(create: true);
|
||||
config.Db = new()
|
||||
{
|
||||
DbhPort = options.DbhPort ?? 5050,
|
||||
DbuPort = options.DbuPort ?? 5051
|
||||
DbhPort = options.DbhPort ?? 5050,
|
||||
DbuPort = options.DbuPort ?? 5051,
|
||||
DbuPlatform = options.DbuPlatform
|
||||
};
|
||||
await context.SaveConfig(config);
|
||||
|
||||
@@ -123,7 +125,10 @@ public sealed partial class Db : IFeature
|
||||
|
||||
await context.CreateFile($"{FeatureName}/entities/entity_base.py", EntityBase);
|
||||
|
||||
var dockerCompose = string.Join('\n', DockerCompose).Replace("%app_name%", appName);
|
||||
var dockerCompose = string.Join('\n', DockerCompose)
|
||||
.Replace("%app_name%", appName)
|
||||
.Replace("%dbu_platform%", options.DbuPlatform.ToDockerPlatformString())
|
||||
;
|
||||
|
||||
await context.CreateFile($"{FeatureName}.docker-compose.yml", dockerCompose);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/bash
|
||||
#!/usr/bin/bash
|
||||
|
||||
dotnet pack -v d
|
||||
|
||||
|
||||
6
MycroForge.Core/Attributes/DockerPlatformAttribute.cs
Normal file
6
MycroForge.Core/Attributes/DockerPlatformAttribute.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace MycroForge.Core.Attributes;
|
||||
|
||||
public class DockerPlatformAttribute : Attribute
|
||||
{
|
||||
public string Platform { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public class Source
|
||||
|
||||
public Source InsertMultiLineAtEnd(params string[] text)
|
||||
{
|
||||
_text += (string.Join('\n', text));
|
||||
_text += string.Join('\n', text);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
19
MycroForge.Core/Extensions/EnumExtensions.cs
Normal file
19
MycroForge.Core/Extensions/EnumExtensions.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Reflection;
|
||||
using MycroForge.Core.Attributes;
|
||||
|
||||
namespace MycroForge.Core.Extensions;
|
||||
|
||||
public static class EnumExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A generic extension method that aids in reflecting
|
||||
/// and retrieving any attribute that is applied to an `Enum`.
|
||||
/// </summary>
|
||||
public static string ToDockerPlatformString(this ProjectConfig.DbConfig.DbuPlatformOptions value)
|
||||
{
|
||||
return value.GetType()
|
||||
.GetMember(value.ToString())
|
||||
.FirstOrDefault()!
|
||||
.GetCustomAttribute<DockerPlatformAttribute>()!.Platform;
|
||||
}
|
||||
}
|
||||
27
MycroForge.Core/ProjectConfig.DbConfig.DbuPlatformOptions.cs
Normal file
27
MycroForge.Core/ProjectConfig.DbConfig.DbuPlatformOptions.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using MycroForge.Core.Attributes;
|
||||
|
||||
namespace MycroForge.Core;
|
||||
|
||||
public partial class ProjectConfig
|
||||
{
|
||||
public partial class DbConfig
|
||||
{
|
||||
public enum DbuPlatformOptions
|
||||
{
|
||||
[DockerPlatform(Platform = "linux/amd64")]
|
||||
linux_amd64,
|
||||
|
||||
[DockerPlatform(Platform = "linux/arm32/v5")]
|
||||
linux_arm32v5,
|
||||
|
||||
[DockerPlatform(Platform = "linux/arm32/v6")]
|
||||
linux_arm32v6,
|
||||
|
||||
[DockerPlatform(Platform = "linux/arm32/v7")]
|
||||
linux_arm32v7,
|
||||
|
||||
[DockerPlatform(Platform = "linux/arm64/v8")]
|
||||
linux_arm64v8
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
namespace MycroForge.Core;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MycroForge.Core;
|
||||
|
||||
public partial class ProjectConfig
|
||||
{
|
||||
public class DbConfig
|
||||
public partial class DbConfig
|
||||
{
|
||||
public int DbhPort { get; set; }
|
||||
|
||||
public int DbuPort { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public DbuPlatformOptions DbuPlatform { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ https://learn.microsoft.com/en-us/dotnet/core/tutorials/cli-templates-create-tem
|
||||
### Build the package
|
||||
`dotnet pack`
|
||||
|
||||
### Push to local nuget
|
||||
### Push to devdisciples nuget
|
||||
`dotnet nuget push bin/Release/MycroForge.PluginTemplate.Package.1.0.0.nupkg --source devdisciples`
|
||||
|
||||
### Install template package from local nuget
|
||||
0
MycroForge.PluginTemplate/readme.md
Normal file
0
MycroForge.PluginTemplate/readme.md
Normal file
11
README.md
11
README.md
@@ -26,4 +26,13 @@ Run the install script in the same directory as the downloaded zip. See the exam
|
||||
|
||||
```bash
|
||||
dotnet nuget add source --name devdisciples --username username --password password https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json --store-password-in-clear-text
|
||||
```
|
||||
```
|
||||
|
||||
### TODO
|
||||
|
||||
- Fix `-c` option for `m4g db generate entity`
|
||||
- Mention `--dbu-platform` option for `m4g init`
|
||||
- Research if System.CommandLine middleware can be used to safeguard commands like `m4g add` or `m4g api`.
|
||||
- Fix up exception handling
|
||||
- Clean up README files
|
||||
-
|
||||
3
docs/.dockerignore
Normal file
3
docs/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
.docusaurus/
|
||||
node_modules/
|
||||
.k8s/
|
||||
3
docs/.gitignore
vendored
3
docs/.gitignore
vendored
@@ -18,3 +18,6 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.k8s/remote*.yml
|
||||
!.k8s/remote.example.yml
|
||||
61
docs/.k8s/local.yml
Normal file
61
docs/.k8s/local.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
# =========================================
|
||||
# App manifest
|
||||
# =========================================
|
||||
|
||||
---
|
||||
# App Deployment
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: m4g-docs-deployment
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: m4g-docs
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: m4g-docs
|
||||
spec:
|
||||
containers:
|
||||
- name: m4g-docs
|
||||
image: m4gdocs:latest
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
---
|
||||
# App Service
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: m4g-docs-service
|
||||
spec:
|
||||
selector:
|
||||
app: m4g-docs
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
type: NodePort
|
||||
|
||||
---
|
||||
# App Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: m4g-docs-ingress
|
||||
spec:
|
||||
ingressClassName: caddy
|
||||
rules:
|
||||
- host: m4g.docs.local
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: m4g-docs-service
|
||||
port:
|
||||
number: 80
|
||||
67
docs/.k8s/remote.example.yml
Normal file
67
docs/.k8s/remote.example.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
# =========================================
|
||||
# App manifest
|
||||
# =========================================
|
||||
|
||||
---
|
||||
# App Deployment
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: m4g-docs-deployment
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: m4g-docs
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: m4g-docs
|
||||
spec:
|
||||
containers:
|
||||
- name: m4g-docs
|
||||
image: git.devdisciples.com/devdisciples/m4gdocs:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
---
|
||||
# App Service
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: m4g-docs-service
|
||||
spec:
|
||||
selector:
|
||||
app: m4g-docs
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
type: NodePort
|
||||
|
||||
---
|
||||
# App Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: m4g-docs-ingress
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: lets-encrypt
|
||||
spec:
|
||||
ingressClassName: public
|
||||
tls:
|
||||
- hosts:
|
||||
- m4g.example.com
|
||||
secretName: example-tls-secret
|
||||
rules:
|
||||
- host: m4g.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: m4g-docs-service
|
||||
port:
|
||||
number: 80
|
||||
27
docs/Dockerfile
Normal file
27
docs/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Base image.
|
||||
## Start with a base image containing NodeJS so we can build Docusaurus.
|
||||
FROM node:lts AS base
|
||||
## Disable colour output from yarn to make logs easier to read.
|
||||
ENV FORCE_COLOR=0
|
||||
## Enable corepack.
|
||||
RUN corepack enable
|
||||
## Set the working directory to `/opt/docusaurus`.
|
||||
WORKDIR /opt/docusaurus
|
||||
|
||||
# Stage 2b: Production build mode.
|
||||
FROM base AS prod
|
||||
## Set the working directory to `/opt/docusaurus`.
|
||||
WORKDIR /opt/docusaurus
|
||||
## Copy over the source code.
|
||||
COPY . /opt/docusaurus/
|
||||
## Install dependencies with `--immutable` to ensure reproducibility.
|
||||
RUN npm ci
|
||||
## Build the static site.
|
||||
RUN npm run build
|
||||
|
||||
## Use a stable nginx image
|
||||
FROM nginx:stable-alpine AS deploy
|
||||
WORKDIR /home/node/app
|
||||
COPY --chown=node:node --from=prod /opt/docusaurus/build/ /usr/share/nginx/html/
|
||||
@@ -39,3 +39,9 @@ $ GIT_USER=<Your GitHub username> yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||
|
||||
|
||||
### Custom
|
||||
|
||||
kubectl --kubeconfig ~/.kube/main.k8s.config apply -f .k8s/remote.yml
|
||||
|
||||
|
||||
25
docs/docs.sln
Normal file
25
docs/docs.sln
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.002.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTest.Plugin", "MyTest.Plugin\MyTest.Plugin.csproj", "{C93CD889-7228-4DA2-B0E2-5273F2FAAFE6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{C93CD889-7228-4DA2-B0E2-5273F2FAAFE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C93CD889-7228-4DA2-B0E2-5273F2FAAFE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C93CD889-7228-4DA2-B0E2-5273F2FAAFE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C93CD889-7228-4DA2-B0E2-5273F2FAAFE6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {241D0F32-CE9B-40CA-BEA2-A2554CA22824}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Commands
|
||||
|
||||
```
|
||||
|
||||
@@ -12,7 +12,8 @@ Usage:
|
||||
m4g add db [options]
|
||||
|
||||
Options:
|
||||
--database-host-port, --dbh-port <database-host-port> The database host port
|
||||
--database-ui-port, --dbu-port <database-ui-port> The database UI port
|
||||
-?, -h, --help Show help and usage information
|
||||
--database-host-port, --dbh-port <database-host-port> The database host port
|
||||
--database-ui-port, --dbu-port <database-ui-port> The database UI port
|
||||
--database-ui-platform, --dbu-platform <linux_amd64|linux_arm32v5|linux_arm32v6|linux_arm32v7|linux_arm64v8> The docker platform for the PhpMyAdmin image
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -15,9 +15,10 @@ Arguments:
|
||||
<name> The name of your project
|
||||
|
||||
Options:
|
||||
--without <api|db|git|gitignore> Features to exclude
|
||||
--api-port <api-port> The API port
|
||||
--database-host-port, --dbh-port <database-host-port> The database host port
|
||||
--database-ui-port, --dbu-port <database-ui-port> The database UI port
|
||||
-?, -h, --help Show help and usage information
|
||||
--without <api|db|git|gitignore> Features to exclude
|
||||
--api-port <api-port> The API port
|
||||
--database-host-port, --dbh-port <database-host-port> The database host port
|
||||
--database-ui-port, --dbu-port <database-ui-port> The database UI port
|
||||
--database-ui-platform, --dbu-platform <linux_amd64|linux_arm32v5|linux_arm32v6|linux_arm32v7|linux_arm64v8> The docker platform for the PhpMyAdmin image
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
|
||||
174
docs/docs/command_plugins.md
Normal file
174
docs/docs/command_plugins.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Command plugins
|
||||
|
||||
MycroForge has a plugin system that allows you to extend the CLI with your own commands.
|
||||
This section will guide you through the process of creating your own extension to the `m4g` command.
|
||||
MycroForge is written in C# sharp and this is the same for plugins, so decent knowledge about `C#` & `.NET` is required.
|
||||
In this tutorial we will create a command plugin that extens the `m4g` command with a `dotenv` sub command.
|
||||
What this command will do is generate a `.env` file in the current directory and print a message to the console.
|
||||
|
||||
## Setup
|
||||
|
||||
To start creating command plugins for MycroFroge, make sure you've added the devdisciples package repository.
|
||||
This can be done by running the following command.
|
||||
|
||||
```
|
||||
dotnet nuget add source --name devdisciples https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json
|
||||
```
|
||||
|
||||
Run the following command to add the `MycroForge.PluginTemplate.Package`.
|
||||
|
||||
```
|
||||
dotnet add package --source devdisciples --version 1.0.0 MycroForge.PluginTemplate.Package
|
||||
```
|
||||
|
||||
## Initialize a plugin package
|
||||
|
||||
Generate a template plugin project by running the following command.
|
||||
|
||||
```
|
||||
m4g plugin init My.Dotenv.Plugin
|
||||
```
|
||||
|
||||
This should generate the following folder structure.
|
||||
|
||||
```
|
||||
My.Dotenv.Plugin
|
||||
┣ 📜HelloWorldCommand.cs
|
||||
┣ 📜HelloWorldCommandPlugin.cs
|
||||
┗ 📜My.Dotenv.Plugin.csproj
|
||||
```
|
||||
|
||||
Rename the following files. Also rename the classes in these files, the easiest way in `vscode` is the right click the class name and select the `Rename symbol` action. Note that this action does not (necessarily) rename the files!
|
||||
|
||||
```
|
||||
HelloWorldCommand.cs => DotenvCommand.cs
|
||||
HelloWorldCommandPlugin.cs => DotenvCommandPlugin.cs
|
||||
```
|
||||
|
||||
Modify `Name` property in `DotenvCommandPlugin.cs`.
|
||||
|
||||
```cs
|
||||
// Before
|
||||
public class DotenvCommandPlugin : ICommandPlugin
|
||||
{
|
||||
public string Name => "My.Plugin";
|
||||
|
||||
public void RegisterServices(IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ISubCommandOf<RootCommand>, HelloWorldCommand>();
|
||||
}
|
||||
}
|
||||
|
||||
// After
|
||||
public class DotenvCommandPlugin : ICommandPlugin
|
||||
{
|
||||
public string Name => "My.Dotenv.Plugin";
|
||||
|
||||
public void RegisterServices(IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ISubCommandOf<RootCommand>, HelloWorldCommand>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Modify `DotenvCommand.cs`.
|
||||
|
||||
```cs
|
||||
// Before
|
||||
public class DotenvCommand : Command, ISubCommandOf<RootCommand>
|
||||
{
|
||||
private readonly Argument<string> NameArgument =
|
||||
new(name: "name", description: "The name of the person to greet");
|
||||
|
||||
private readonly Option<bool> AllCapsOption =
|
||||
new(aliases: ["-a", "--all-caps"], description: "Print the name in all caps");
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public DotenvCommand(ProjectContext context) :
|
||||
base("hello", "An example command generated by dotnet new using the m4gp template")
|
||||
{
|
||||
_context = context;
|
||||
AddArgument(NameArgument);
|
||||
AddOption(AllCapsOption);
|
||||
this.SetHandler(ExecuteAsync, NameArgument, AllCapsOption);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync(string name, bool allCaps)
|
||||
{
|
||||
name = allCaps ? name.ToUpper() : name;
|
||||
|
||||
await _context.CreateFile("hello_world.txt",
|
||||
$"Hello {name}!",
|
||||
"This file was generated by your custom command!"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// After
|
||||
public class DotenvCommand : Command, ISubCommandOf<RootCommand>
|
||||
{
|
||||
private readonly Argument<string> VarsArgument =
|
||||
new(name: "vars", description: "Env vars to include in the .env file separated by ';'");
|
||||
|
||||
private readonly Option<bool> PrintOption =
|
||||
new(aliases: ["-o", "--overwrite"], description: "Overwrite the .env file if it exists");
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public DotenvCommand(ProjectContext context) :
|
||||
// dotenv = the name of the sub command that will be added to the m4g command
|
||||
base("dotenv", "Generate a .env file in the current directory")
|
||||
{
|
||||
_context = context;
|
||||
AddArgument(VarsArgument);
|
||||
AddOption(PrintOption);
|
||||
this.SetHandler(ExecuteAsync, VarsArgument, PrintOption);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync(string vars, bool overwrite)
|
||||
{
|
||||
var path = Path.Join(Environment.CurrentDirectory, ".env");
|
||||
var exists = File.Exists(path);
|
||||
|
||||
if (exists && !overwrite)
|
||||
{
|
||||
Console.WriteLine($"File {path} already exists, add the -o or --overwrite flag to overwrite it.");
|
||||
return;
|
||||
}
|
||||
|
||||
var content = string.Join(Environment.NewLine, vars.Split(';'));
|
||||
await _context.CreateFile(".env", content);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Install the plugin
|
||||
|
||||
Open a terminal an make sure you're in the root directory of the plugin, i.e. the `My.Dotenv.Plugin` folder.
|
||||
Run the following command to install the plugin.
|
||||
|
||||
```
|
||||
m4g plugin install --platform <platform=linux_arm|linux_arm64|linux_x64|osx_arm64|osx_x64>
|
||||
```
|
||||
|
||||
Make sure to choose the right platform option for your machine.
|
||||
If everything went well then running `m4g` should now also show a `dotenv` command.
|
||||
|
||||
## Test the plugin
|
||||
|
||||
Try running `m4g dotenv "FIRSTNAME=JOHN;LASTNAME=JOE"`, this should generate a `.env` in the current directory with the vars you specified.
|
||||
|
||||
## Uninstall the plugin
|
||||
|
||||
Uninstall the plugin by running `m4g plugin install My.Dotenv.Plugin`.
|
||||
|
||||
## Resources
|
||||
|
||||
For examples of how the core commands are implemented, you can take a look at the commands in the [MycroForge.CLI.Commands](https://git.devdisciples.com/devdisciples/mycroforge/src/branch/main/MycroForge.CLI/Commands) namespace.
|
||||
|
||||
The MycroForge.CLI project uses [SystemCommand.Line](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial) for the CLI support, check out the Microsoft documentation for more info.
|
||||
68
docs/docs/getting_started.md
Normal file
68
docs/docs/getting_started.md
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Getting Started
|
||||
|
||||
## Requirements
|
||||
|
||||
To use MycroForge, ensure you have the following dependencies installed:
|
||||
|
||||
- **bash**
|
||||
- **git**
|
||||
- **Python 3.10**
|
||||
- **Docker**
|
||||
- **.NET 8**
|
||||
- **XCode Command Line Tools (MacOS only)**
|
||||
|
||||
### Adding the Package Registry
|
||||
|
||||
Before installing MycroForge, add the package registry by running the following command:
|
||||
|
||||
```
|
||||
dotnet nuget add source --name devdisciples https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json
|
||||
```
|
||||
|
||||
### Install
|
||||
|
||||
```
|
||||
dotnet tool install -g MycroForge.CLI
|
||||
```
|
||||
|
||||
### Uninstall
|
||||
|
||||
```
|
||||
dotnet tool install -g MycroForge.CLI
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
MycroForge is designed to run in a POSIX compliant environment. It has been tested on Windows using WSL2 with Ubuntu 22.04.03. So it is recommended to run MycroForge within the same or a similar WSL2 distribution for optimal performance.
|
||||
|
||||
### MacOS
|
||||
|
||||
#### Post install steps
|
||||
After installing MycroForge, the dotnet CLI will show a message with some instructions to make the `m4g` command available in `zsh`.
|
||||
It should look similar to the example below.
|
||||
|
||||
```sh
|
||||
Tools directory '/Users/username/.dotnet/tools' is not currently on the PATH environment variable.
|
||||
If you are using zsh, you can add it to your profile by running the following command:
|
||||
|
||||
cat << \EOF >> ~/.zprofile
|
||||
# Add .NET Core SDK tools
|
||||
export PATH="$PATH:/Users/username/.dotnet/tools"
|
||||
EOF
|
||||
|
||||
And run zsh -l to make it available for current session.
|
||||
|
||||
You can only add it to the current session by running the following command:
|
||||
|
||||
export PATH="$PATH:/Users/username/.dotnet/tools"
|
||||
```
|
||||
|
||||
#### Known issues
|
||||
|
||||
##### FastAPI swagger blank screen
|
||||
|
||||
If you see a blank screen when opening the FastAPI Swagger documentation, then make sure you've activated the Safari developer tools.
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Install
|
||||
|
||||
## Requirements
|
||||
|
||||
MycroForge has the following dependencies.
|
||||
|
||||
- bash
|
||||
- git
|
||||
- Python3 (3.10)
|
||||
- Docker
|
||||
- .NET 8
|
||||
|
||||
### Windows
|
||||
|
||||
To simplify the implementation of this tool, it assumes that it's running in a POSIX compliant environment.
|
||||
MycroForge has been developed and tested on Windows in WSL2 Ubuntu 22.04.03.
|
||||
So when running on Windows, it's recommended to run MycroForge in the same environment or atleast in a similar WSL2 distro.
|
||||
@@ -2,144 +2,15 @@
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
|
||||
# Intro
|
||||
|
||||
## What is MycroForge?
|
||||
Welcome to **MycroForge** – an opinionated CLI tool designed to streamline the development of FastAPI and SQLAlchemy-based backends. With MycroForge, you can effortlessly create backend projects through a convenient command line interface.
|
||||
|
||||
MycroForge is an opinionated CLI tool that is meant to facilitate the development of FastAPI & SQLAlchemy based backends.
|
||||
It provides a command line interface that allows users to generate a skeleton project and other common items like
|
||||
database entities, migrations, routers and even basic CRUD functionality. The main purpose of this tool is to generate
|
||||
boilerplate code and to provide a unified interface for performing recurrent development activities.
|
||||
## Key Features
|
||||
|
||||
|
||||
## Generating a project
|
||||
|
||||
To generate a project you can run the following command.
|
||||
|
||||
`m4g init <name>`
|
||||
|
||||
```
|
||||
Description:
|
||||
Initialize a new project
|
||||
|
||||
Usage:
|
||||
m4g init <name> [options]
|
||||
|
||||
Arguments:
|
||||
<name> The name of your project
|
||||
|
||||
Options:
|
||||
--without <api|db|git> Features to exclude
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
```
|
||||
|
||||
Running this command will generate the following project structure.
|
||||
|
||||
```
|
||||
📦<project_name>
|
||||
┣ 📂.git
|
||||
┣ 📂.venv
|
||||
┣ 📂api
|
||||
┃ ┗ 📂routers
|
||||
┃ ┃ ┗ 📜hello.py
|
||||
┣ 📂db
|
||||
┃ ┣ 📂engine
|
||||
┃ ┃ ┗ 📜async_session.py
|
||||
┃ ┣ 📂entities
|
||||
┃ ┃ ┗ 📜entity_base.py
|
||||
┃ ┣ 📂versions
|
||||
┃ ┣ 📜README
|
||||
┃ ┣ 📜env.py
|
||||
┃ ┣ 📜script.py.mako
|
||||
┃ ┗ 📜settings.py
|
||||
┣ 📜.gitignore
|
||||
┣ 📜alembic.ini
|
||||
┣ 📜db.docker-compose.yml
|
||||
┣ 📜m4g.json
|
||||
┣ 📜main.py
|
||||
┗ 📜requirements.txt
|
||||
```
|
||||
|
||||
Let's go through these one by one.
|
||||
|
||||
### .git
|
||||
|
||||
The `m4g init` command will initialize new projects with git by default.
|
||||
If you don't want to use git you can pass the option `--without git` to `m4g init`.
|
||||
|
||||
### .venv
|
||||
|
||||
To promote isolation of Python dependencies, new projects are initialized with a virtual environment by default.
|
||||
TODO: This is a good section to introduce the `m4g hydrate` command.
|
||||
|
||||
### api/routers/hello.py
|
||||
|
||||
This file defines a basic example router, which is imported and mapped in `main.py`. This router is just an example and
|
||||
can be removed or modified at you discretion.
|
||||
|
||||
### db/engine/async_session.py
|
||||
|
||||
This file defines the `async_session` function, which can be used to open an asynchronous session to a database.
|
||||
|
||||
### db/entities/entity_base.py
|
||||
|
||||
This file contains an automatically generated entity base class that derives from the DeclarativeBase.
|
||||
All entities must inherit from this class, so that SQLAlchemy & alembic can track them. The entities directory is also
|
||||
where all newly generated entities will be stored.
|
||||
|
||||
### db/versions
|
||||
|
||||
This is where the generated database migrations will be stored.
|
||||
|
||||
### db/README
|
||||
|
||||
This README file is automatically generated by the alembic init command.
|
||||
|
||||
### db/env.py
|
||||
|
||||
This is the database environment file that is used by alembic to interact with the database.
|
||||
If you take a closer look at the imports, you'll see that the file has been modified to assign `EntityBase.metadata` to
|
||||
a variable called `target_metadata`, this will allow alembic to track changes in your entities. You'll also find that
|
||||
the `DbSettings` class is used to get the connectionstring. Any time you generate a new database entity, or create a
|
||||
many-to-many relation between two entities, this file will also be modified to include the generated classes.
|
||||
|
||||
### db/script.py.mako
|
||||
|
||||
This file is automatically generated by the alembic init command.
|
||||
|
||||
### db/settings.py
|
||||
|
||||
This file defines the `DbSettings` class, that is responsible for retrieving the database connectionstring.
|
||||
You will probably want to modify this class to retrieve the connectionstring from a secret manager at some point.
|
||||
|
||||
### .gitignore
|
||||
|
||||
The default .gitignore file that is generated by the `m4g init` command. Modify this file at your discretion.
|
||||
|
||||
### alembic.ini
|
||||
|
||||
This file is automatically generated by the alembic init command.
|
||||
|
||||
### db.docker-compose.yml
|
||||
|
||||
A docker compose file for running a database locally.
|
||||
|
||||
### m4g.json
|
||||
|
||||
This file contains some configs that are used by the CLI, for example the ports to map to the API and database.
|
||||
|
||||
### main.py
|
||||
|
||||
The entrypoint for the application. When generating entities, many-to-many relations or routers, this file will be
|
||||
modified to include the generated files.
|
||||
|
||||
### requirements.txt
|
||||
|
||||
The requirements file containing the Python dependencies.
|
||||
TODO: introduce the `m4g install` & `m4g uninstall` commands.
|
||||
|
||||
|
||||
## Plugin system
|
||||
|
||||
TODO: Dedicate a section to the Plugin system
|
||||
- **Project Skeleton Generation:** Quickly generate a well-structured project skeleton tailored for FastAPI and SQLAlchemy, ensuring you start with best practices.
|
||||
- **Database Entities:** Easily create and manage database entities, simplifying your database interactions.
|
||||
- **Migrations:** Handle database migrations seamlessly, allowing for smooth transitions and updates.
|
||||
- **Routers:** Generate and manage routers to keep your application modular and organized.
|
||||
- **CRUD Functionality:** Automatically generate basic CRUD (Create, Read, Update, Delete) operations to accelerate your development process.
|
||||
|
||||
111
docs/docs/project_layout.md
Normal file
111
docs/docs/project_layout.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Project layout
|
||||
|
||||
When you generate a new project with `m4g init <project_name>`, it will create a folder like the example below.
|
||||
|
||||
```
|
||||
📦<project_name>
|
||||
┣ 📂.git
|
||||
┣ 📂.venv
|
||||
┣ 📂api
|
||||
┃ ┗ 📂routers
|
||||
┃ ┃ ┗ 📜hello.py
|
||||
┣ 📂db
|
||||
┃ ┣ 📂engine
|
||||
┃ ┃ ┗ 📜async_session.py
|
||||
┃ ┣ 📂entities
|
||||
┃ ┃ ┗ 📜entity_base.py
|
||||
┃ ┣ 📂versions
|
||||
┃ ┣ 📜README
|
||||
┃ ┣ 📜env.py
|
||||
┃ ┣ 📜script.py.mako
|
||||
┃ ┗ 📜settings.py
|
||||
┣ 📜.gitignore
|
||||
┣ 📜alembic.ini
|
||||
┣ 📜db.docker-compose.yml
|
||||
┣ 📜m4g.json
|
||||
┣ 📜main.py
|
||||
┗ 📜requirements.txt
|
||||
```
|
||||
|
||||
Let's go through these one by one.
|
||||
|
||||
### .git
|
||||
|
||||
The `m4g init` command will initialize new projects with git by default.
|
||||
If you don't want to use git you can pass the option `--without git` to `m4g init`.
|
||||
|
||||
### .venv
|
||||
|
||||
To promote isolation of Python dependencies, new projects are initialized with a virtual environment by default.
|
||||
When you clone a MycroForge repository from git, it won't have a `.venv` folder yet.
|
||||
You can run `m4g hydrate` in the root folder of the project to restore the dependencies.
|
||||
|
||||
### api/routers/hello.py
|
||||
|
||||
This file defines a basic example router, which is imported and mapped in `main.py`. This router is just an example and
|
||||
can be removed or modified at you discretion.
|
||||
|
||||
### db/engine/async_session.py
|
||||
|
||||
This file defines the `async_session` function, which can be used to open an asynchronous session to a database.
|
||||
|
||||
### db/entities/entity_base.py
|
||||
|
||||
This file contains an automatically generated entity base class that derives from the DeclarativeBase.
|
||||
All entities must inherit from this class, so that SQLAlchemy & alembic can track them. The entities directory is also
|
||||
where all newly generated entities will be stored.
|
||||
|
||||
### db/versions
|
||||
|
||||
This is where the generated database migrations will be stored.
|
||||
|
||||
### db/README
|
||||
|
||||
This README file is automatically generated by the alembic init command.
|
||||
|
||||
### db/env.py
|
||||
|
||||
This is the database environment file that is used by alembic to interact with the database.
|
||||
If you take a closer look at the imports, you'll see that the file has been modified to assign `EntityBase.metadata` to
|
||||
a variable called `target_metadata`, this will allow alembic to track changes in your entities. You'll also find that
|
||||
the `DbSettings` class is used to get the connectionstring. Any time you generate a new database entity, or create a
|
||||
many-to-many relation between two entities, this file will also be modified to include the generated classes.
|
||||
|
||||
### db/script.py.mako
|
||||
|
||||
This file is automatically generated by the alembic init command.
|
||||
|
||||
### db/settings.py
|
||||
|
||||
This file defines the `DbSettings` class, that is responsible for retrieving the database connectionstring.
|
||||
You will probably want to modify this class to retrieve the connectionstring from a secret manager at some point.
|
||||
|
||||
### .gitignore
|
||||
|
||||
The default .gitignore file that is generated by the `m4g init` command. Modify this file at your discretion.
|
||||
|
||||
### alembic.ini
|
||||
|
||||
This file is automatically generated by the alembic init command.
|
||||
|
||||
### db.docker-compose.yml
|
||||
|
||||
A docker compose file for running a database locally.
|
||||
|
||||
### m4g.json
|
||||
|
||||
This file contains some configs that are used by the CLI, for example the ports to map to the API and database.
|
||||
|
||||
### main.py
|
||||
|
||||
The entrypoint for the application. When generating entities, many-to-many relations or routers, this file will be
|
||||
modified to include the generated files.
|
||||
|
||||
### requirements.txt
|
||||
|
||||
The requirements file containing the Python dependencies.
|
||||
Whenever you run `m4g install` or `m4g uninstall` this file will be updated too.
|
||||
@@ -4,20 +4,17 @@ sidebar_position: 3
|
||||
|
||||
# Tutorial
|
||||
|
||||
We're going to build a simple todo app to demonstrate the capabilities of the MycroForge CLI.
|
||||
After this tutorial, you should have a solid foundation to start exploring and using MycroForge to develop your
|
||||
projects.
|
||||
In this tutorial, we'll build a simple todo app to demonstrate the capabilities of the MycroForge CLI.
|
||||
By the end, you should have a solid foundation to start exploring and using MycroForge for your projects.
|
||||
|
||||
## General notes
|
||||
|
||||
The commands in this tutorial assume that you're running them from a MycroForge root directory.
|
||||
The commands in this tutorial assume that you are running them from the root directory of your MycroForge project.
|
||||
|
||||
## Initialize the project
|
||||
## Initialize the Project
|
||||
|
||||
Open a terminal and `cd` into the directory where your project should be created.
|
||||
Run `m4g init todo-app` to initialize a new project and open the newly created project by running `code todo-app`.
|
||||
Make sure you have `vscode` on you machine before running this command. If you prefer using another editor, then
|
||||
manually open the generated `todo-app` folder.
|
||||
Open a terminal and navigate (`cd`) to the directory where your project should be created.
|
||||
Run the following command to initialize a new project and open it in VSCode:
|
||||
|
||||
## Setup the database
|
||||
|
||||
@@ -31,7 +28,9 @@ locally.
|
||||
|
||||
The first step is to start the database, you can do this by running the following command in a terminal.
|
||||
|
||||
`m4g db run`
|
||||
```bash
|
||||
m4g db run
|
||||
```
|
||||
|
||||
This command starts the services defined in the `db.docker-compose.yml` file.
|
||||
You can verify that the services are up by running `docker container ls`. If everything went well, then the previous
|
||||
@@ -50,9 +49,13 @@ When you're done developing, you can shut down the local database by running `m4
|
||||
Now that the database is running, we can start to create our entities. Run the commands below to create the `Todo` &
|
||||
`Tag` entities.
|
||||
|
||||
`m4g db generate entity Tag --column "description:str:String(255)"`
|
||||
```bash
|
||||
m4g db generate entity Tag --column "description:str:String(255)"
|
||||
```
|
||||
|
||||
`m4g db generate entity Todo --column "description:str:String(255)" -c "is_done:bool:Boolean()"`
|
||||
```bash
|
||||
m4g db generate entity Todo --column "description:str:String(255)" -c "is_done:bool:Boolean()"
|
||||
```
|
||||
|
||||
After running these commands, you should find the generated entities in the `db/entities` folder of your project.
|
||||
You should also see that the `main.py` & `db/env.py` files have been modified to include the newly generated entity.
|
||||
@@ -68,7 +71,9 @@ Creating a one-to-many relation would also make sense, but for the purpose of de
|
||||
the many-to-many relation, because this one is the most complex, since it requires an additional mapping to be included
|
||||
in the database schema.
|
||||
|
||||
`m4g db link many Todo --to-many Tag`
|
||||
```bash
|
||||
m4g db link many Todo --to-many Tag
|
||||
```
|
||||
|
||||
After running this command you should see that both the `Todo` and `Tag` entities now have a new field referencing the
|
||||
a `List` containing instances of the other entity.
|
||||
@@ -82,7 +87,9 @@ examine the command. The same is true for all the other commands as well.
|
||||
Now that we've generated our entities, it's time to generate a migration that will apply these changes in the database.
|
||||
Generate the initial migration by running the following command.
|
||||
|
||||
`m4g db generate migration initial_migration`
|
||||
```bash
|
||||
m4g db generate migration initial_migration
|
||||
```
|
||||
|
||||
After running this command, you should see the new migration in the `db/version` directory.
|
||||
|
||||
@@ -91,7 +98,9 @@ After running this command, you should see the new migration in the `db/version`
|
||||
The last step for the database setup is to actually apply the new migration to the database. This can be done by running
|
||||
the following command.
|
||||
|
||||
`m4g db migrate`
|
||||
```bash
|
||||
m4g db migrate
|
||||
```
|
||||
|
||||
After running this command, you should now see a populated schema when visiting [PhpMyAdmin](http://localhost:5051).
|
||||
If for whatever reason you want to undo the last migration, you can simply run `m4g db rollback`.
|
||||
@@ -105,9 +114,13 @@ Writing this code can be boring, since it's pretty much boilerplate with some cu
|
||||
Fortunately, MycroForge can generate a good chunk of this boring code on your behalf. Run the following commands to
|
||||
generate CRUD functionality for the `Todo` & `Tag` classes.
|
||||
|
||||
`m4g api generate crud Tag`
|
||||
```bash
|
||||
m4g api generate crud Tag
|
||||
```
|
||||
|
||||
`m4g api generate crud Todo`
|
||||
```bash
|
||||
m4g api generate crud Todo
|
||||
```
|
||||
|
||||
After running this command you should see that the `api/requests`,`api/routers` & `api/services` now contain the
|
||||
relevant classes need to support the generated CRUD functionality. This could should be relatively straightforward, so
|
||||
@@ -121,15 +134,22 @@ yet. We need to be able to specify which `Tags` to add to a `Todo` when creating
|
||||
To do this, we will allow for a `tag_ids` field in both the `CreateTodoRequest` & the `UpdateTodoRequest`.
|
||||
This field will contain the ids of the `Tags` that are associated with a `Todo`.
|
||||
|
||||
Modify `CreateTodoRequest` in `api/requests/create_todo_request.py`, you might need to import `List` from `typing`.
|
||||
Modify `CreateTodoRequest` in `api/requests/create_todo_request.py`.
|
||||
|
||||
```python
|
||||
# Before
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class CreateTodoRequest(BaseModel):
|
||||
description: str = None
|
||||
is_done: bool = None
|
||||
|
||||
description: str = None
|
||||
is_done: bool = None
|
||||
|
||||
# After
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class CreateTodoRequest(BaseModel):
|
||||
description: str = None
|
||||
is_done: bool = None
|
||||
@@ -140,12 +160,19 @@ Modify `UpdateTodoRequest` in `api/requests/update_todo_request.py`, you might n
|
||||
|
||||
```python
|
||||
# Before
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class UpdateTodoRequest(BaseModel):
|
||||
description: Optional[str] = None
|
||||
is_done: Optional[bool] = None
|
||||
tag_ids: Optional[List[int]] = []
|
||||
description: Optional[str] = None
|
||||
is_done: Optional[bool] = None
|
||||
|
||||
# After
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class UpdateTodoRequest(BaseModel):
|
||||
description: Optional[str] = None
|
||||
is_done: Optional[bool] = None
|
||||
@@ -167,16 +194,18 @@ Modify `TodoService.list`
|
||||
|
||||
```python
|
||||
# Before
|
||||
async with async_session() as session:
|
||||
stmt = select(Todo)
|
||||
results = (await session.scalars(stmt)).all()
|
||||
return results
|
||||
async def list(self) -> List[Todo]:
|
||||
async with async_session() as session:
|
||||
stmt = select(Todo)
|
||||
results = (await session.scalars(stmt)).all()
|
||||
return results
|
||||
|
||||
# After
|
||||
async with async_session() as session:
|
||||
stmt = select(Todo).options(selectinload(Todo.tags))
|
||||
results = (await session.scalars(stmt)).all()
|
||||
return results
|
||||
async def list(self) -> List[Todo]:
|
||||
async with async_session() as session:
|
||||
stmt = select(Todo).options(selectinload(Todo.tags))
|
||||
results = (await session.scalars(stmt)).all()
|
||||
return results
|
||||
```
|
||||
|
||||
Modify `TodoService.get_by_id`
|
||||
@@ -279,5 +308,11 @@ Modify `TodoService.update`
|
||||
return True
|
||||
```
|
||||
|
||||
At this point, the app should be ready to test.
|
||||
TODO: Elaborate!
|
||||
## Test the API!
|
||||
|
||||
Run the following command.
|
||||
```bash
|
||||
m4g api run
|
||||
```
|
||||
|
||||
Go to http://localhost:5000/docs and test your Todo API!
|
||||
@@ -77,8 +77,9 @@ const config: Config = {
|
||||
copyright: `Copyright © ${new Date().getFullYear()} DevDisciples`,
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.github,
|
||||
darkTheme: prismThemes.dracula,
|
||||
theme: prismThemes.oneLight,
|
||||
darkTheme: prismThemes.oneDark,
|
||||
additionalLanguages: ["csharp"]
|
||||
},
|
||||
} satisfies Preset.ThemeConfig,
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"start": "docusaurus start --host 0.0.0.0",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
|
||||
@@ -10,22 +10,22 @@ type FeatureItem = {
|
||||
|
||||
const FeatureList: FeatureItem[] = [
|
||||
{
|
||||
title: 'Initialize a skeleton project quickly',
|
||||
title: 'Initialize a Skeleton Project Quickly',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Initialize a skeleton project that supports FastAPI and SQLAlchemy with a single command.
|
||||
Here is an example. <code>m4g init todo-app</code>
|
||||
Start a new FastAPI and SQLAlchemy project with a single command.
|
||||
For example: <code>m4g init todo-app</code>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Generate common items',
|
||||
title: 'Generate Common Items',
|
||||
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
|
||||
description: (
|
||||
<>
|
||||
MycroForge allows you to generate boilerplate code for common items like entities, service & routers.
|
||||
It can even generate a basic CRUD setup around an entity!
|
||||
Use MycroForge to generate boilerplate code for entities, services, and routers.
|
||||
It even supports basic CRUD setup!
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -34,15 +34,15 @@ const FeatureList: FeatureItem[] = [
|
||||
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
|
||||
description: (
|
||||
<>
|
||||
Extend MycroForge with your own commands by creating a plugin!
|
||||
Create plugins to extend MycroForge with your own custom commands.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function Feature({title, Svg, description}: FeatureItem) {
|
||||
function Feature({ title, Svg, description }: FeatureItem) {
|
||||
return (
|
||||
<div className={clsx('col col--4')}>
|
||||
<div className={clsx('col col--4', styles.feature)}>
|
||||
<div className="text--center">
|
||||
<Svg className={styles.featureSvg} role="img" />
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ function HomepageHeader() {
|
||||
<Link
|
||||
className="button button--secondary button--lg"
|
||||
to="/docs/intro">
|
||||
MycroForge Tutorial
|
||||
Get started now!
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user