Improved a bunch of stuff

This commit is contained in:
Donné Napo 2024-04-24 12:38:10 +02:00
parent abd116b032
commit 5e1be11ec2
34 changed files with 793 additions and 543 deletions

View File

@ -0,0 +1,132 @@
using Humanizer;
using MycroForge.Parsing;
namespace MycroForge.CLI.CodeGen;
public partial class EntityLinker
{
public class EntityModel : PythonSourceModifier
{
private readonly string _className;
private readonly string _path;
private readonly List<PythonParser.Import_fromContext> _importCtxs;
private readonly List<string> _importsBuffer;
private PythonParser.Import_fromContext LastImport => _importCtxs.Last();
private readonly List<PythonParser.Class_defContext> _classCtxs;
private PythonParser.AssignmentContext _tableCtx;
private readonly List<PythonParser.AssignmentContext> _columnCtxs;
private readonly List<string> _columnsBuffer;
private PythonParser.AssignmentContext LastColumn => _columnCtxs.Last();
public string ClassName => _className;
public string Path => _path;
public string FieldName => _className.ToLower().Underscore();
public string TableName => GetOriginalText(_tableCtx)
.Replace("__tablename__", string.Empty)
.Replace("=", string.Empty)
.Replace("\"", string.Empty)
.Trim();
public EntityModel(string className, string path, string source) : base(source)
{
_className = className;
_path = path;
_importCtxs = new();
_importsBuffer = new();
_classCtxs = new();
_tableCtx = default!;
_columnCtxs = new();
_columnsBuffer = new();
}
public void Initialize()
{
var tree = Parser.file_input();
Visit(tree);
if (!_classCtxs.Any(c => GetOriginalText(c).Contains(_className)))
throw new Exception($"Entity {_className} was not found in {_path}.");
if (_columnCtxs.Count == 0)
throw new Exception($"Entity {_className} has no columns.");
_importsBuffer.Add(GetOriginalText(LastImport));
_columnsBuffer.Add(GetOriginalText(LastColumn));
InsertRelationshipImport();
InsertForeignKeyImport();
}
private void InsertRelationshipImport()
{
var relationship = _importCtxs.FirstOrDefault(import =>
{
var text = GetOriginalText(import);
return text.Contains("sqlalchemy.orm") && text.Contains("relationship");
});
if (relationship is null)
_importsBuffer.Add("from sqlalchemy.orm import relationship");
}
private void InsertForeignKeyImport()
{
var foreignKey = _importCtxs.FirstOrDefault(import =>
{
var text = GetOriginalText(import);
return text.Contains("sqlalchemy") && text.Contains("ForeignKey");
});
if (foreignKey is null)
_importsBuffer.Add("from sqlalchemy import ForeignKey");
}
public override object? VisitImport_from(PythonParser.Import_fromContext context)
{
_importCtxs.Add(context);
return base.VisitImport_from(context);
}
public override object? VisitClass_def(PythonParser.Class_defContext context)
{
_classCtxs.Add(context);
return base.VisitClass_def(context);
}
public override object? VisitAssignment(PythonParser.AssignmentContext context)
{
var text = GetOriginalText(context);
if (text.StartsWith("__tablename__"))
_tableCtx = context;
if (text.Contains("Mapped["))
_columnCtxs.Add(context);
return base.VisitAssignment(context);
}
public void AppendColumn(params string[] text) => _columnsBuffer.Add(string.Join('\n', text));
public void AppendImportIfNotExists(string module, string import, params string[] text)
{
var isExisting = _importCtxs.Select(GetOriginalText).Any(ctx => ctx.Contains(module) && ctx.Contains(import));
var isBuffered = _importsBuffer.Any(txt => txt.Contains(module) && txt.Contains(import));
if (!isExisting && !isBuffered)
_importsBuffer.Add(string.Join('\n', text));
}
public override string Rewrite()
{
Rewrite(LastImport, _importsBuffer.ToArray());
Rewrite(LastColumn, _columnsBuffer.ToArray());
return Rewriter.GetText();
}
}
}

View File

@ -1,16 +1,136 @@
using MycroForge.Parsing;
using Humanizer;
namespace MycroForge.CLI.CodeGen;
public class EntityLinker : PythonSourceModifier
public partial class EntityLinker
{
public EntityLinker(string source) : base(source)
private readonly ProjectContext _context;
private readonly string _left;
private readonly string _right;
public EntityLinker(ProjectContext context, string left, string right)
{
_context = context;
_left = left;
_right = right;
}
public override object? VisitAssignment(PythonParser.AssignmentContext context)
public async Task OneToOne()
{
Console.WriteLine(GetOriginalText(context));
return base.VisitAssignment(context);
var left = await LoadEntity(_left);
var right = await LoadEntity(_right);
left.AppendColumn([
$"\t{right.FieldName}: Mapped[\"{right.ClassName}\"] = relationship(back_populates=\"{left.FieldName}\")"
]);
right.AppendColumn([
$"\t{left.FieldName}_id: Mapped[int] = mapped_column(ForeignKey(\"{left.TableName}.id\"))",
$"\t{left.FieldName}: Mapped[\"{left.ClassName}\"] = relationship(back_populates=\"{right.FieldName}\")"
]);
await _context.WriteFile(left.Path, left.Rewrite());
await _context.WriteFile(right.Path, right.Rewrite());
}
public async Task OneToMany()
{
var left = await LoadEntity(_left);
var right = await LoadEntity(_right);
left.AppendImportIfNotExists(
"typing",
"List",
"from typing import List"
);
left.AppendColumn([
$"\t{right.FieldName.Pluralize()}: Mapped[List[\"{right.ClassName}\"]] = relationship()"
]);
right.AppendColumn([
$"\t{left.FieldName}_id: Mapped[int] = mapped_column(ForeignKey(\"{left.TableName}.id\"))",
$"\t{left.FieldName}: Mapped[\"{left.ClassName}\"] = relationship(back_populates=\"{right.FieldName}\")"
]);
await _context.WriteFile(left.Path, left.Rewrite());
await _context.WriteFile(right.Path, right.Rewrite());
}
public async Task ManyToOne()
{
var left = await LoadEntity(_left);
var right = await LoadEntity(_right);
left.AppendImportIfNotExists(
"typing",
"Optional",
"from typing import Optional"
);
left.AppendColumn([
$"\t{right.FieldName}_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"{right.TableName}.id\"))",
$"\t{right.FieldName}: Mapped[\"{right.ClassName}\"] = relationship(back_populates=\"{left.FieldName.Pluralize()}\")"
]);
left.AppendImportIfNotExists(
"typing",
"List",
"from typing import List"
);
right.AppendColumn([
$"\t{left.FieldName}_id: Mapped[int] = mapped_column(ForeignKey(\"{left.TableName}.id\"))",
$"\t{left.FieldName.Pluralize()}: Mapped[List[\"{left.ClassName}\"]] = relationship(back_populates=\"{right.FieldName}\")"
]);
await _context.WriteFile(left.Path, left.Rewrite());
await _context.WriteFile(right.Path, right.Rewrite());
}
public async Task ManyToMany()
{
// var left = await LoadEntity(_left);
// var right = await LoadEntity(_right);
//
// left.AppendImportIfNotExists(
// $"orm.entities.{right.FieldName}",
// $"{right.ClassName}",
// $"from orm.entities.{right.FieldName} import {right.ClassName}"
// );
// left.AppendImportIfNotExists(
// "typing",
// "Optional",
// "from typing import Optional"
// );
// left.AppendColumn([
// $"\t{right.FieldName}_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"{right.TableName}.id\"))",
// $"\t{right.FieldName}: Mapped[\"{right.ClassName}\"] = relationship(back_populates=\"{left.FieldName.Pluralize()}\")\n"
// ]);
//
// right.AppendImportIfNotExists(
// $"orm.entities.{left.FieldName}",
// $"{left.ClassName}",
// $"from orm.entities.{left.FieldName} import {left.ClassName}"
// );
// left.AppendImportIfNotExists(
// "typing",
// "List",
// "from typing import List"
// );
// right.AppendColumn([
// $"\t{left.FieldName}_id: Mapped[int] = mapped_column(ForeignKey(\"{left.TableName}.id\"))",
// $"\t{left.FieldName.Pluralize()}: Mapped[List[\"{left.ClassName}\"]] = relationship(back_populates=\"{right.FieldName}\")\n"
// ]);
//
// await _context.WriteFile(left.Path, left.Rewrite());
// await _context.WriteFile(right.Path, right.Rewrite());
}
private async Task<EntityModel> LoadEntity(string name)
{
var path = GetEntityPath(name);
var entity = new EntityModel(name, path, await _context.ReadFile(path));
entity.Initialize();
return entity;
}
private string GetEntityPath(string name) => $"orm/entities/{name.Underscore().ToLower()}.py";
}

View File

@ -5,9 +5,9 @@ namespace MycroForge.CLI.CodeGen;
public abstract class PythonSourceModifier : PythonParserBaseVisitor<object?>
{
private CommonTokenStream Stream { get; }
private PythonParser Parser { get; }
private TokenStreamRewriter Rewriter { get; }
protected CommonTokenStream Stream { get; }
protected PythonParser Parser { get; }
protected TokenStreamRewriter Rewriter { get; }
protected PythonSourceModifier(string source)
{
@ -18,7 +18,7 @@ public abstract class PythonSourceModifier : PythonParserBaseVisitor<object?>
Rewriter = new TokenStreamRewriter(Stream);
}
public string Rewrite()
public virtual string Rewrite()
{
var tree = Parser.file_input();
Visit(tree);

View File

@ -1,21 +0,0 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
using MycroForge.CLI.Features;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Add
{
public class Api : Command, ISubCommandOf<Add>
{
public Api(ProjectContext context, IEnumerable<IFeature> features) :
base("api", "Add FastAPI to your project")
{
var feature = features.First(f => f.Name == Features.Api.FeatureName);
this.SetHandler(async () => await feature.ExecuteAsync(context));
}
}
}
}

View File

@ -1,26 +0,0 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
using MycroForge.CLI.Features;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Add
{
public class Orm : Command, ISubCommandOf<Add>
{
public Orm(ProjectContext context, IEnumerable<IFeature> features) :
base("orm", "Add SQLAlchemy to your project")
{
var feature = features.First(f => f.Name == Features.Orm.FeatureName);
this.SetHandler(async () => await feature.ExecuteAsync(context));
}
public class Generate
{
}
}
}
}

View File

@ -0,0 +1,56 @@
using System.CommandLine;
using Humanizer;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Api
{
public partial class Generate
{
public class Router : Command, ISubCommandOf<Generate>
{
private static readonly string[] Template =
[
"from fastapi import APIRouter",
"from fastapi.responses import JSONResponse",
"from fastapi.encoders import jsonable_encoder",
"",
"router = APIRouter()",
"",
"@router.get(\"/{name}\")",
"async def index(name: str):",
"\treturn JSONResponse(status_code=200, content=jsonable_encoder({'greeting': f\"Hello, {name}!\"}))"
];
private static readonly Argument<string> NameArgument =
new(name: "name", description: "The name of the api router");
private readonly ProjectContext _context;
public Router(ProjectContext context) : base("router", "Generate an api router")
{
_context = context;
AddAlias("r");
AddArgument(NameArgument);
this.SetHandler(ExecuteAsync, NameArgument);
}
private async Task ExecuteAsync(string name)
{
var moduleName = name.Underscore();
await _context.CreateFile($"api/routers/{moduleName}.py", Template);
var main = await _context.ReadFile("main.py");
main += string.Join('\n',
$"\nfrom api.routers import {moduleName}",
$"app.include_router(prefix=\"/{name.Kebaberize()}\", router={moduleName}.router)\n"
);
await _context.WriteFile("main.py", main);
}
}
}
}
}

View File

@ -0,0 +1,21 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Api
{
public partial class Generate : Command, ISubCommandOf<Api>
{
public Generate(IEnumerable<ISubCommandOf<Generate>> subCommands) :
base("generate", "Generate an API item")
{
AddAlias("g");
foreach (var subCommandOf in subCommands.Cast<Command>())
AddCommand(subCommandOf);
}
}
}
}

View File

@ -0,0 +1,26 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Api
{
public class Run : Command, ISubCommandOf<Api>
{
public Run() : base("run", "Run your app")
{
this.SetHandler(ExecuteAsync);
}
private async Task ExecuteAsync()
{
await Bash.ExecuteAsync([
"source .venv/bin/activate",
"uvicorn main:app --reload"
]);
}
}
}
}

View File

@ -1,14 +1,14 @@
using System.CommandLine;
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public new partial class Add : Command, ISubCommandOf<MycroForge>
public partial class Api : Command, ISubCommandOf<MycroForge>
{
public Add(IEnumerable<ISubCommandOf<Add>> subCommands) :
base("add", "Add a predefined feature to your project")
public Api(IEnumerable<ISubCommandOf<Api>> subCommands) :
base("api", "API related commands")
{
foreach (var subCommandOf in subCommands)
AddCommand((subCommandOf as Command)!);

View File

@ -1,37 +0,0 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Entity
{
public partial class Link
{
public class Many : Command, ISubCommandOf<Link>
{
private static readonly Argument<string> NameArgument =
new(name: "primary", description: "The left side of the relation");
private static readonly Option<string> ToOneOption =
new(name: "--to-one", description: "The right side of the relation");
private static readonly Option<string> ToManyOption =
new(name: "--to-many", description: "The right side of the relation");
public Many() : base("many", "Define a n:m relation")
{
AddArgument(NameArgument);
AddOption(ToOneOption);
AddOption(ToManyOption);
this.SetHandler(ExecuteAsync);
}
private async Task ExecuteAsync()
{
}
}
}
}
}

View File

@ -1,38 +0,0 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Entity
{
public partial class Link
{
public class One : Command, ISubCommandOf<Link>
{
private static readonly Argument<string> NameArgument =
new(name: "primary", description: "The left side of the relation");
private static readonly Option<string> ToOneOption =
new(name: "--to-one", description: "The right side of the relation");
private static readonly Option<string> ToManyOption =
new(name: "--to-many", description: "The right side of the relation");
public One() : base("one", "Define a 1:n relation")
{
AddArgument(NameArgument);
AddOption(ToOneOption);
AddOption(ToManyOption);
this.SetHandler(ExecuteAsync);
}
private async Task ExecuteAsync()
{
}
}
}
}
}

View File

@ -1,17 +0,0 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Entity : Command, ISubCommandOf<MycroForge>
{
public Entity(IEnumerable<ISubCommandOf<Entity>> commands) :
base("entity", "Manage the entities in your project")
{
foreach (var command in commands.Cast<Command>())
AddCommand(command);
}
}
}

View File

@ -1,59 +0,0 @@
using System.CommandLine;
using Humanizer;
using MycroForge.CLI.CodeGen;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Generate
{
// ReSharper disable once MemberHidesStaticFromOuterClass
public class Entity : Command, ISubCommandOf<Generate>
{
private static readonly string[] Template =
[
"from sqlalchemy import String",
"from sqlalchemy.orm import Mapped, mapped_column",
"from orm.entities.entity_base import EntityBase",
"",
"class %class_name%(EntityBase):",
"\t__tablename__ = \"%table_name%\"",
"\tid: Mapped[int] = mapped_column(primary_key=True)",
"\tvalue: Mapped[str] = mapped_column(String(255))",
"",
"\tdef __repr__(self) -> str:",
"\t\treturn f\"%class_name%(id={self.id!r}, value={self.value!r})\""
];
private static readonly Argument<string> NameArgument =
new(name: "name", description: "The name of the orm entity");
private readonly ProjectContext _context;
public Entity(ProjectContext context) : base("entity", "Generate and orm entity")
{
_context = context;
AddAlias("e");
AddArgument(NameArgument);
this.SetHandler(ExecuteAsync, NameArgument);
}
private async Task ExecuteAsync(string name)
{
var className = name.Underscore().Pascalize();
var moduleName = name.Underscore();
var code = string.Join('\n', Template);
code = code.Replace("%class_name%", className);
code = code.Replace("%table_name%", name.ToLower().Underscore());
await _context.CreateFile($"orm/entities/{moduleName}.py", code);
var env = await _context.ReadFile("orm/env.py");
env = new OrmEnvUpdater(env, moduleName, className).Rewrite();
await _context.WriteFile("orm/env.py", env);
}
}
}
}

View File

@ -1,32 +0,0 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Generate
{
public class Migration : Command, ISubCommandOf<Generate>
{
private static readonly Argument<string> NameArgument =
new(name: "name", description: "The name of the migration");
public Migration() : base("migration", "Generate a migration")
{
AddAlias("m");
AddArgument(NameArgument);
this.SetHandler(ExecuteAsync, NameArgument);
}
private async Task ExecuteAsync(string name)
{
await Bash.ExecuteAsync(
"source .venv/bin/activate",
$"alembic revision --autogenerate -m \"{name}\" --rev-id $(date -u +\"%Y%m%d%H%M%S\")"
);
}
}
}
}

View File

@ -1,53 +0,0 @@
using System.CommandLine;
using Humanizer;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Generate
{
public class Router : Command, ISubCommandOf<Generate>
{
private static readonly string[] Template =
[
"from fastapi import APIRouter",
"from fastapi.responses import JSONResponse",
"from fastapi.encoders import jsonable_encoder",
"",
"router = APIRouter()",
"",
"@router.get(\"/{name}\")",
"async def index(name: str):",
"\treturn JSONResponse(status_code=200, content=jsonable_encoder({'greeting': f\"Hello, {name}!\"}))"
];
private static readonly Argument<string> NameArgument =
new(name: "name", description: "The name of the api router");
private readonly ProjectContext _context;
public Router(ProjectContext context) : base("router", "Generate an api router")
{
_context = context;
AddAlias("r");
AddArgument(NameArgument);
this.SetHandler(ExecuteAsync, NameArgument);
}
private async Task ExecuteAsync(string name)
{
var moduleName = name.Underscore();
await _context.CreateFile($"api/routers/{moduleName}.py", Template);
var main = await _context.ReadFile("main.py");
main += string.Join('\n',
$"\nfrom api.routers import {moduleName}",
$"app.include_router(prefix=\"/{name.Kebaberize()}\", router={moduleName}.router)\n"
);
await _context.WriteFile("main.py", main);
}
}
}
}

View File

@ -1,18 +0,0 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Generate : Command, ISubCommandOf<MycroForge>
{
public Generate(IEnumerable<ISubCommandOf<Generate>> subCommands) :
base("generate", "Generate a project item")
{
AddAlias("g");
foreach (var subCommandOf in subCommands)
AddCommand((subCommandOf as Command)!);
}
}
}

View File

@ -1,5 +1,4 @@
using System.CommandLine;
using Microsoft.Extensions.DependencyInjection;
using MycroForge.CLI.Commands.Interfaces;
using MycroForge.CLI.Features;
@ -9,127 +8,60 @@ public partial class MycroForge
{
public class Init : Command, ISubCommandOf<MycroForge>
{
#region GitIgnore
private static readonly string[] GitIgnore =
private static readonly string[] DefaultFeatures =
[
"# Byte-compiled / optimized / DLL files", "__pycache__/", "*.py[cod]", "*$py.class", "# C extensions",
"*.so", "# Distribution / packaging", ".Python", "build/", "develop-eggs/", "dist/", "downloads/", "eggs/",
".eggs/", "lib/", "lib64/", "parts/", "sdist/", "var/", "wheels/", "share/python-wheels/", "*.egg-info/",
".installed.cfg", "*.egg", "MANIFEST", "# PyInstaller",
"# Usually these files are written by a python script from a template",
"# before PyInstaller builds the exe, so as to inject date/other infos into it.", "*.manifest", "*.spec",
"# Installer logs", "pip-log.txt", "pip-delete-this-directory.txt", "# Unit test / coverage reports",
"htmlcov/", ".tox/", ".nox/", ".coverage", ".coverage.*", ".cache", "nosetests.xml", "coverage.xml",
"*.cover", "*.py,cover", ".hypothesis/", ".pytest_cache/", "cover/", "# Translations", "*.mo", "*.pot",
"# Django stuff:", "*.log", "local_settings.py", "db.sqlite3", "db.sqlite3-journal", "# Flask stuff:",
"instance/", ".webassets-cache", "# Scrapy stuff:", ".scrapy", "# Sphinx documentation", "docs/_build/",
"# PyBuilder", ".pybuilder/", "target/", "# Jupyter Notebook", ".ipynb_checkpoints", "# IPython",
"profile_default/", "ipython_config.py", "# pyenv",
"# For a library or package, you might want to ignore these files since the code is",
"# intended to run in multiple environments; otherwise, check them in:", "# .python-version", "# pipenv",
"# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.",
"# However, in case of collaboration, if having platform-specific dependencies or dependencies",
"# having no cross-platform support, pipenv may install dependencies that don't work, or not",
"# install all needed dependencies.", "#Pipfile.lock", "# poetry",
"# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.",
"# This is especially recommended for binary packages to ensure reproducibility, and is more",
"# commonly ignored for libraries.",
"# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control",
"#poetry.lock", "# pdm",
"# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.",
"#pdm.lock",
"# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it",
"# in version control.", "# https://pdm.fming.dev/#use-with-ide", ".pdm.toml",
"# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm", "__pypackages__/",
"# Celery stuff", "celerybeat-schedule", "celerybeat.pid", "# SageMath parsed files", "*.sage.py",
"# Environments", ".env", ".venv", "env/", "venv/", "ENV/", "env.bak/", "venv.bak/",
"# Spyder project settings", ".spyderproject", ".spyproject", "# Rope project settings", ".ropeproject",
"# mkdocs documentation", "/site", "# mypy", ".mypy_cache/", ".dmypy.json", "dmypy.json",
"# Pyre type checker", ".pyre/", "# pytype static type analyzer", ".pytype/", "# Cython debug symbols",
"cython_debug/", "# PyCharm",
"# JetBrains specific template is maintained in a separate JetBrains.gitignore that can",
"# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore",
"# and can be added to the global gitignore or merged into this file. For a more nuclear",
"# option (not recommended) you can uncomment the following to ignore the entire idea folder.", "#.idea/"
Features.Git.FeatureName,
Features.Api.FeatureName,
Features.Orm.FeatureName
];
#endregion
private static readonly Argument<string> NameArgument =
new(name: "name", description: "The name of your project");
private static readonly Option<string> EntryPoint =
new(name: "--entrypoint", description: "The name of the entrypoint file");
private static readonly Option<string> BranchOption =
new(name: "--branch", description: "The name of the initial git branch");
private static readonly Option<IEnumerable<string>> FeaturesOption =
new(name: "--features", description: "The features to include")
{
AllowMultipleArgumentsPerToken = true
};
private readonly IServiceProvider _services;
private static readonly Option<IEnumerable<string>> WithoutOption =
new Option<IEnumerable<string>>(name: "--without", description: "Features to exclude")
.FromAmong(DefaultFeatures);
private readonly ProjectContext _context;
private readonly List<IFeature> _features;
public Init(IServiceProvider services) : base("init", "Initialize a new project")
public Init(ProjectContext context, IEnumerable<IFeature> features) :
base("init", "Initialize a new project")
{
_context = context;
_features = features.ToList();
AddArgument(NameArgument);
AddOption(EntryPoint);
AddOption(BranchOption);
AddOption(FeaturesOption);
this.SetHandler(ExecuteAsync, NameArgument, EntryPoint, BranchOption, FeaturesOption);
_services = services;
_features = _services.GetServices<IFeature>().ToList();
AddOption(WithoutOption);
this.SetHandler(ExecuteAsync, NameArgument, WithoutOption);
}
private async Task ExecuteAsync(string name, string entrypoint, string branch, IEnumerable<string> features)
private async Task ExecuteAsync(string name, IEnumerable<string> without)
{
var featuresList = features.ToList();
Validate(featuresList);
await Initialize(name, entrypoint, branch, featuresList);
}
private void Validate(List<string> features)
{
foreach (var feature in features)
// Validate excluded features
var withoutList = without.ToList();
foreach (var feature in withoutList)
if (_features.All(f => f.Name != feature))
throw new Exception($"Feature {feature} was not found.");
}
throw new Exception($"Feature {feature} does not exist.");
private async Task Initialize(string name, string entrypoint, string branch, List<string> features)
{
// Create the project directory and change the directory for the ProjectContext
var projectRoot = await CreateDirectory(name);
var ctx = _services.GetRequiredService<ProjectContext>();
ctx.ChangeDirectory(projectRoot);
_context.ChangeDirectory(projectRoot);
// Create the config file and initialize the config
await ctx.CreateFile("m4g.json", "{}");
await ctx.LoadConfig(force: true);
await _context.CreateFile("m4g.json", "{}");
await _context.LoadConfig(force: true);
// Create the entrypoint file
entrypoint = string.IsNullOrEmpty(entrypoint) ? "main.py" : entrypoint;
await ctx.CreateFile(entrypoint, string.Empty);
ctx.Config.Entrypoint = entrypoint;
// Create the default .gitignore
await ctx.CreateFile(".gitignore", GitIgnore);
await _context.CreateFile("main.py");
// Create the venv
await Bash.ExecuteAsync($"python3 -m venv {Path.Combine(projectRoot, ".venv")}");
// Initialize git
var _branch = string.IsNullOrEmpty(branch) ? "main" : branch;
await Bash.ExecuteAsync($"git -c init.defaultBranch={_branch} init {projectRoot}");
// Initialize features
if (features.Count > 0)
await InitializeFeatures(ctx, features);
// Initialize default features
foreach (var feature in _features.Where(f => DefaultFeatures.Contains(f.Name)))
if (!withoutList.Contains(feature.Name))
await feature.ExecuteAsync(_context);
Console.WriteLine($"Directory {projectRoot} was successfully initialized");
}
@ -149,11 +81,5 @@ public partial class MycroForge
return directory;
}
private async Task InitializeFeatures(ProjectContext projectCtx, List<string> features)
{
foreach (var feature in features)
await _features.First(p => p.Name == feature).ExecuteAsync(projectCtx);
}
}
}

View File

@ -1,18 +0,0 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Migrations : Command, ISubCommandOf<MycroForge>
{
public Migrations(IEnumerable<ISubCommandOf<Migrations>> subCommands) :
base("migrations", "Manage your migrations")
{
AddAlias("m");
foreach (var subCommandOf in subCommands)
AddCommand((subCommandOf as Command)!);
}
}
}

View File

@ -0,0 +1,61 @@
using System.CommandLine;
using Humanizer;
using MycroForge.CLI.CodeGen;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Orm
{
public partial class Generate
{
public class Entity : Command, ISubCommandOf<Generate>
{
private static readonly string[] Template =
[
"from sqlalchemy import String",
"from sqlalchemy.orm import Mapped, mapped_column",
"from orm.entities.entity_base import EntityBase",
"",
"class %class_name%(EntityBase):",
"\t__tablename__ = \"%table_name%\"",
"\tid: Mapped[int] = mapped_column(primary_key=True)",
"\tvalue: Mapped[str] = mapped_column(String(255))",
"",
"\tdef __repr__(self) -> str:",
"\t\treturn f\"%class_name%(id={self.id!r}, value={self.value!r})\""
];
private static readonly Argument<string> NameArgument =
new(name: "name", description: "The name of the orm entity");
private readonly ProjectContext _context;
public Entity(ProjectContext context) : base("entity", "Generate and orm entity")
{
_context = context;
AddAlias("e");
AddArgument(NameArgument);
this.SetHandler(ExecuteAsync, NameArgument);
}
private async Task ExecuteAsync(string name)
{
var className = name.Underscore().Pascalize();
var moduleName = name.Underscore();
var code = string.Join('\n', Template);
code = code.Replace("%class_name%", className);
code = code.Replace("%table_name%", name.Underscore().ToLower());
await _context.CreateFile($"orm/entities/{moduleName}.py", code);
var env = await _context.ReadFile("orm/env.py");
env = new OrmEnvUpdater(env, moduleName, className).Rewrite();
await _context.WriteFile("orm/env.py", env);
}
}
}
}
}

View File

@ -0,0 +1,34 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Orm
{
public partial class Generate
{
public class Migration : Command, ISubCommandOf<Generate>
{
private static readonly Argument<string> NameArgument =
new(name: "name", description: "The name of the migration");
public Migration() : base("migration", "Generate a migration")
{
AddAlias("m");
AddArgument(NameArgument);
this.SetHandler(ExecuteAsync, NameArgument);
}
private async Task ExecuteAsync(string name)
{
await Bash.ExecuteAsync(
"source .venv/bin/activate",
$"alembic revision --autogenerate -m \"{name}\" --rev-id $(date -u +\"%Y%m%d%H%M%S\")"
);
}
}
}
}
}

View File

@ -0,0 +1,22 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Orm
{
public partial class Generate : Command, ISubCommandOf<Orm>
{
public Generate(IEnumerable<ISubCommandOf<Generate>> subCommands) :
base("generate", "Generate an ORM item")
{
AddAlias("g");
foreach (var subCommandOf in subCommands.Cast<Command>())
AddCommand(subCommandOf);
}
}
}
}

View File

@ -0,0 +1,62 @@
using System.CommandLine;
using MycroForge.CLI.CodeGen;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Orm
{
public partial class Link
{
public class Many : Command, ISubCommandOf<Link>
{
private readonly ProjectContext _context;
private static readonly Argument<string> LeftArgument =
new(name: "entity", description: "The left side of the relation");
private static readonly Option<string> ToOneOption =
new(name: "--to-one", description: "The right side of the relation");
private static readonly Option<string> ToManyOption =
new(name: "--to-many", description: "The right side of the relation");
public Many(ProjectContext context) : base("many", "Define a n:m relation")
{
_context = context;
AddArgument(LeftArgument);
AddOption(ToOneOption);
AddOption(ToManyOption);
this.SetHandler(ExecuteAsync, LeftArgument, ToOneOption, ToManyOption);
}
private async Task ExecuteAsync(string left, string? toOneOption, string? toManyOption)
{
if (toOneOption is not null && toManyOption is not null)
throw new Exception("Cannot set both --to-one and --to-many option.");
if (toOneOption is not null)
await ManyToOne(left, toOneOption);
else if (toManyOption is not null)
await ManyToMany(left, toManyOption);
else throw new Exception("Set --to-one or --to-many option.");
}
private async Task ManyToOne(string left, string toOneOption)
{
await new EntityLinker(_context, left, toOneOption).ManyToOne();
}
private async Task ManyToMany(string left, string toManyOption)
{
await new EntityLinker(_context, left, toManyOption).ManyToMany();
}
}
}
}
}

View File

@ -0,0 +1,62 @@
using System.CommandLine;
using MycroForge.CLI.CodeGen;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Orm
{
public partial class Link
{
public class One : Command, ISubCommandOf<Link>
{
private readonly ProjectContext _context;
private static readonly Argument<string> LeftArgument =
new(name: "entity", description: "The left side of the relation");
private static readonly Option<string> ToOneOption =
new(name: "--to-one", description: "The right side of the relation");
private static readonly Option<string> ToManyOption =
new(name: "--to-many", description: "The right side of the relation");
public One(ProjectContext context) : base("one", "Define a 1:n relation")
{
_context = context;
AddArgument(LeftArgument);
AddOption(ToOneOption);
AddOption(ToManyOption);
this.SetHandler(ExecuteAsync, LeftArgument, ToOneOption, ToManyOption);
}
private async Task ExecuteAsync(string left, string? toOneOption, string? toManyOption)
{
if (toOneOption is not null && toManyOption is not null)
throw new Exception("Cannot set both --to-one and --to-many option.");
if (toOneOption is not null)
await OneToOne(left, toOneOption);
else if (toManyOption is not null)
await OneToMany(left, toManyOption);
else throw new Exception("Set --to-one or --to-many option.");
}
private async Task OneToOne(string left, string toOneOption)
{
await new EntityLinker(_context, left, toOneOption).OneToOne();
}
private async Task OneToMany(string left, string toManyOption)
{
await new EntityLinker(_context, left, toManyOption).OneToMany();
}
}
}
}
}

View File

@ -1,19 +1,19 @@
using System.CommandLine;
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Entity
public partial class Orm
{
public partial class Link : Command, ISubCommandOf<Entity>
public partial class Link : Command, ISubCommandOf<Orm>
{
public Link(IEnumerable<ISubCommandOf<Link>> commands) :
base("link", "Define relationships between entities")
{
foreach (var command in commands)
AddCommand((command as Command)!);
foreach (var command in commands.Cast<Command>())
AddCommand(command);
}
}
}

View File

@ -1,17 +1,16 @@
using System.CommandLine;
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Migrations
public partial class Orm
{
public class Apply : Command, ISubCommandOf<Migrations>
public class Migrate : Command, ISubCommandOf<Orm>
{
public Apply() : base("apply", "Apply migrations to the database")
public Migrate() : base("migrate", "Apply migrations to the database")
{
AddAlias("a");
this.SetHandler(ExecuteAsync);
}

View File

@ -1,17 +1,16 @@
using System.CommandLine;
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Migrations
public partial class Orm
{
public class Rollback : Command, ISubCommandOf<Migrations>
public class Rollback : Command, ISubCommandOf<Orm>
{
public Rollback() : base("rollback", "Rollback the last migration")
{
AddAlias("r");
this.SetHandler(ExecuteAsync);
}

View File

@ -0,0 +1,17 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public partial class Orm : Command, ISubCommandOf<MycroForge>
{
public Orm(IEnumerable<ISubCommandOf<Orm>> subCommands)
: base("orm", "ORM related commands")
{
foreach (var subCommandOf in subCommands.Cast<Command>())
AddCommand(subCommandOf);
}
}
}

View File

@ -1,23 +0,0 @@
using System.CommandLine;
using MycroForge.CLI.Commands.Interfaces;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
public class Run : Command, ISubCommandOf<MycroForge>
{
public Run() : base("run", "Run your app")
{
this.SetHandler(ExecuteAsync);
}
private async Task ExecuteAsync()
{
await Bash.ExecuteAsync([
"source .venv/bin/activate",
"uvicorn main:app --reload"
]);
}
}
}

View File

@ -15,6 +15,7 @@ public static class ServiceCollectionExtensions
Args = args.Length == 0 ? ["--help"] : args
});
services.AddScoped<ProjectContext>();
services.AddScoped<IFeature, Git>();
services.AddScoped<IFeature, Api>();
services.AddScoped<IFeature, Orm>();
@ -26,31 +27,26 @@ public static class ServiceCollectionExtensions
// Register "m4g"
services.AddScoped<Commands.MycroForge>();
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Init>();
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Run>();
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Install>();
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Uninstall>();
// Register "m4g add"
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Add>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Add>, Commands.MycroForge.Add.Api>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Add>, Commands.MycroForge.Add.Orm>();
// Register "m4g api"
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Api>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Api>, Commands.MycroForge.Api.Run>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Api>, Commands.MycroForge.Api.Generate>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Api.Generate>, Commands.MycroForge.Api.Generate.Router>();
// Register "m4g generate"
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Generate>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Generate>, Commands.MycroForge.Generate.Entity>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Generate>, Commands.MycroForge.Generate.Router>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Generate>, Commands.MycroForge.Generate.Migration>();
// Register "m4g entity"
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Entity>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Entity>, Commands.MycroForge.Entity.Link>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Entity.Link>, Commands.MycroForge.Entity.Link.One>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Entity.Link>, Commands.MycroForge.Entity.Link.Many>();
// Register "m4g migrations"
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Migrations>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Migrations>, Commands.MycroForge.Migrations.Apply>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Migrations>, Commands.MycroForge.Migrations.Rollback>();
// Register "m4g orm"
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Orm>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm>, Commands.MycroForge.Orm.Migrate>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm>, Commands.MycroForge.Orm.Rollback>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm>, Commands.MycroForge.Orm.Generate>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm.Generate>, Commands.MycroForge.Orm.Generate.Entity>();
services
.AddScoped<ISubCommandOf<Commands.MycroForge.Orm.Generate>, Commands.MycroForge.Orm.Generate.Migration>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm>, Commands.MycroForge.Orm.Link>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm.Link>, Commands.MycroForge.Orm.Link.One>();
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm.Link>, Commands.MycroForge.Orm.Link.Many>();
return services;
}

View File

@ -40,19 +40,6 @@ public sealed class Api : IFeature
return;
}
Console.WriteLine(string.Join("\n", [
$"Adding feature {FeatureName}",
"Requirements:",
" - fastapi",
" - uvicorn[standard]",
]));
await Bash.ExecuteAsync(
"source .venv/bin/activate",
"python3 -m pip install fastapi uvicorn[standard]",
"python3 -m pip freeze > requirements.txt"
);
await context.CreateFile("api/routers/hello.py", HelloRouter);
var main = await context.ReadFile("main.py");

View File

@ -0,0 +1,60 @@
namespace MycroForge.CLI.Features;
public class Git : IFeature
{
#region GitIgnore
private static readonly string[] GitIgnore =
[
"# Byte-compiled / optimized / DLL files", "__pycache__/", "*.py[cod]", "*$py.class", "# C extensions",
"*.so", "# Distribution / packaging", ".Python", "build/", "develop-eggs/", "dist/", "downloads/", "eggs/",
".eggs/", "lib/", "lib64/", "parts/", "sdist/", "var/", "wheels/", "share/python-wheels/", "*.egg-info/",
".installed.cfg", "*.egg", "MANIFEST", "# PyInstaller",
"# Usually these files are written by a python script from a template",
"# before PyInstaller builds the exe, so as to inject date/other infos into it.", "*.manifest", "*.spec",
"# Installer logs", "pip-log.txt", "pip-delete-this-directory.txt", "# Unit test / coverage reports",
"htmlcov/", ".tox/", ".nox/", ".coverage", ".coverage.*", ".cache", "nosetests.xml", "coverage.xml",
"*.cover", "*.py,cover", ".hypothesis/", ".pytest_cache/", "cover/", "# Translations", "*.mo", "*.pot",
"# Django stuff:", "*.log", "local_settings.py", "db.sqlite3", "db.sqlite3-journal", "# Flask stuff:",
"instance/", ".webassets-cache", "# Scrapy stuff:", ".scrapy", "# Sphinx documentation", "docs/_build/",
"# PyBuilder", ".pybuilder/", "target/", "# Jupyter Notebook", ".ipynb_checkpoints", "# IPython",
"profile_default/", "ipython_config.py", "# pyenv",
"# For a library or package, you might want to ignore these files since the code is",
"# intended to run in multiple environments; otherwise, check them in:", "# .python-version", "# pipenv",
"# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.",
"# However, in case of collaboration, if having platform-specific dependencies or dependencies",
"# having no cross-platform support, pipenv may install dependencies that don't work, or not",
"# install all needed dependencies.", "#Pipfile.lock", "# poetry",
"# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.",
"# This is especially recommended for binary packages to ensure reproducibility, and is more",
"# commonly ignored for libraries.",
"# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control",
"#poetry.lock", "# pdm",
"# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.",
"#pdm.lock",
"# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it",
"# in version control.", "# https://pdm.fming.dev/#use-with-ide", ".pdm.toml",
"# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm", "__pypackages__/",
"# Celery stuff", "celerybeat-schedule", "celerybeat.pid", "# SageMath parsed files", "*.sage.py",
"# Environments", ".env", ".venv", "env/", "venv/", "ENV/", "env.bak/", "venv.bak/",
"# Spyder project settings", ".spyderproject", ".spyproject", "# Rope project settings", ".ropeproject",
"# mkdocs documentation", "/site", "# mypy", ".mypy_cache/", ".dmypy.json", "dmypy.json",
"# Pyre type checker", ".pyre/", "# pytype static type analyzer", ".pytype/", "# Cython debug symbols",
"cython_debug/", "# PyCharm",
"# JetBrains specific template is maintained in a separate JetBrains.gitignore that can",
"# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore",
"# and can be added to the global gitignore or merged into this file. For a more nuclear",
"# option (not recommended) you can uncomment the following to ignore the entire idea folder.", "#.idea/"
];
#endregion
public const string FeatureName = "git";
public string Name => FeatureName;
public async Task ExecuteAsync(ProjectContext context)
{
await context.CreateFile(".gitignore", GitIgnore);
await Bash.ExecuteAsync($"git -c init.defaultBranch=main init {context.RootDirectory}");
}
}

View File

@ -64,14 +64,6 @@ public sealed class Orm : IFeature
return;
}
Console.WriteLine(string.Join("\n", [
$"Adding feature {FeatureName}",
"Requirements:",
" - asyncmy",
" - sqlalchemy",
" - alembic",
]));
await Bash.ExecuteAsync(
"source .venv/bin/activate",
"python3 -m pip install asyncmy sqlalchemy alembic",

View File

@ -1,32 +1,53 @@
// using System.CommandLine;
// using MycroForge.CLI;
// using MycroForge.CLI.Exceptions;
// using MycroForge.CLI.Extensions;
// using Microsoft.Extensions.DependencyInjection;
// using Microsoft.Extensions.Hosting;
using System.CommandLine;
using MycroForge.CLI;
using MycroForge.CLI.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using var host = Host
.CreateDefaultBuilder()
.ConfigureServices((_, services) =>
{
services
.AddServices(args)
.AddCommands();
})
.Build();
try
{
var ctx = host.Services.GetRequiredService<ProjectContext>();
await ctx.LoadConfig();
await host.Services.GetRequiredService<MycroForge.CLI.Commands.MycroForge>().InvokeAsync(args);
await ctx.SaveConfig();
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
// using MycroForge.CLI.CodeGen;
// using MycroForge.Parsing;
//
// using var host = Host
// .CreateDefaultBuilder()
// .ConfigureServices((_, services) =>
// {
// services
// .AddServices(args)
// .AddCommands();
// })
// .Build();
// var src = new Tester().Rewrite();
// Console.WriteLine(src);
//
// try
// class Tester : PythonSourceModifier
// {
// var ctx = host.Services.GetRequiredService<ProjectContext>();
// await ctx.LoadConfig();
// await host.Services.GetRequiredService<MycroForge.CLI.Commands.MycroForge>().InvokeAsync(args);
// await ctx.SaveConfig();
// public Tester() : base(File.ReadAllText("scripts/user.py"))
// {
//
// }
// catch(Exception e)
//
// public override object? VisitAssignment(PythonParser.AssignmentContext context)
// {
// Console.WriteLine(e.Message);
// Console.WriteLine(GetOriginalText(context));
// return base.VisitAssignment(context);
// }
// }
using MycroForge.CLI.CodeGen;
var src = new EntityLinker(await File.ReadAllTextAsync("scripts/user.py")).Rewrite();
// using MycroForge.CLI.CodeGen;
// var src = new EntityModifier(await File.ReadAllTextAsync("scripts/user.py")).Rewrite();
// Console.WriteLine(src);

View File

@ -2,6 +2,5 @@
public class ProjectConfig
{
public string Entrypoint { get; set; } = string.Empty;
public List<string> Features { get; set; } = new();
}