mirror of
https://github.com/Pythagora-io/gpt-pilot.git
synced 2026-01-09 21:27:53 -05:00
Add new fullstack template (react+express) and refactor templates to support options
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -9,8 +10,12 @@ from core.db.models import Specification
|
||||
from core.llm.parser import JSONParser
|
||||
from core.log import get_logger
|
||||
from core.telemetry import telemetry
|
||||
from core.templates.base import BaseProjectTemplate, NoOptions
|
||||
from core.templates.example_project import EXAMPLE_PROJECTS
|
||||
from core.templates.registry import PROJECT_TEMPLATES, ProjectTemplateEnum
|
||||
from core.templates.registry import (
|
||||
PROJECT_TEMPLATES,
|
||||
ProjectTemplateEnum,
|
||||
)
|
||||
from core.ui.base import ProjectStage
|
||||
|
||||
ARCHITECTURE_STEP_NAME = "Project architecture"
|
||||
@@ -21,6 +26,14 @@ WARN_FRAMEWORKS_URL = "https://github.com/Pythagora-io/gpt-pilot/wiki/Using-GPT-
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class AppType(str, Enum):
|
||||
WEB = "web-app"
|
||||
API = "api-service"
|
||||
MOBILE = "mobile-app"
|
||||
DESKTOP = "desktop-app"
|
||||
CLI = "cli-tool"
|
||||
|
||||
|
||||
# FIXME: all the reponse pydantic models should be strict (see config._StrictModel), also check if we
|
||||
# can disallow adding custom Python attributes to the model
|
||||
class SystemDependency(BaseModel):
|
||||
@@ -54,9 +67,9 @@ class PackageDependency(BaseModel):
|
||||
|
||||
|
||||
class Architecture(BaseModel):
|
||||
architecture: str = Field(
|
||||
None,
|
||||
description="General description of the app architecture.",
|
||||
app_type: AppType = Field(
|
||||
AppType.WEB,
|
||||
description="Type of the app to build.",
|
||||
)
|
||||
system_dependencies: list[SystemDependency] = Field(
|
||||
None,
|
||||
@@ -66,9 +79,16 @@ class Architecture(BaseModel):
|
||||
None,
|
||||
description="List of framework/language-specific packages used by the app.",
|
||||
)
|
||||
|
||||
|
||||
class TemplateSelection(BaseModel):
|
||||
architecture: str = Field(
|
||||
None,
|
||||
description="General description of the app architecture.",
|
||||
)
|
||||
template: Optional[ProjectTemplateEnum] = Field(
|
||||
None,
|
||||
description="Project template to use for the app, if any (optional, can be null).",
|
||||
description="Project template to use for the app, or null if no template is a good fit.",
|
||||
)
|
||||
|
||||
|
||||
@@ -89,23 +109,70 @@ class Architect(BaseAgent):
|
||||
await self.check_system_dependencies(spec)
|
||||
|
||||
self.next_state.specification = spec
|
||||
telemetry.set("template", spec.template)
|
||||
telemetry.set("templates", spec.templates)
|
||||
self.next_state.action = ARCHITECTURE_STEP_NAME
|
||||
return AgentResponse.done(self)
|
||||
|
||||
async def plan_architecture(self, spec: Specification):
|
||||
await self.send_message("Planning project architecture ...")
|
||||
async def select_templates(self, spec: Specification) -> dict[str, BaseProjectTemplate]:
|
||||
"""
|
||||
Select project template(s) to use based on the project description.
|
||||
|
||||
Although the Pythagora database models support multiple projects, this
|
||||
function will achoose at most one project template, as we currently don't
|
||||
have templates that could be used together in a single project.
|
||||
|
||||
:param spec: Project specification.
|
||||
:return: Dictionary of selected project templates.
|
||||
"""
|
||||
await self.send_message("Selecting starter templates ...")
|
||||
|
||||
llm = self.get_llm()
|
||||
convo = AgentConvo(self).template("technologies", templates=PROJECT_TEMPLATES).require_schema(Architecture)
|
||||
convo = (
|
||||
AgentConvo(self)
|
||||
.template(
|
||||
"select_templates",
|
||||
templates=PROJECT_TEMPLATES,
|
||||
)
|
||||
.require_schema(TemplateSelection)
|
||||
)
|
||||
tpl: TemplateSelection = await llm(convo, parser=JSONParser(TemplateSelection))
|
||||
templates = {}
|
||||
if tpl.template:
|
||||
template_class = PROJECT_TEMPLATES.get(tpl.template)
|
||||
if template_class:
|
||||
options = await self.configure_template(spec, template_class)
|
||||
templates[tpl.template] = template_class(
|
||||
options,
|
||||
self.state_manager,
|
||||
self.process_manager,
|
||||
)
|
||||
|
||||
return tpl.architecture, templates
|
||||
|
||||
async def plan_architecture(self, spec: Specification):
|
||||
await self.send_message("Planning project architecture ...")
|
||||
architecture_description, templates = await self.select_templates(spec)
|
||||
|
||||
await self.send_message("Picking technologies to use ...")
|
||||
|
||||
llm = self.get_llm()
|
||||
convo = (
|
||||
AgentConvo(self)
|
||||
.template(
|
||||
"technologies",
|
||||
templates=templates,
|
||||
architecture=architecture_description,
|
||||
)
|
||||
.require_schema(Architecture)
|
||||
)
|
||||
arch: Architecture = await llm(convo, parser=JSONParser(Architecture))
|
||||
|
||||
await self.check_compatibility(arch)
|
||||
|
||||
spec.architecture = arch.architecture
|
||||
spec.architecture = architecture_description
|
||||
spec.templates = {t.name: t.options_dict for t in templates.values()}
|
||||
spec.system_dependencies = [d.model_dump() for d in arch.system_dependencies]
|
||||
spec.package_dependencies = [d.model_dump() for d in arch.package_dependencies]
|
||||
spec.template = arch.template.value if arch.template else None
|
||||
|
||||
async def check_compatibility(self, arch: Architecture) -> bool:
|
||||
warn_system_deps = [dep.name for dep in arch.system_dependencies if dep.name.lower() in WARN_SYSTEM_DEPS]
|
||||
@@ -113,7 +180,7 @@ class Architect(BaseAgent):
|
||||
|
||||
if warn_system_deps:
|
||||
await self.ask_question(
|
||||
f"Warning: GPT Pilot doesn't officially support {', '.join(warn_system_deps)}. "
|
||||
f"Warning: Pythagora doesn't officially support {', '.join(warn_system_deps)}. "
|
||||
f"You can try to use {'it' if len(warn_system_deps) == 1 else 'them'}, but you may run into problems.",
|
||||
buttons={"continue": "Continue"},
|
||||
buttons_only=True,
|
||||
@@ -122,7 +189,7 @@ class Architect(BaseAgent):
|
||||
|
||||
if warn_package_deps:
|
||||
await self.ask_question(
|
||||
f"Warning: GPT Pilot works best with vanilla JavaScript. "
|
||||
f"Warning: Pythagora works best with vanilla JavaScript. "
|
||||
f"You can try try to use {', '.join(warn_package_deps)}, but you may run into problems. "
|
||||
f"Visit {WARN_FRAMEWORKS_URL} for more information.",
|
||||
buttons={"continue": "Continue"},
|
||||
@@ -142,8 +209,8 @@ class Architect(BaseAgent):
|
||||
spec.architecture = arch["architecture"]
|
||||
spec.system_dependencies = arch["system_dependencies"]
|
||||
spec.package_dependencies = arch["package_dependencies"]
|
||||
spec.template = arch["template"]
|
||||
telemetry.set("template", spec.template)
|
||||
spec.templates = arch["templates"]
|
||||
telemetry.set("templates", spec.templates)
|
||||
|
||||
async def check_system_dependencies(self, spec: Specification):
|
||||
"""
|
||||
@@ -157,6 +224,7 @@ class Architect(BaseAgent):
|
||||
deps = spec.system_dependencies
|
||||
|
||||
for dep in deps:
|
||||
await self.send_message(f"Checking if {dep['name']} is available ...")
|
||||
status_code, _, _ = await self.process_manager.run_command(dep["test"])
|
||||
dep["installed"] = bool(status_code == 0)
|
||||
if status_code != 0:
|
||||
@@ -174,11 +242,30 @@ class Architect(BaseAgent):
|
||||
else:
|
||||
await self.send_message(f"✅ {dep['name']} is available.")
|
||||
|
||||
telemetry.set(
|
||||
"architecture",
|
||||
{
|
||||
"description": spec.architecture,
|
||||
"system_dependencies": deps,
|
||||
"package_dependencies": spec.package_dependencies,
|
||||
},
|
||||
async def configure_template(self, spec: Specification, template_class: BaseProjectTemplate) -> BaseModel:
|
||||
"""
|
||||
Ask the LLM to configure the template options.
|
||||
|
||||
Based on the project description, the LLM should pick the options that
|
||||
make the most sense. If template has no options, the method is a no-op
|
||||
and returns an empty options model.
|
||||
|
||||
:param spec: Project specification.
|
||||
:param template_class: Template that needs to be configured.
|
||||
:return: Configured options model.
|
||||
"""
|
||||
if template_class.options_class is NoOptions:
|
||||
# If template has no options, no need to ask LLM for anything
|
||||
return NoOptions()
|
||||
|
||||
llm = self.get_llm()
|
||||
convo = (
|
||||
AgentConvo(self)
|
||||
.template(
|
||||
"configure_template",
|
||||
project_description=spec.description,
|
||||
project_template=template_class,
|
||||
)
|
||||
.require_schema(template_class.options_class)
|
||||
)
|
||||
return await llm(convo, parser=JSONParser(template_class.options_class))
|
||||
|
||||
@@ -3,6 +3,7 @@ import sys
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import jsonref
|
||||
from pydantic import BaseModel
|
||||
|
||||
from core.config import get_config
|
||||
@@ -88,6 +89,17 @@ class AgentConvo(Convo):
|
||||
return child
|
||||
|
||||
def require_schema(self, model: BaseModel) -> "AgentConvo":
|
||||
schema_txt = json.dumps(model.model_json_schema())
|
||||
self.user(f"IMPORTANT: Your response MUST conform to this JSON schema:\n```\n{schema_txt}\n```")
|
||||
def remove_defs(d):
|
||||
if isinstance(d, dict):
|
||||
return {k: remove_defs(v) for k, v in d.items() if k != "$defs"}
|
||||
elif isinstance(d, list):
|
||||
return [remove_defs(v) for v in d]
|
||||
else:
|
||||
return d
|
||||
|
||||
schema_txt = json.dumps(remove_defs(jsonref.loads(json.dumps(model.model_json_schema()))))
|
||||
self.user(
|
||||
f"IMPORTANT: Your response MUST conform to this JSON schema:\n```\n{schema_txt}\n```."
|
||||
f"YOU MUST NEVER add any additional fields to your response, and NEVER add additional preamble like 'Here is your JSON'."
|
||||
)
|
||||
return self
|
||||
|
||||
@@ -106,6 +106,7 @@ class ErrorHandler(BaseAgent):
|
||||
{
|
||||
"id": uuid4().hex,
|
||||
"user_feedback": f"Error running command: {cmd}",
|
||||
"user_feedback_qa": None,
|
||||
"description": llm_response,
|
||||
"alternative_solutions": [],
|
||||
"attempts": 1,
|
||||
|
||||
@@ -194,9 +194,9 @@ class Orchestrator(BaseAgent):
|
||||
elif (
|
||||
not state.epics
|
||||
or not self.current_state.unfinished_tasks
|
||||
or (state.specification.template and not state.files)
|
||||
or (state.specification.templates and not state.files)
|
||||
):
|
||||
# Ask the Tech Lead to break down the initial project or feature into tasks and apply project template
|
||||
# Ask the Tech Lead to break down the initial project or feature into tasks and apply project templates
|
||||
return TechLead(self.state_manager, self.ui, process_manager=self.process_manager)
|
||||
|
||||
if state.current_task and state.docs is None and state.specification.complexity != Complexity.SIMPLE:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -12,7 +11,7 @@ from core.llm.parser import JSONParser
|
||||
from core.log import get_logger
|
||||
from core.telemetry import telemetry
|
||||
from core.templates.example_project import EXAMPLE_PROJECTS
|
||||
from core.templates.registry import apply_project_template, get_template_description, get_template_summary
|
||||
from core.templates.registry import PROJECT_TEMPLATES
|
||||
from core.ui.base import ProjectStage, success_source
|
||||
|
||||
log = get_logger(__name__)
|
||||
@@ -51,9 +50,9 @@ class TechLead(BaseAgent):
|
||||
|
||||
await self.ui.send_project_stage(ProjectStage.CODING)
|
||||
|
||||
if self.current_state.specification.template and not self.current_state.files:
|
||||
await self.apply_project_template()
|
||||
self.next_state.action = "Apply project template"
|
||||
if self.current_state.specification.templates and not self.current_state.files:
|
||||
await self.apply_project_templates()
|
||||
self.next_state.action = "Apply project templates"
|
||||
return AgentResponse.done(self)
|
||||
|
||||
if self.current_state.current_epic:
|
||||
@@ -77,25 +76,39 @@ class TechLead(BaseAgent):
|
||||
}
|
||||
]
|
||||
|
||||
async def apply_project_template(self) -> Optional[str]:
|
||||
async def apply_project_templates(self):
|
||||
state = self.current_state
|
||||
summaries = []
|
||||
|
||||
# Only do this for the initial project and if the template is specified
|
||||
if len(state.epics) != 1 or not state.specification.template:
|
||||
return None
|
||||
# Only do this for the initial project and if the templates are specified
|
||||
if len(state.epics) != 1 or not state.specification.templates:
|
||||
return
|
||||
|
||||
description = get_template_description(state.specification.template)
|
||||
log.info(f"Applying project template: {state.specification.template}")
|
||||
await self.send_message(f"Applying project template {description} ...")
|
||||
summary = await apply_project_template(
|
||||
self.current_state.specification.template,
|
||||
self.state_manager,
|
||||
self.process_manager,
|
||||
)
|
||||
# Saving template files will fill this in and we want it clear for the
|
||||
# first task.
|
||||
for template_name, template_options in state.specification.templates.items():
|
||||
template_class = PROJECT_TEMPLATES.get(template_name)
|
||||
if not template_class:
|
||||
log.error(f"Project template not found: {template_name}")
|
||||
continue
|
||||
|
||||
template = template_class(
|
||||
template_options,
|
||||
self.state_manager,
|
||||
self.process_manager,
|
||||
)
|
||||
|
||||
description = template.description
|
||||
log.info(f"Applying project template: {template.name}")
|
||||
await self.send_message(f"Applying project template {description} ...")
|
||||
summary = await template.apply()
|
||||
summaries.append(summary)
|
||||
|
||||
# Saving template files will fill this in and we want it clear for the first task.
|
||||
self.next_state.relevant_files = None
|
||||
return summary
|
||||
|
||||
if summaries:
|
||||
spec = self.current_state.specification.clone()
|
||||
spec.description += "\n\n" + "\n\n".join(summaries)
|
||||
self.next_state.specification = spec
|
||||
|
||||
async def ask_for_new_feature(self) -> AgentResponse:
|
||||
if len(self.current_state.epics) > 2:
|
||||
@@ -140,7 +153,8 @@ class TechLead(BaseAgent):
|
||||
"plan",
|
||||
epic=epic,
|
||||
task_type=self.current_state.current_epic.get("source", "app"),
|
||||
existing_summary=get_template_summary(self.current_state.specification.template),
|
||||
# FIXME: we're injecting summaries to initial description
|
||||
existing_summary=None,
|
||||
)
|
||||
.require_schema(DevelopmentPlan)
|
||||
)
|
||||
|
||||
@@ -95,6 +95,7 @@ def parse_arguments() -> Namespace:
|
||||
--import-v0: Import data from a v0 (gpt-pilot) database with the given path
|
||||
--email: User's email address, if provided
|
||||
--extension-version: Version of the VSCode extension, if used
|
||||
--no-check: Disable initial LLM API check
|
||||
:return: Parsed arguments object.
|
||||
"""
|
||||
version = get_version()
|
||||
@@ -134,6 +135,7 @@ def parse_arguments() -> Namespace:
|
||||
)
|
||||
parser.add_argument("--email", help="User's email address", required=False)
|
||||
parser.add_argument("--extension-version", help="Version of the VSCode extension", required=False)
|
||||
parser.add_argument("--no-check", help="Disable initial LLM API check", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
|
||||
@@ -110,19 +110,28 @@ async def start_new_project(sm: StateManager, ui: UIBase) -> bool:
|
||||
:param ui: User interface.
|
||||
:return: True if the project was created successfully, False otherwise.
|
||||
"""
|
||||
try:
|
||||
user_input = await ui.ask_question(
|
||||
"What is the project name?",
|
||||
allow_empty=False,
|
||||
source=pythagora_source,
|
||||
)
|
||||
except (KeyboardInterrupt, UIClosedError):
|
||||
user_input = UserInput(cancelled=True)
|
||||
while True:
|
||||
try:
|
||||
user_input = await ui.ask_question(
|
||||
"What is the project name?",
|
||||
allow_empty=False,
|
||||
source=pythagora_source,
|
||||
)
|
||||
except (KeyboardInterrupt, UIClosedError):
|
||||
user_input = UserInput(cancelled=True)
|
||||
|
||||
if user_input.cancelled:
|
||||
return False
|
||||
if user_input.cancelled:
|
||||
return False
|
||||
|
||||
project_state = await sm.create_project(user_input.text)
|
||||
project_name = user_input.text.strip()
|
||||
if not project_name:
|
||||
await ui.send_message("Please choose a project name", source=pythagora_source)
|
||||
elif len(project_name) > 100:
|
||||
await ui.send_message("Please choose a shorter project name", source=pythagora_source)
|
||||
else:
|
||||
break
|
||||
|
||||
project_state = await sm.create_project(project_name)
|
||||
return project_state is not None
|
||||
|
||||
|
||||
@@ -136,12 +145,13 @@ async def run_pythagora_session(sm: StateManager, ui: UIBase, args: Namespace):
|
||||
:return: True if the application ran successfully, False otherwise.
|
||||
"""
|
||||
|
||||
if not await llm_api_check(ui):
|
||||
await ui.send_message(
|
||||
"Pythagora cannot start because the LLM API is not reachable.",
|
||||
source=pythagora_source,
|
||||
)
|
||||
return False
|
||||
if not args.no_check:
|
||||
if not await llm_api_check(ui):
|
||||
await ui.send_message(
|
||||
"Pythagora cannot start because the LLM API is not reachable.",
|
||||
source=pythagora_source,
|
||||
)
|
||||
return False
|
||||
|
||||
if args.project or args.branch or args.step:
|
||||
telemetry.set("is_continuation", True)
|
||||
|
||||
@@ -28,6 +28,7 @@ DEFAULT_IGNORE_PATHS = [
|
||||
"*.csv",
|
||||
"*.log",
|
||||
"go.sum",
|
||||
"migration_lock.toml",
|
||||
]
|
||||
IGNORE_SIZE_THRESHOLD = 50000 # 50K+ files are ignored by default
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""refactor specification.template to specification.templates
|
||||
|
||||
Revision ID: 08d71952ec2f
|
||||
Revises: ff891d366761
|
||||
Create Date: 2024-06-14 18:23:09.070736
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "08d71952ec2f"
|
||||
down_revision: Union[str, None] = "ff891d366761"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("specifications", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("templates", sa.JSON(), nullable=True))
|
||||
batch_op.drop_column("template")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("specifications", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("template", sa.VARCHAR(), nullable=True))
|
||||
batch_op.drop_column("templates")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,3 +1,4 @@
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sqlalchemy import delete, distinct, select
|
||||
@@ -29,7 +30,8 @@ class Specification(Base):
|
||||
architecture: Mapped[str] = mapped_column(default="")
|
||||
system_dependencies: Mapped[list[dict]] = mapped_column(default=list)
|
||||
package_dependencies: Mapped[list[dict]] = mapped_column(default=list)
|
||||
template: Mapped[Optional[str]] = mapped_column()
|
||||
templates: Mapped[Optional[dict]] = mapped_column()
|
||||
|
||||
complexity: Mapped[str] = mapped_column(server_default=Complexity.HARD)
|
||||
example_project: Mapped[Optional[str]] = mapped_column()
|
||||
|
||||
@@ -45,7 +47,7 @@ class Specification(Base):
|
||||
architecture=self.architecture,
|
||||
system_dependencies=self.system_dependencies,
|
||||
package_dependencies=self.package_dependencies,
|
||||
template=self.template,
|
||||
templates=deepcopy(self.templates) if self.templates else None,
|
||||
complexity=self.complexity,
|
||||
example_project=self.example_project,
|
||||
)
|
||||
|
||||
10
core/prompts/architect/configure_template.prompt
Normal file
10
core/prompts/architect/configure_template.prompt
Normal file
@@ -0,0 +1,10 @@
|
||||
You're starting a new software project. The specification provided by the client is:
|
||||
|
||||
```
|
||||
{{ project_description }}
|
||||
```
|
||||
|
||||
Based on the specification, we've decided to use the following project scaffolding/template: {{ project_template.description }}.
|
||||
|
||||
To start, we need to specify options for the project template:
|
||||
{{ project_template.options_description }}
|
||||
29
core/prompts/architect/select_templates.prompt
Normal file
29
core/prompts/architect/select_templates.prompt
Normal file
@@ -0,0 +1,29 @@
|
||||
You're designing the architecture and technical specifications for a new project.
|
||||
|
||||
To speed up the project development, you need to consider if you should use a project template or start from scratch. If you decide to use a template, you should choose the one that best fits the project requirements.
|
||||
|
||||
Here is a high level description of "{{ state.branch.project.name }}":
|
||||
```
|
||||
{{ state.specification.description }}
|
||||
```
|
||||
|
||||
You have an option to use project templates that implement standard boilerplate/scaffolding so you can start faster and be more productive. To be considered, a template must be compatible with the project requirements (it doesn't need to implement everything that will be used in the project, just a useful subset of needed technologies). You should pick one template that's the best match for this project.
|
||||
|
||||
If no project templates are a good match, don't pick any! It's better to start from scratch than to use a template that is not a good fit for the project and then spend time reworking it to fit the requirements.
|
||||
|
||||
Here are the available project templates:
|
||||
{% for template in templates.values() %}
|
||||
### {{ template.name }} ({{ template.stack }})
|
||||
{{ template.description }}
|
||||
|
||||
Contains:
|
||||
{{ template.summary }}
|
||||
{% endfor %}
|
||||
|
||||
Output your response in a valid JSON format like in this example:
|
||||
```json
|
||||
{
|
||||
"architecture": "Detailed description of the architecture of the application",
|
||||
"template": "foo" // or null if you decide not to use a project template
|
||||
}
|
||||
```
|
||||
@@ -2,33 +2,19 @@ You're designing the architecture and technical specifications for a new project
|
||||
|
||||
If the project requirements call out for specific technology, use that. Otherwise, if working on a web app, prefer Node.js for the backend (with Express if a web server is needed, and MongoDB if a database is needed), and Bootstrap for the front-end. You MUST NOT use Docker, Kubernetes, microservices and single-page app frameworks like React, Next.js, Angular, Vue or Svelte unless the project details explicitly require it.
|
||||
|
||||
Here are the details for the new project:
|
||||
-----------------------------
|
||||
{% include "partials/project_details.prompt" %}
|
||||
{% include "partials/features_list.prompt" %}
|
||||
-----------------------------
|
||||
Here is a high level description of "{{ state.branch.project.name }}":
|
||||
```
|
||||
{{ state.specification.description }}
|
||||
```
|
||||
|
||||
Based on these details, think step by step to design the architecture for the project and choose technologies to use in building it.
|
||||
Here is a short description of the project architecture:
|
||||
{{ architecture }}
|
||||
|
||||
1. First, design and describe project architecture in general terms
|
||||
2. Then, list any system dependencies that should be installed on the system prior to start of development. For each system depedency, output a {{ os }} command to check whether it's installed.
|
||||
3. Finally, list any other 3rd party packages or libraries that will be used (that will be installed later using packager a package manager in the project repository/environment).
|
||||
4. {% if templates %}Optionally, choose a project starter template.{% else %}(for this project there are no available starter/boilerplate templates, so there's no template to choose){% endif %}
|
||||
Based on these details, think step by step and choose technologies to use in building it.
|
||||
|
||||
{% if templates %}
|
||||
You have an option to use a project template that implements standard boilerplate/scaffolding so you can start faster and be more productive. To be considered, a template must be compatible with the architecture and technologies you've choosen (it doesn't need to implement everything that will be used in the project, just a useful subset). If multiple templates can be considered, pick one that's the best match.
|
||||
|
||||
If no project templates are a good match, don't pick any! It's better to start from scratch than to use a template that is not a good fit for the project and then spend time reworking it to fit the requirements.
|
||||
|
||||
Here are the available project templates:
|
||||
{% for name, tpl in templates.items() %}
|
||||
### {{ name }}
|
||||
{{ tpl.description }}
|
||||
|
||||
Contains:
|
||||
{{ tpl.summary }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
1. First, list any system dependencies that should be installed on the system prior to start of development. For each system depedency, output a {{ os }} command to check whether it's installed.
|
||||
2. Then, list any other 3rd party packages or libraries that will be used (that will be installed later using packager a package manager in the project repository/environment).
|
||||
3. Finally, list the folder structure of the project, including any key files that should be included.
|
||||
|
||||
*IMPORTANT*: You must follow these rules while creating your project:
|
||||
|
||||
@@ -40,7 +26,6 @@ Contains:
|
||||
Output only your response in JSON format like in this example, without other commentary:
|
||||
```json
|
||||
{
|
||||
"architecture": "Detailed description of the architecture of the application",
|
||||
"system_dependencies": [
|
||||
{
|
||||
"name": "Node.js",
|
||||
@@ -62,7 +47,6 @@ Output only your response in JSON format like in this example, without other com
|
||||
"description": "Express web server for Node"
|
||||
},
|
||||
...
|
||||
],
|
||||
"template": "name of the project template to use" // or null if you decide not to use a project template
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
1
core/prompts/code-monkey/iteration.prompt
Normal file
1
core/prompts/code-monkey/iteration.prompt
Normal file
@@ -0,0 +1 @@
|
||||
{% extends "troubleshooter/iteration.prompt" %}
|
||||
140
core/templates/base.py
Normal file
140
core/templates/base.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from json import loads
|
||||
from os.path import dirname, join
|
||||
from typing import TYPE_CHECKING, Any, Optional, Type
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from core.log import get_logger
|
||||
from core.templates.render import Renderer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.proc.process_manager import ProcessManager
|
||||
from core.state.state_manager import StateManager
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class NoOptions(BaseModel):
|
||||
"""
|
||||
Options class for templates that do not require any options.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BaseProjectTemplate:
|
||||
"""
|
||||
Base project template, providing a common interface for all project templates.
|
||||
"""
|
||||
|
||||
name: str
|
||||
path: str
|
||||
description: str
|
||||
options_class: Type[BaseModel]
|
||||
options_description: str
|
||||
file_descriptions: dict
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
options: BaseModel,
|
||||
state_manager: "StateManager",
|
||||
process_manager: "ProcessManager",
|
||||
):
|
||||
"""
|
||||
Create a new project template.
|
||||
|
||||
:param options: The options to use for the template.
|
||||
:param state_manager: The state manager instance to save files to.
|
||||
:param process_manager: ProcessManager instance to run the install commands.
|
||||
"""
|
||||
if isinstance(options, dict):
|
||||
options = self.options_class(**options)
|
||||
|
||||
self.options = options
|
||||
self.state_manager = state_manager
|
||||
self.process_manager = process_manager
|
||||
|
||||
self.file_renderer = Renderer(join(dirname(__file__), "tree"))
|
||||
self.info_renderer = Renderer(join(dirname(__file__), "info"))
|
||||
|
||||
def filter(self, path: str) -> Optional[str]:
|
||||
"""
|
||||
Filter a file path to be included in the rendered template.
|
||||
|
||||
The method is called for every file in the template tree before rendering.
|
||||
If the method returns None or an empty string, the file will be skipped.
|
||||
Otherwise, the file will be rendered and stored under the file name
|
||||
matching the provided filename.
|
||||
|
||||
By default (base template), this function returns the path as-is.
|
||||
|
||||
:param path: The file path to include or exclude.
|
||||
:return: The path to use, or None if the file should be skipped.
|
||||
"""
|
||||
return path
|
||||
|
||||
async def apply(self) -> Optional[str]:
|
||||
"""
|
||||
Apply a project template to a new project.
|
||||
|
||||
:param template_name: The name of the template to apply.
|
||||
:param state_manager: The state manager instance to save files to.
|
||||
:param process_manager: The process manager instance to run install hooks with.
|
||||
:return: A summary of the applied template, or None if no template was applied.
|
||||
"""
|
||||
state = self.state_manager.current_state
|
||||
project_name = state.branch.project.name
|
||||
project_folder = state.branch.project.folder_name
|
||||
project_description = state.specification.description
|
||||
|
||||
log.info(f"Applying project template {self.name} with options: {self.options_dict}")
|
||||
|
||||
files = self.file_renderer.render_tree(
|
||||
self.path,
|
||||
{
|
||||
"project_name": project_name,
|
||||
"project_folder": project_folder,
|
||||
"project_description": project_description,
|
||||
"random_secret": uuid4().hex,
|
||||
"options": self.options_dict,
|
||||
},
|
||||
self.filter,
|
||||
)
|
||||
|
||||
for file_name, file_content in files.items():
|
||||
desc = self.file_descriptions.get(file_name)
|
||||
metadata = {"description": desc} if desc else None
|
||||
await self.state_manager.save_file(
|
||||
file_name,
|
||||
file_content,
|
||||
metadata=metadata,
|
||||
from_template=True,
|
||||
)
|
||||
|
||||
try:
|
||||
await self.install_hook()
|
||||
except Exception as err:
|
||||
log.error(
|
||||
f"Error running install hook for project template '{self.name}': {err}",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return self.info_renderer.render_template(
|
||||
join(self.path, "summary.tpl"),
|
||||
{
|
||||
"description": self.description,
|
||||
"options": self.options,
|
||||
},
|
||||
)
|
||||
|
||||
async def install_hook(self):
|
||||
"""
|
||||
Command to run to complete the project scaffolding setup.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def options_dict(self) -> dict[str, Any]:
|
||||
"""Template options as a Python dictionary."""
|
||||
return loads(self.options.model_dump_json())
|
||||
@@ -18,13 +18,13 @@ Functional Specification:
|
||||
- Todos persist between sessions using the browser's local storage. The application saves any changes to the todo list (additions or state changes) in local storage and retrieves this data when the application is reloaded.
|
||||
|
||||
Technical Specification:
|
||||
- Platform/Technologies: The application is a web application developed using React. No backend technologies are required.
|
||||
- Platform/Technologies: The application is a web application developed using React on frontend and Express on the backend, using SQLite as the database.
|
||||
- Styling: Use Bootstrap 5 for a simple and functional interface. Load Boostrap from the CDN (don't install it locally):
|
||||
- https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css
|
||||
- https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js
|
||||
- State Management: Directly in the React component
|
||||
- make sure to initialize the state from the local storage as default (... = useState(JSON.parse(localStorage.getItem('todos')) || []) to avoid race conditions
|
||||
- Data Persistence: The application uses the browser's local storage to persist todos between sessions. It stores the array of todos as a JSON string and parses this data on application load.
|
||||
- Data Persistence: Using the SQLite database on the backend via a REST API.
|
||||
"""
|
||||
|
||||
EXAMPLE_PROJECT_ARCHITECTURE = {
|
||||
@@ -47,7 +47,9 @@ EXAMPLE_PROJECT_ARCHITECTURE = {
|
||||
{"name": "react-dom", "description": "Serves as the entry point to the DOM and server renderers for React."},
|
||||
{"name": "bootstrap", "description": "Frontend framework for developing responsive and mobile-first websites."},
|
||||
],
|
||||
"template": "javascript_react",
|
||||
"templates": {
|
||||
"javascript_react": {},
|
||||
},
|
||||
}
|
||||
|
||||
EXAMPLE_PROJECT_PLAN = [
|
||||
|
||||
9
core/templates/info/javascript_react/summary.tpl
Normal file
9
core/templates/info/javascript_react/summary.tpl
Normal file
@@ -0,0 +1,9 @@
|
||||
Here's what's already been implemented:
|
||||
|
||||
* React web app using Vite devserver/bundler
|
||||
* Initial setup with Vite for fast development
|
||||
* Basic project structure for React development
|
||||
* Development server setup for hot reloading
|
||||
* Minimal configuration to get started with React
|
||||
* Frontend-only, compatible with any backend stack
|
||||
|
||||
10
core/templates/info/node_express_mongoose/summary.tpl
Normal file
10
core/templates/info/node_express_mongoose/summary.tpl
Normal file
@@ -0,0 +1,10 @@
|
||||
Here's what's already been implemented:
|
||||
|
||||
* Node + Express + MongoDB web app with session-based authentication, EJS views and Bootstrap 5
|
||||
* initial Node + Express setup
|
||||
* User model in Mongoose ORM with username and password fields, ensuring username is unique and hashing passwords with bcrypt prior to saving to the database
|
||||
* session-based authentication using username + password (hashed using bcrypt) in routes/authRoutes.js, using express-session
|
||||
* authentication middleware to protect routes that require login
|
||||
* EJS view engine, html head, header and footer EJS partials, with included Boostrap 5.x CSS and JS
|
||||
* routes and EJS views for login, register, and home (main) page
|
||||
* config loading from environment using dotenv with a placeholder .env.example file: you will need to create a .env file with your own values
|
||||
39
core/templates/info/react_express/summary.tpl
Normal file
39
core/templates/info/react_express/summary.tpl
Normal file
@@ -0,0 +1,39 @@
|
||||
Here's what's already been implemented:
|
||||
|
||||
* {{ description }}
|
||||
* Frontend:
|
||||
* ReactJS based frontend in `ui/` folder using Vite devserver
|
||||
* Integrated shadcn-ui component library with Tailwind CSS framework
|
||||
* Client-side routing using `react-router-dom` with page components defined in `ui/pages/` and other components in `ui/components`
|
||||
* Implememented pages:
|
||||
* Home - home (index) page (`/`)
|
||||
{% if options.auth %}
|
||||
* Login - login page (`/login/`) - on login, stores the auth token to `token` variable in local storage
|
||||
* Register - register page (`/register/`)
|
||||
{% endif %}
|
||||
* Backend:
|
||||
* Express-based server implementing REST API endpoints in `api/`
|
||||
{% if options.db_type == "sql" %}
|
||||
* Relational (SQL) database support with Prisma ORM using SQLite as the database
|
||||
{% elif options.db_type == "nosql" %}
|
||||
* MongoDB database support with Mongoose
|
||||
{% endif %}
|
||||
{% if options.email %}
|
||||
* Email sending support using Nodemailer
|
||||
{% endif %}
|
||||
{% if options.auth %}
|
||||
* Token-based authentication (using opaque bearer tokens)
|
||||
* User authentication (email + password):
|
||||
* login/register API endpoints in `/api/routes/authRoutes.js`
|
||||
* authorization middleware in `/api/middlewares/authMiddleware.js`
|
||||
* user management logic in `/api/services/userService.js`
|
||||
{% endif %}
|
||||
* Development server:
|
||||
* Vite devserver for frontend (`npm run dev:ui` to start the Vite dev server)
|
||||
* Nodemon for backend (`npm run dev:api` to start Node.js server with Nodemon)
|
||||
* Concurrently to run both servers together with a single command (`npm run dev`) - the preferred way to start the server in development
|
||||
* Notes:
|
||||
{% if options.db_type == "sql" %}
|
||||
* Whenever a database model is changed or added in `schema.prisma`, remember to run `npx prisma format && npx prisma generate` to update the Prisma client
|
||||
* For model relationships, remember to always also add the reverse relationship in `schema.prisma` at the same time, otherwise the database migration will fail
|
||||
{% endif %}
|
||||
@@ -1,28 +1,12 @@
|
||||
from core.proc.process_manager import ProcessManager
|
||||
from .base import BaseProjectTemplate, NoOptions
|
||||
|
||||
|
||||
async def install_hook(process_manager: ProcessManager):
|
||||
"""
|
||||
Command to run to complete the project scaffolding setup.
|
||||
|
||||
:param process_manager: ProcessManager instance to run the install commands with.
|
||||
"""
|
||||
await process_manager.run_command("npm install")
|
||||
|
||||
|
||||
JAVASCRIPT_REACT = {
|
||||
"path": "javascript_react",
|
||||
"description": "React web app using Vite devserver/bundler",
|
||||
"summary": "\n".join(
|
||||
[
|
||||
"* Initial setup with Vite for fast development",
|
||||
"* Basic project structure for React development",
|
||||
"* Development server setup for hot reloading",
|
||||
"* Minimal configuration to get started with React",
|
||||
]
|
||||
),
|
||||
"install_hook": install_hook,
|
||||
"files": {
|
||||
class JavascriptReactProjectTemplate(BaseProjectTemplate):
|
||||
stack = "frontend"
|
||||
name = "javascript_react"
|
||||
path = "javascript_react"
|
||||
description = "React web app using Vite devserver/bundler"
|
||||
file_descriptions = {
|
||||
"vite.config.js": "Configuration file for Vite, a fast developer-friendly Javascript bundler/devserver.",
|
||||
"index.html": "Main entry point for the project. It includes a basic HTML structure with a root div element and a script tag importing a JavaScript file named main.jsx using the module type. References: src/main.jsx",
|
||||
".eslintrc.cjs": "Configuration file for ESLint, a static code analysis tool for identifying problematic patterns found in JavaScript code. It defines rules for linting JavaScript code with a focus on React applications.",
|
||||
@@ -34,5 +18,18 @@ JAVASCRIPT_REACT = {
|
||||
"src/App.jsx": "Defines a functional component that serves as the root component in the project. The component is exported as the default export. References: src/App.css",
|
||||
"src/main.jsx": "Main entry point for a React application. It imports necessary modules, renders the main component 'App' inside a 'React.StrictMode' component, and mounts it to the root element in the HTML document. References: App.jsx, index.css",
|
||||
"src/assets/.gitkeep": "Empty file",
|
||||
},
|
||||
}
|
||||
}
|
||||
summary = "\n".join(
|
||||
[
|
||||
"* Initial setup with Vite for fast development",
|
||||
"* Basic project structure for React development",
|
||||
"* Development server setup for hot reloading",
|
||||
"* Minimal configuration to get started with React",
|
||||
"* Frontend-only, compatible with any backend stack",
|
||||
]
|
||||
)
|
||||
options_class = NoOptions
|
||||
options_description = ""
|
||||
|
||||
async def install_hook(self):
|
||||
await self.process_manager.run_command("npm install")
|
||||
|
||||
@@ -1,31 +1,12 @@
|
||||
from core.proc.process_manager import ProcessManager
|
||||
from .base import BaseProjectTemplate, NoOptions
|
||||
|
||||
|
||||
async def install_hook(process_manager: ProcessManager):
|
||||
"""
|
||||
Command to run to complete the project scaffolding setup.
|
||||
|
||||
:param process_manager: ProcessManager instance to run the install commands with.
|
||||
"""
|
||||
await process_manager.run_command("npm install")
|
||||
|
||||
|
||||
NODE_EXPRESS_MONGOOSE = {
|
||||
"path": "node_express_mongoose",
|
||||
"description": "Node + Express + MongoDB web app with session-based authentication, EJS views and Bootstrap 5",
|
||||
"summary": "\n".join(
|
||||
[
|
||||
"* initial Node + Express setup",
|
||||
"* User model in Mongoose ORM with username and password fields, ensuring username is unique and hashing passwords with bcrypt prior to saving to the database",
|
||||
"* session-based authentication using username + password (hashed using bcrypt) in routes/authRoutes.js, using express-session",
|
||||
"* authentication middleware to protect routes that require login",
|
||||
"* EJS view engine, html head, header and footer EJS partials, with included Boostrap 5.x CSS and JS",
|
||||
"* routes and EJS views for login, register, and home (main) page",
|
||||
"* config loading from environment using dotenv with a placeholder .env.example file: you will need to create a .env file with your own values",
|
||||
]
|
||||
),
|
||||
"install_hook": install_hook,
|
||||
"files": {
|
||||
class NodeExpressMongooseProjectTemplate(BaseProjectTemplate):
|
||||
stack = "backend"
|
||||
name = "node_express_mongoose"
|
||||
path = "node_express_mongoose"
|
||||
description = "Node + Express + MongoDB web app with session-based authentication, EJS views and Bootstrap 5"
|
||||
file_descriptions = {
|
||||
".env.example": "The .env.example file serves as a template for setting up environment variables used in the application. It provides placeholders for values such as the port number, MongoDB database URL, and session secret string.",
|
||||
".env": "This file is a configuration file in the form of a .env file. It contains environment variables used by the application, such as the port to listen on, the MongoDB database URL, and the session secret string.",
|
||||
"server.js": "This `server.js` file sets up an Express server with MongoDB database connection, session management using connect-mongo, templating engine EJS, static file serving, authentication routes, error handling, and request logging. [References: dotenv, mongoose, express, express-session, connect-mongo, ./routes/authRoutes]",
|
||||
@@ -41,5 +22,20 @@ NODE_EXPRESS_MONGOOSE = {
|
||||
"models/User.js": "This file defines a Mongoose model for a user with fields for username and password. It includes a pre-save hook to hash the user's password before saving it to the database using bcrypt. [References: mongoose, bcrypt]",
|
||||
"public/js/main.js": "The main.js file is a placeholder for future JavaScript code. It currently does not contain any specific functionality.",
|
||||
"public/css/style.css": "This file is a placeholder for custom styles. It does not contain any specific styles but is intended for adding custom CSS styles.",
|
||||
},
|
||||
}
|
||||
}
|
||||
summary = "\n".join(
|
||||
[
|
||||
"* initial Node + Express setup",
|
||||
"* User model in Mongoose ORM with username and password fields, ensuring username is unique and hashing passwords with bcrypt prior to saving to the database",
|
||||
"* session-based authentication using username + password (hashed using bcrypt) in routes/authRoutes.js, using express-session",
|
||||
"* authentication middleware to protect routes that require login",
|
||||
"* EJS view engine, html head, header and footer EJS partials, with included Boostrap 5.x CSS and JS",
|
||||
"* routes and EJS views for login, register, and home (main) page",
|
||||
"* config loading from environment using dotenv with a placeholder .env.example file: you will need to create a .env file with your own values",
|
||||
]
|
||||
)
|
||||
options_class = NoOptions
|
||||
options_description = ""
|
||||
|
||||
async def install_hook(self):
|
||||
await self.process_manager.run_command("npm install")
|
||||
|
||||
122
core/templates/react_express.py
Normal file
122
core/templates/react_express.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from core.log import get_logger
|
||||
|
||||
from .base import BaseProjectTemplate
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class DatabaseType(str, Enum):
|
||||
SQL = "sql"
|
||||
NOSQL = "nosql"
|
||||
NONE = "none"
|
||||
|
||||
|
||||
class TemplateOptions(BaseModel):
|
||||
db_type: DatabaseType = Field(
|
||||
DatabaseType.NONE,
|
||||
description="Type of database to use in the project: relational/SQL (eg SQLite or Postgres), nosql (eg Mongo or Redis) or no database at all",
|
||||
)
|
||||
auth: bool = Field(
|
||||
description="Whether the app supports users and email/password authentication",
|
||||
)
|
||||
|
||||
|
||||
TEMPLATE_OPTIONS = """
|
||||
* Database Type (`db_type`): What type of database should the project use: SQL (relational database like SQLite or Postgres), NoSQL (MongoDB, Redis), or no database at all.
|
||||
* Authentication (`auth`): Does the project support users registering and authenticating (using email/password).
|
||||
"""
|
||||
|
||||
|
||||
class ReactExpressProjectTemplate(BaseProjectTemplate):
|
||||
stack = "fullstack"
|
||||
name = "react_express"
|
||||
path = "react_express"
|
||||
description = "React frontend with Node/Express REST API backend"
|
||||
file_descriptions = {
|
||||
".babelrc": "Configuration file used by Babel, a JavaScript transpiler, to define presets for transforming code. In this specific file, two presets are defined: 'env' with a target of 'node' set to 'current', and 'jest' for Jest testing framework.",
|
||||
".env": "Contains environment variables used to configure the application. It specifies the Node environment, log level, port to listen on, database provider and URL, as well as the session secret string.",
|
||||
".eslintrc.json": "Contains ESLint configuration settings for the project. It specifies the environment (browser, ES2021, Node.js, Jest), extends the ESLint recommended rules, sets parser options for ECMAScript version 12 and module source type, and defines a custom rule to flag unused variables except for 'req', 'res', and 'next' parameters.",
|
||||
".gitignore": "Specifies patterns to exclude certain files and directories from being tracked by Git version control. It helps in preventing unnecessary files from being committed to the repository.",
|
||||
"README.md": "Main README for a time-tracking web app for freelancers. The app uses React for the frontend, Node/Express for the backend, Prisma ORM, and SQLite database. It also utilizes Bootstrap for UI styling. The app allows users to register with email and password, uses opaque bearer tokens for authentication, and provides features like time tracking, saving time entries, viewing recent entries, generating reports, and exporting time entries in CSV format. The README also includes instructions for installation, development, testing, production deployment, and Docker usage.",
|
||||
"api/app.js": "Sets up an Express app for handling API routes and serving a pre-built frontend. It enables CORS, parses JSON and URL-encoded data, serves static files, and defines routes for authentication and API endpoints. Additionally, it serves the pre-built frontend from the '../dist' folder for all other routes.",
|
||||
"api/middlewares/authMiddleware.js": "Implements middleware functions for authentication and user authorization. The 'authenticateWithToken' function checks the Authorization header in the request, extracts the token, and authenticates the user using the UserService. The 'requireUser' function ensures that a user is present in the request object before allowing access to subsequent routes.",
|
||||
"api/middlewares/errorMiddleware.js": "Implements middleware functions for handling 404 and 500 errors in an Express API. The 'handle404' function is responsible for returning a 404 response when a requested resource is not found or an unsupported HTTP method is used. The 'handleError' function is used to handle errors that occur within route handlers by logging the error details and sending a 500 response.",
|
||||
"api/models/init.js": "Initializes the database client for interacting with the database.",
|
||||
"api/models/user.js": "Defines a Mongoose schema for a user in a database, including fields like email, password, token, name, creation date, last login date, and account status. It also includes methods for authenticating users with password or token, setting and regenerating passwords, and custom JSON transformation. The file exports a Mongoose model named 'User' based on the defined schema.",
|
||||
"api/routes/authRoutes.js": "Defines routes related to user authentication using Express.js. It includes endpoints for user login, registration, logout, and password management. The file imports services, middlewares, and utilities required for handling authentication logic.",
|
||||
"api/routes/index.js": "Defines the API routes using the Express framework. It creates an instance of the Express Router and exports it to be used in the main application. The routes defined in this file are expected to have a '/api/' prefix to differentiate them from UI/frontend routes.",
|
||||
"api/services/userService.js": "Implements a UserService class that provides various methods for interacting with user data in the database. It includes functions for listing users, getting a user by ID or email, updating user information, deleting users, authenticating users with password or token, creating new users, setting user passwords, and regenerating user tokens. The class utilizes the 'crypto' library for generating random UUIDs and imports functions from 'password.js' for password hashing and validation.",
|
||||
"api/utils/log.js": "Defines a logger utility using the 'pino' library for logging purposes. It sets the log level based on the environment variable 'LOG_LEVEL' or defaults to 'info' in production and 'debug' in other environments. It validates the provided log level against the available levels in 'pino' and throws an error if an invalid level is specified. The logger function creates a new logger instance with the specified name and log level.",
|
||||
"api/utils/mail.js": "Implements a utility function to send emails using nodemailer. It reads configuration options from environment variables and creates a nodemailer transporter with the specified options. The main function exported from this file is used to send emails by passing the necessary parameters like 'from', 'to', 'subject', and 'text'.",
|
||||
"api/utils/password.js": "Implements functions related to password hashing and validation using the bcrypt algorithm. It provides functions to generate a password hash, validate a password against a hash, and check the format of a hash.",
|
||||
"index.html": "The main entry point for the web application front-end. It defines the basic structure of an HTML document with a title and a root div element where the application content will be rendered. Additionally, it includes a script tag that imports the main.jsx file as a module, indicating that this file contains JavaScript code to be executed in a modular fashion.",
|
||||
"package.json": "Configuration file used for both Node.js/Express backend and React/Vite frontend define metadata about the project such as name, version, description, dependencies, devDependencies, scripts, etc. It also specifies the entry point of the application through the 'main' field.",
|
||||
"prisma/schema.prisma": "Defines the Prisma ORM schema for the project. It specifies the data source configuration, generator settings, and a 'User' model with various fields like id, email, password, token, name, createdAt, lastLoginAt, and isActive. It also includes index definitions for 'email' and 'token' fields.",
|
||||
"public/.gitkeep": "(empty file)",
|
||||
"server.js": "The main entry point for the backend. It sets up an HTTP server using Node.js's 'http' module, loads environment variables using 'dotenv', imports the main application logic from 'app.js', and initializes a logger from 'log.js'. It also handles uncaught exceptions and unhandled rejections, logging errors and closing the server accordingly. The main function starts the server on a specified port, defaulting to 3000 if not provided in the environment variables.",
|
||||
"ui/assets/.gitkeep": "(empty file)",
|
||||
"ui/index.css": "Defines main styling rules for the user interface elements. It sets the root font properties, body layout, and heading styles.",
|
||||
"ui/main.jsx": "Responsible for setting up the main UI components of the application using React and React Router. It imports necessary dependencies like React, ReactDOM, and react-router-dom. It also imports the main CSS file for styling. The file defines the main router configuration for the app, setting up the Home page to be displayed at the root path. Finally, it renders the main UI components using ReactDOM.createRoot.",
|
||||
"ui/pages/Home.css": "Defines the styling for the home page of the UI. It sets the maximum width of the root element to 1280px, centers it horizontally on the page, adds padding around it, and aligns the text in the center.",
|
||||
"ui/pages/Home.jsx": "Defines a functional component named 'Home' that gets displayed on the app home page (`/`). It imports styles from the 'Home.css' file.",
|
||||
"vite.config.js": "The 'vite.config.js' file is used to configure the Vite build tool for a project. In this specific file, the configuration is defined using the 'defineConfig' function provided by Vite. It includes the 'react' plugin from '@vitejs/plugin-react' to enable React support in the project. The configuration sets up the plugins array with the 'react' plugin initialized.",
|
||||
}
|
||||
|
||||
summary = "\n".join(
|
||||
[
|
||||
"* React-based frontend using Vite devserver",
|
||||
"* Radix/Shadcn UI components with Tailwind CSS, and React Router",
|
||||
"* Node.js/Express REST API backend",
|
||||
"* Dotenv-based configuration",
|
||||
"* Database integration - optional (MongoDB via Mongoose or SQL/relational via Prisma)",
|
||||
"* User authentication (email+password) - optional",
|
||||
]
|
||||
)
|
||||
|
||||
options_class = TemplateOptions
|
||||
options_description = TEMPLATE_OPTIONS.strip()
|
||||
|
||||
async def install_hook(self):
|
||||
await self.process_manager.run_command("npm install")
|
||||
if self.options.db_type == DatabaseType.SQL:
|
||||
await self.process_manager.run_command("npx prisma generate")
|
||||
await self.process_manager.run_command("npx prisma migrate dev --name initial")
|
||||
|
||||
def filter(self, path: str) -> Optional[str]:
|
||||
if not self.options.auth and path in [
|
||||
"api/middlewares/authMiddleware.js",
|
||||
"api/models/user.js",
|
||||
"api/routes/authRoutes.js",
|
||||
"api/services/userService.js",
|
||||
"api/utils/password.js",
|
||||
"ui/pages/Login.jsx",
|
||||
"ui/pages/Register.jsx",
|
||||
]:
|
||||
log.debug(f"Skipping {path} as auth is disabled")
|
||||
return None
|
||||
|
||||
if self.options.db_type != DatabaseType.SQL.value and path in [
|
||||
"prisma/schema.prisma",
|
||||
]:
|
||||
log.debug(f"Skipping {path} as ORM is not Prisma")
|
||||
return None
|
||||
|
||||
if self.options.db_type != DatabaseType.NOSQL.value and path in [
|
||||
"api/models/user.js",
|
||||
]:
|
||||
log.debug(f"Skipping {path} as Orm is not Mongoose")
|
||||
return None
|
||||
|
||||
if self.options.db_type == DatabaseType.NONE.value and path in [
|
||||
"api/models/init.js",
|
||||
]:
|
||||
log.debug(f"Skipping {path} as database integration is not enabled")
|
||||
return None
|
||||
|
||||
log.debug(f"Including project template file {path}")
|
||||
return path
|
||||
@@ -1,20 +1,10 @@
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from core.log import get_logger
|
||||
from core.proc.process_manager import ProcessManager
|
||||
from core.state.state_manager import StateManager
|
||||
|
||||
from .javascript_react import JAVASCRIPT_REACT
|
||||
from .node_express_mongoose import NODE_EXPRESS_MONGOOSE
|
||||
from .render import Renderer
|
||||
|
||||
PROJECT_TEMPLATES = {
|
||||
"node_express_mongoose": NODE_EXPRESS_MONGOOSE,
|
||||
"javascript_react": JAVASCRIPT_REACT,
|
||||
}
|
||||
from .javascript_react import JavascriptReactProjectTemplate
|
||||
from .node_express_mongoose import NodeExpressMongooseProjectTemplate
|
||||
from .react_express import ReactExpressProjectTemplate
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
@@ -22,87 +12,13 @@ log = get_logger(__name__)
|
||||
class ProjectTemplateEnum(str, Enum):
|
||||
"""Choices of available project templates."""
|
||||
|
||||
NODE_EXPRESS_MONGOOSE = "node_express_mongoose"
|
||||
JAVASCRIPT_REACT = "javascript_react"
|
||||
JAVASCRIPT_REACT = JavascriptReactProjectTemplate.name
|
||||
NODE_EXPRESS_MONGOOSE = NodeExpressMongooseProjectTemplate.name
|
||||
REACT_EXPRESS = ReactExpressProjectTemplate.name
|
||||
|
||||
|
||||
async def apply_project_template(
|
||||
template_name: str,
|
||||
state_manager: StateManager,
|
||||
process_manager: ProcessManager,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Apply a project template to a new project.
|
||||
|
||||
:param template_name: The name of the template to apply.
|
||||
:param state_manager: The state manager instance to save files to.
|
||||
:param process_manager: The process manager instance to run install hooks with.
|
||||
:return: A summary of the applied template, or None if no template was applied.
|
||||
"""
|
||||
if not template_name or template_name not in PROJECT_TEMPLATES:
|
||||
log.warning(f"Project template '{template_name}' not found, ignoring")
|
||||
return None
|
||||
|
||||
project_name = state_manager.current_state.branch.project.name
|
||||
project_description = state_manager.current_state.specification.description
|
||||
template = PROJECT_TEMPLATES[template_name]
|
||||
install_hook = template.get("install_hook")
|
||||
|
||||
# TODO: this could be configurable to get premium templates
|
||||
r = Renderer(os.path.join(os.path.dirname(__file__), "tpl"))
|
||||
|
||||
log.info(f"Applying project template {template_name}...")
|
||||
|
||||
files = r.render_tree(
|
||||
template["path"],
|
||||
{
|
||||
"project_name": project_name,
|
||||
"project_description": project_description,
|
||||
"random_secret": uuid4().hex,
|
||||
},
|
||||
)
|
||||
|
||||
descriptions = template.get("files", {})
|
||||
for file_name, file_content in files.items():
|
||||
desc = descriptions.get(file_name)
|
||||
metadata = {"description": desc} if desc else None
|
||||
await state_manager.save_file(file_name, file_content, metadata=metadata, from_template=True)
|
||||
|
||||
try:
|
||||
if install_hook:
|
||||
await install_hook(process_manager)
|
||||
except Exception as err:
|
||||
log.error(
|
||||
f"Error running install hook for project template '{template_name}': {err}",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return template["summary"]
|
||||
|
||||
|
||||
def get_template_summary(template_name: str) -> Optional[str]:
|
||||
"""
|
||||
Get a summary of a project template.
|
||||
|
||||
:param template_name: The name of the project template.
|
||||
:return: A summary of the template, or None if no template was found.
|
||||
"""
|
||||
if not template_name or template_name not in PROJECT_TEMPLATES:
|
||||
log.warning(f"Project template '{template_name}' not found, ignoring")
|
||||
return None
|
||||
template = PROJECT_TEMPLATES[template_name]
|
||||
return template["summary"]
|
||||
|
||||
|
||||
def get_template_description(template_name: str) -> Optional[str]:
|
||||
"""
|
||||
Get the description of a project template.
|
||||
|
||||
:param template_name: The name of the project template.
|
||||
:return: A summary of the template, or None if no template was found.
|
||||
"""
|
||||
if not template_name or template_name not in PROJECT_TEMPLATES:
|
||||
log.warning(f"Project template '{template_name}' not found, ignoring")
|
||||
return None
|
||||
template = PROJECT_TEMPLATES[template_name]
|
||||
return template["description"]
|
||||
PROJECT_TEMPLATES = {
|
||||
JavascriptReactProjectTemplate.name: JavascriptReactProjectTemplate,
|
||||
NodeExpressMongooseProjectTemplate.name: NodeExpressMongooseProjectTemplate,
|
||||
ReactExpressProjectTemplate.name: ReactExpressProjectTemplate,
|
||||
}
|
||||
|
||||
@@ -8,6 +8,16 @@ from typing import Any, Callable
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
|
||||
def escape_string(str: str) -> str:
|
||||
"""
|
||||
Escape special characters in a string
|
||||
|
||||
:param str: The string to escape
|
||||
:return: The escaped string
|
||||
"""
|
||||
return str.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
||||
|
||||
|
||||
class Renderer:
|
||||
"""
|
||||
Render a Jinja template
|
||||
@@ -40,7 +50,7 @@ class Renderer:
|
||||
keep_trailing_newline=True,
|
||||
)
|
||||
# Add filters here
|
||||
# self.jinja_env.filters["qstr"] = qstr
|
||||
self.jinja_env.filters["escape_string"] = escape_string
|
||||
|
||||
def render_template(self, template: str, context: Any) -> str:
|
||||
"""
|
||||
|
||||
13
core/templates/tree/react_express/.babelrc
Normal file
13
core/templates/tree/react_express/.babelrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"node": "current"
|
||||
}
|
||||
}
|
||||
],
|
||||
"jest"
|
||||
]
|
||||
}
|
||||
44
core/templates/tree/react_express/.env
Normal file
44
core/templates/tree/react_express/.env
Normal file
@@ -0,0 +1,44 @@
|
||||
# Node environment - "production" (default) or "development"
|
||||
NODE_ENV=development
|
||||
|
||||
# Log level to use, default "info" in production and "debug" in development
|
||||
# See https://github.com/pinojs/pino
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# Port to listen on, default 3000
|
||||
PORT=3000
|
||||
|
||||
{% if options.db_type == 'sql' %}
|
||||
# See https://www.prisma.io/docs/reference/database-reference/connection-urls#format
|
||||
# For PostgreSQL:
|
||||
# DATABASE_PROVIDER=postgresql
|
||||
# DATABASE_URL=postgresql://user:password@host/database
|
||||
# Default is SQLite:
|
||||
DATABASE_PROVIDER=sqlite
|
||||
DATABASE_URL=file:./db.sqlite
|
||||
|
||||
{% elif options.db_type == 'nosql' %}
|
||||
# See https://mongoosejs.com/docs/connections.html
|
||||
DATABASE_URL=mongodb://localhost/myDb
|
||||
|
||||
{% endif %}
|
||||
{% if options.email %}
|
||||
# E-mail sending with nodemailer; see https://nodemailer.com/smtp/#general-options
|
||||
# NODEMAILER_HOST=
|
||||
# NODEMAILER_PORT=25
|
||||
# NODEMAILER_USER=
|
||||
# NODEMAILER_PASS=
|
||||
# NODEMAILER_SECURE=false
|
||||
|
||||
{% endif %}
|
||||
{% if options.bg_tasks %}
|
||||
# URL pointing to Redis, default is redis://127.0.0.1:6379 (localhost)
|
||||
# REDIS_URL=
|
||||
|
||||
# Queue name for background tasks using bull
|
||||
# BG_TASKS_QUEUE=bg-tasks
|
||||
|
||||
{% endif %}
|
||||
# Session secret string (must be unique to your server)
|
||||
SESSION_SECRET={{ random_secret }}
|
||||
|
||||
18
core/templates/tree/react_express/.eslintrc.json
Normal file
18
core/templates/tree/react_express/.eslintrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": ["error", { "argsIgnorePattern": "(req|res|next)" }]
|
||||
}
|
||||
}
|
||||
32
core/templates/tree/react_express/.gitignore
vendored
Normal file
32
core/templates/tree/react_express/.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# SQLite databases, data files
|
||||
*.db
|
||||
*.csv
|
||||
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
|
||||
92
core/templates/tree/react_express/README.md
Normal file
92
core/templates/tree/react_express/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# {{ project_name }}
|
||||
|
||||
{{ project_description }}
|
||||
|
||||
## Quickstart
|
||||
|
||||
1. Install required packages:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Update `.env` with your settings.
|
||||
|
||||
{% if options.db_type == 'sql' %}
|
||||
3. Create initial database migration:
|
||||
|
||||
```
|
||||
npx prisma migrate dev --name initial
|
||||
```
|
||||
|
||||
When run the first time, it will also install
|
||||
`@prisma/client` and generate client code.
|
||||
|
||||
4. Run the tests:
|
||||
{% else %}
|
||||
3. Run the tests:
|
||||
{% endif %}
|
||||
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
To run the server in development mode, with log pretty-printing and hot-reload:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
To run the tests, run the `test` script (`npm run test`). ESLint is used for linting and its configuration is specified in `.eslintrc.json`.
|
||||
|
||||
Code style is automatically formatted using `prettier`. To manually run prettier, use `npm run prettier`. Better yet, integrate your editor to run it on save.
|
||||
|
||||
## Production
|
||||
|
||||
To run the app in production, run:
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Logs will be sent to the standard output in JSON format.
|
||||
{% if options.bg_tasks %}
|
||||
|
||||
## Background tasks with Bull
|
||||
|
||||
A simple task queue is built using `bull` and backed by Redis. Tasks are defined and exported in `src/tasks.js`. Call proxies are created automatically and tasks can be queued with:
|
||||
|
||||
```
|
||||
import { tasks } from "./src/utils/queue.js";
|
||||
const result = await tasks.someFunction(...);
|
||||
```
|
||||
|
||||
To run the worker(s) that will execute the queued tasks, run:
|
||||
|
||||
```
|
||||
npm run worker
|
||||
```
|
||||
{% endif %}
|
||||
|
||||
## Using Docker
|
||||
|
||||
Build the docker image with:
|
||||
|
||||
docker build -t {{ project_folder }} .
|
||||
|
||||
The default command is to start the web server (gunicorn). Run the image with `-P` docker option to expose the internal port (3000) and check the exposed port with `docker ps`:
|
||||
|
||||
docker run --env-file .env --P {{ project_folder }}
|
||||
docker ps
|
||||
|
||||
Make sure you provide the correct path to the env file (this example assumes it's located in the local directory).
|
||||
|
||||
To run a custom command using the image (for example, starting the Node
|
||||
shell):
|
||||
|
||||
docker run --env-file .env {{ project_folder }} npm run shell
|
||||
|
||||
For more information on the docker build process, see the included
|
||||
`Dockerfile`.
|
||||
39
core/templates/tree/react_express/api/app.js
Normal file
39
core/templates/tree/react_express/api/app.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import path from 'path';
|
||||
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
|
||||
{% if options.auth %}
|
||||
import authRoutes from './routes/authRoutes.js';
|
||||
import { authenticateWithToken } from './middlewares/authMiddleware.js';
|
||||
{% endif %}
|
||||
import apiRoutes from './routes/index.js';
|
||||
|
||||
// Set up Express app
|
||||
const app = express();
|
||||
|
||||
// Pretty-print JSON responses
|
||||
app.enable('json spaces');
|
||||
// We want to be consistent with URL paths, so we enable strict routing
|
||||
app.enable('strict routing');
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(cors());
|
||||
{% if options.auth %}
|
||||
|
||||
// Authentication routes
|
||||
app.use(authRoutes);
|
||||
app.use(authenticateWithToken);
|
||||
{% endif %}
|
||||
|
||||
app.use(apiRoutes);
|
||||
|
||||
app.use(express.static(path.join(import.meta.dirname, "..", "dist")));
|
||||
|
||||
// Assume all other routes are frontend and serve pre-built frontend from ../dist/ folder
|
||||
app.get(/.*/, async (req, res) => {
|
||||
res.sendFile(path.join(import.meta.dirname, "..", "dist", "index.html"));
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,29 @@
|
||||
import UserService from '../services/userService.js';
|
||||
|
||||
export const authenticateWithToken = (req, res, next) => {
|
||||
const authHeader = req.get('Authorization');
|
||||
if (authHeader) {
|
||||
const m = authHeader.match(/^(Token|Bearer) (.+)/i);
|
||||
if (m) {
|
||||
UserService.authenticateWithToken(m[2])
|
||||
.then((user) => {
|
||||
req.user = user;
|
||||
next();
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
export const requireUser = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import logger from '../utils/log.js';
|
||||
|
||||
const log = logger('api:middleware');
|
||||
|
||||
/* 404 handler for the missing API endpoints
|
||||
* Due to how Express works, we don't know if the URL or HTTP method is
|
||||
* incorrect, so we return 404 in both cases.
|
||||
*/
|
||||
export const handle404 = (req, res, next) => {
|
||||
const { method, originalUrl } = req;
|
||||
log.info({ method, originalUrl }, `Unhandled API request ${method} ${originalUrl}`);
|
||||
return res.status(404).json({ error: 'Resource not found or unsupported HTTP method' });
|
||||
};
|
||||
|
||||
/* 500 handler in case we have an error in one of our route handlers
|
||||
*/
|
||||
export const handleError = (error, req, res, next) => {
|
||||
const { method, originalUrl } = req;
|
||||
|
||||
log.error({ method, originalUrl, error }, `Error while handling ${method} ${originalUrl}`);
|
||||
res.status(500).json({ error });
|
||||
};
|
||||
31
core/templates/tree/react_express/api/models/init.js
Normal file
31
core/templates/tree/react_express/api/models/init.js
Normal file
@@ -0,0 +1,31 @@
|
||||
{% if options.db_type == 'nosql' %}
|
||||
import mongoose from 'mongoose';
|
||||
import logger from '../utils/log.js';
|
||||
|
||||
const log = logger('models');
|
||||
|
||||
const dbInit = async (options = {}) => {
|
||||
const mongoUrl = process.env.DATABASE_URL || 'mongodb://localhost/myDb';
|
||||
|
||||
try {
|
||||
await mongoose.connect(mongoUrl, options);
|
||||
log.debug(`Connected to MongoDB at ${mongoUrl}`);
|
||||
} catch (err) {
|
||||
log.fatal(`Error connecting to database ${mongoUrl}:`, err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export default dbInit;
|
||||
{% endif %}
|
||||
{% if options.db_type == 'sql' %}
|
||||
import Prisma from '@prisma/client';
|
||||
|
||||
// PrismaClient is not available when testing
|
||||
const { PrismaClient } = Prisma || {};
|
||||
const prisma = PrismaClient ? new PrismaClient() : {};
|
||||
|
||||
{% if options.auth %}
|
||||
export const User = prisma.user;
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
79
core/templates/tree/react_express/api/models/user.js
Normal file
79
core/templates/tree/react_express/api/models/user.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { randomUUID } from 'crypto';
|
||||
import isEmail from 'validator/lib/isEmail.js';
|
||||
|
||||
import { generatePasswordHash, validatePassword, isPasswordHash } from '../utils/password.js';
|
||||
|
||||
const schema = new mongoose.Schema({
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
validate: { validator: isEmail, message: 'Invalid email' },
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
validate: { validator: isPasswordHash, message: 'Invalid password hash' },
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
unique: true,
|
||||
index: true,
|
||||
default: () => randomUUID(),
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
immutable: true,
|
||||
},
|
||||
lastLoginAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
}, {
|
||||
versionKey: false,
|
||||
});
|
||||
|
||||
schema.set('toJSON', {
|
||||
/* eslint-disable */
|
||||
transform: (doc, ret, options) => {
|
||||
delete ret._id;
|
||||
delete ret.password;
|
||||
return ret;
|
||||
},
|
||||
/* eslint-enable */
|
||||
});
|
||||
|
||||
schema.statics.authenticateWithPassword = async function authenticateWithPassword(email, password) {
|
||||
const user = await this.findOne({ email }).exec();
|
||||
if (!user) return null;
|
||||
|
||||
const passwordValid = await validatePassword(password, user.password);
|
||||
if (!passwordValid) return null;
|
||||
|
||||
user.lastLoginAt = Date.now();
|
||||
const updatedUser = await user.save();
|
||||
|
||||
return updatedUser;
|
||||
};
|
||||
|
||||
schema.methods.regenerateToken = async function regenerateToken() {
|
||||
this.token = randomUUID();
|
||||
if (!this.isNew) {
|
||||
await this.save();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
const User = mongoose.model('User', schema);
|
||||
export default User;
|
||||
64
core/templates/tree/react_express/api/routes/authRoutes.js
Normal file
64
core/templates/tree/react_express/api/routes/authRoutes.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
import UserService from '../services/userService.js';
|
||||
import { requireUser } from '../middlewares/authMiddleware.js';
|
||||
import logger from '../utils/log.js';
|
||||
|
||||
const log = logger('api/routes/authRoutes');
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/api/auth/login', async (req, res) => {
|
||||
const sendError = msg => res.status(400).json({ error: msg });
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return sendError('Email and password are required');
|
||||
}
|
||||
|
||||
const user = await UserService.authenticateWithPassword(email, password);
|
||||
|
||||
if (user) {
|
||||
return res.json(user);
|
||||
} else {
|
||||
return sendError('Email or password is incorrect');
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/api/auth/login', (req, res) => res.status(405).json({ error: 'Login with POST instead' }));
|
||||
|
||||
router.post('/api/auth/register', async (req, res, next) => {
|
||||
if (req.user) {
|
||||
return res.json({ user: req.user });
|
||||
}
|
||||
try {
|
||||
const user = await UserService.createUser(req.body);
|
||||
return res.status(201).json(user);
|
||||
} catch (error) {
|
||||
log.error('Error while registering user', error);
|
||||
return res.status(400).json({ error });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/api/auth/register', (req, res) => res.status(405).json({ error: 'Register with POST instead' }));
|
||||
|
||||
router.all('/api/auth/logout', async (req, res) => {
|
||||
if (req.user) {
|
||||
await UserService.regenerateToken(req.user);
|
||||
}
|
||||
return res.status(204).send();
|
||||
});
|
||||
|
||||
router.post('/api/auth/password', requireUser, async (req, res) => {
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password) {
|
||||
return res.status(400).json({ error: 'Password is required' });
|
||||
}
|
||||
|
||||
await UserService.setPassword(req.user, password);
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
export default router;
|
||||
8
core/templates/tree/react_express/api/routes/index.js
Normal file
8
core/templates/tree/react_express/api/routes/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Define API routes here
|
||||
// All API routes must have /api/ prefix to avoid conflicts with the UI/frontend.
|
||||
|
||||
export default router;
|
||||
232
core/templates/tree/react_express/api/services/userService.js
Normal file
232
core/templates/tree/react_express/api/services/userService.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
{% set mongoose = options.db_type == 'nosql' %}
|
||||
{% if mongoose %}
|
||||
import User from '../models/user.js';
|
||||
{% else %}
|
||||
import { User } from '../models/init.js';
|
||||
{% endif %}
|
||||
import { generatePasswordHash, validatePassword } from '../utils/password.js';
|
||||
|
||||
class UserService {
|
||||
static async list() {
|
||||
try {
|
||||
{% if mongoose %}
|
||||
return User.find();
|
||||
{% else %}
|
||||
const users = await User.findMany();
|
||||
return users.map((u) => ({ ...u, password: undefined }));
|
||||
{% endif %}
|
||||
} catch (err) {
|
||||
throw `Database error while listing users: ${err}`;
|
||||
}
|
||||
}
|
||||
|
||||
static async get(id) {
|
||||
try {
|
||||
{% if mongoose %}
|
||||
return User.findOne({ _id: id }).exec();
|
||||
{% else %}
|
||||
const user = await User.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
delete user.password;
|
||||
return user;
|
||||
{% endif %}
|
||||
} catch (err) {
|
||||
throw `Database error while getting the user by their ID: ${err}`;
|
||||
}
|
||||
}
|
||||
|
||||
static async getByEmail(email) {
|
||||
try {
|
||||
{% if mongoose %}
|
||||
return User.findOne({ email }).exec();
|
||||
{% else %}
|
||||
const user = await User.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
delete user.password;
|
||||
return user;
|
||||
{% endif %}
|
||||
} catch (err) {
|
||||
throw `Database error while getting the user by their email: ${err}`;
|
||||
}
|
||||
}
|
||||
|
||||
static async update(id, data) {
|
||||
try {
|
||||
{% if mongoose %}
|
||||
return User.findOneAndUpdate({ _id: id }, data, { new: true, upsert: false });
|
||||
{% else %}
|
||||
return User.update({
|
||||
where: { id },
|
||||
}, {
|
||||
data,
|
||||
});
|
||||
{% endif %}
|
||||
} catch (err) {
|
||||
throw `Database error while updating user ${id}: ${err}`;
|
||||
}
|
||||
}
|
||||
|
||||
static async delete(id) {
|
||||
try {
|
||||
{% if mongoose %}
|
||||
const result = await User.deleteOne({ _id: id }).exec();
|
||||
return (result.deletedCount === 1);
|
||||
{% else %}
|
||||
return User.delete({
|
||||
where: { id },
|
||||
});
|
||||
{% endif %}
|
||||
} catch (err) {
|
||||
throw `Database error while deleting user ${id}: ${err}`;
|
||||
}
|
||||
}
|
||||
|
||||
static async authenticateWithPassword(email, password) {
|
||||
if (!email) throw 'Email is required';
|
||||
if (!password) throw 'Password is required';
|
||||
|
||||
try {
|
||||
{% if mongoose %}
|
||||
const user = await User.findOne({email}).exec();
|
||||
{% else %}
|
||||
const user = await User.findUnique({
|
||||
where: {email},
|
||||
});
|
||||
{% endif %}
|
||||
if (!user) return null;
|
||||
|
||||
const passwordValid = await validatePassword(password, user.password);
|
||||
if (!passwordValid) return null;
|
||||
|
||||
{% if mongoose %}
|
||||
user.lastLoginAt = Date.now();
|
||||
const updatedUser = await user.save();
|
||||
{% else %}
|
||||
user.lastLoginAt = new Date();
|
||||
const updatedUser = await User.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLoginAt: user.lastLoginAt },
|
||||
});
|
||||
|
||||
delete updatedUser.password;
|
||||
{% endif %}
|
||||
return updatedUser;
|
||||
} catch (err) {
|
||||
throw `Database error while authenticating user ${email} with password: ${err}`;
|
||||
}
|
||||
}
|
||||
|
||||
static async authenticateWithToken(token) {
|
||||
try {
|
||||
{% if mongoose %}
|
||||
return User.findOne({ token }).exec();
|
||||
{% else %}
|
||||
const user = await User.findUnique({
|
||||
where: { token },
|
||||
});
|
||||
if (!user) return null;
|
||||
|
||||
delete user.password;
|
||||
return user;
|
||||
{% endif %}
|
||||
} catch (err) {
|
||||
throw `Database error while authenticating user ${email} with token: ${err}`;
|
||||
}
|
||||
}
|
||||
|
||||
static async createUser({ email, password, name = '' }) {
|
||||
if (!email) throw 'Email is required';
|
||||
if (!password) throw 'Password is required';
|
||||
|
||||
const existingUser = await UserService.getByEmail(email);
|
||||
if (existingUser) throw 'User with this email already exists';
|
||||
|
||||
const hash = await generatePasswordHash(password);
|
||||
|
||||
try {
|
||||
{% if mongoose %}
|
||||
const user = new User({
|
||||
email,
|
||||
password: hash,
|
||||
name,
|
||||
token: randomUUID(),
|
||||
});
|
||||
|
||||
await user.save();
|
||||
{% else %}
|
||||
const data = {
|
||||
email,
|
||||
password: hash,
|
||||
name,
|
||||
token: randomUUID(),
|
||||
};
|
||||
|
||||
const user = await User.create({ data });
|
||||
|
||||
delete user.password;
|
||||
{% endif %}
|
||||
return user;
|
||||
} catch (err) {
|
||||
throw `Database error while creating new user: ${err}`;
|
||||
}
|
||||
}
|
||||
|
||||
static async setPassword(user, password) {
|
||||
if (!password) throw 'Password is required';
|
||||
user.password = await generatePasswordHash(password); // eslint-disable-line
|
||||
|
||||
try {
|
||||
{% if mongoose %}
|
||||
if (!user.isNew) {
|
||||
await user.save();
|
||||
}
|
||||
{% else %}
|
||||
if (user.id) {
|
||||
return User.update({
|
||||
where: { id: user.id },
|
||||
data: { password: user.password },
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
return user;
|
||||
} catch (err) {
|
||||
throw `Database error while setting user password: ${err}`;
|
||||
}
|
||||
}
|
||||
|
||||
static async regenerateToken(user) {
|
||||
user.token = randomUUID(); // eslint-disable-line
|
||||
|
||||
try {
|
||||
{% if mongoose %}
|
||||
if (!user.isNew) {
|
||||
await user.save();
|
||||
}
|
||||
{% else %}
|
||||
if (user.id) {
|
||||
return User.update({
|
||||
where: { id: user.id },
|
||||
data: { password: user.password },
|
||||
});
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
return user;
|
||||
} catch (err) {
|
||||
throw `Database error while generating user token: ${err}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UserService;
|
||||
13
core/templates/tree/react_express/api/utils/log.js
Normal file
13
core/templates/tree/react_express/api/utils/log.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import pino from 'pino';
|
||||
|
||||
const DEFAULT_LOG_LEVEL = process.env.NODE_ENV === "production" ? "info" : "debug";
|
||||
const level = process.env.LOG_LEVEL || DEFAULT_LOG_LEVEL;
|
||||
|
||||
if (!pino.levels.values[level]) {
|
||||
const validLevels = Object.keys(pino.levels.values).join(', ');
|
||||
throw new Error(`Log level must be one of: ${validLevels}`);
|
||||
}
|
||||
|
||||
const logger = (name) => pino({ name, level });
|
||||
|
||||
export default logger;
|
||||
33
core/templates/tree/react_express/api/utils/mail.js
Normal file
33
core/templates/tree/react_express/api/utils/mail.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/* Send mail using nodemailer
|
||||
*
|
||||
* Configure using NODEMAILER_* env variables.
|
||||
* See https://nodemailer.com/smtp/ for all options
|
||||
*
|
||||
* Send mail with:
|
||||
*
|
||||
* import transport from "./src/utils/mail.js";
|
||||
* await transport.sendMail({ from, to, subject, text });
|
||||
*
|
||||
* For all message options, see: https://nodemailer.com/message/
|
||||
*/
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
import config from "./config.js";
|
||||
|
||||
const options = {
|
||||
host: config.NODEMAILER_HOST,
|
||||
port: config.NODEMAILER_PORT,
|
||||
secure: config.NODEMAILER_SECURE,
|
||||
};
|
||||
|
||||
if (config.NODEMAILER_USER && config.NODMAILER_PASS) {
|
||||
options.auth = {
|
||||
user: config.NODEMAILER_USER,
|
||||
pass: config.NODEMAILER_PASS,
|
||||
};
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(options);
|
||||
const sendMail = transporter.sendMail.bind(transporter);
|
||||
|
||||
export default sendMail;
|
||||
38
core/templates/tree/react_express/api/utils/password.js
Normal file
38
core/templates/tree/react_express/api/utils/password.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
/**
|
||||
* Hashes the password using bcrypt algorithm
|
||||
* @param {string} password - The password to hash
|
||||
* @return {string} Password hash
|
||||
*/
|
||||
export const generatePasswordHash = async (password) => {
|
||||
const salt = await bcrypt.genSalt();
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
return hash;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the password against the hash
|
||||
* @param {string} password - The password to verify
|
||||
* @param {string} hash - Password hash to verify against
|
||||
* @return {boolean} True if the password matches the hash, false otherwise
|
||||
*/
|
||||
export const validatePassword = async (password, hash) => {
|
||||
const result = await bcrypt.compare(password, hash);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks that the hash has a valid format
|
||||
* @param {string} hash - Hash to check format for
|
||||
* @return {boolean} True if passed string seems like valid hash, false otherwise
|
||||
*/
|
||||
export const isPasswordHash = (hash) => {
|
||||
if (!hash || hash.length !== 60) return false;
|
||||
try {
|
||||
bcrypt.getRounds(hash);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
17
core/templates/tree/react_express/components.json
Normal file
17
core/templates/tree/react_express/components.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "ui/index.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
15
core/templates/tree/react_express/index.html
Normal file
15
core/templates/tree/react_express/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ project_name }}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/ui/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
10
core/templates/tree/react_express/jsconfig.json
Normal file
10
core/templates/tree/react_express/jsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./ui/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
77
core/templates/tree/react_express/package.json
Normal file
77
core/templates/tree/react_express/package.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"name": "{{ folder_name }}",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"description": "{{ project_name|escape_string}}",
|
||||
"main": "server.js",
|
||||
"author": "",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"bcrypt": "*",
|
||||
"bull": "*",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cors": "*",
|
||||
"dotenv": "*",
|
||||
"express": "*",
|
||||
"jsonschema": "*",
|
||||
{% if options.db_type == 'nosql' %}
|
||||
"mongoose": "*",
|
||||
"validator": "*",
|
||||
{% endif %}
|
||||
"lucide-react": "^0.395.0",
|
||||
"nodemailer": "*",
|
||||
"pino": "*",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prisma/client": "*",
|
||||
"@types/node": "^20.14.6",
|
||||
"@types/react": "^18.2.64",
|
||||
"@types/react-dom": "^18.2.21",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"babel": "*",
|
||||
"babel-preset-env": "*",
|
||||
"babel-preset-jest": "*",
|
||||
"concurrently": "*",
|
||||
"eslint": "*",
|
||||
"eslint-config-airbnb-base": "*",
|
||||
"eslint-plugin-import": "*",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"jest": "*",
|
||||
"nodemon": "*",
|
||||
"pino-pretty": "*",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "*",
|
||||
{% if options.db_type == 'sql' %}
|
||||
"prisma": "*",
|
||||
{% endif %}
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "*",
|
||||
"supertest": "*",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"vite": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"start:api": "node server.js",
|
||||
"dev:api": "nodemon -w api -w .env -w server.js server | pino-pretty -clt -i 'hostname,pid'",
|
||||
"lint:api": "eslint .",
|
||||
"prettier:api": "prettier -w .",
|
||||
"test:api": "jest --roots test --verbose",
|
||||
"coverage:api": "jest --roots test --verbose --coverage",
|
||||
"watch-test:api": "jest --roots test --verbose --watch",
|
||||
"dev:ui": "vite",
|
||||
"build:ui": "vite build",
|
||||
"lint:ui": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview:ui": "vite preview",
|
||||
"dev": "concurrently -n api,ui \"npm:dev:api\" \"npm:dev:ui\""
|
||||
}
|
||||
}
|
||||
6
core/templates/tree/react_express/postcss.config.js
Normal file
6
core/templates/tree/react_express/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
27
core/templates/tree/react_express/prisma/schema.prisma
Normal file
27
core/templates/tree/react_express/prisma/schema.prisma
Normal file
@@ -0,0 +1,27 @@
|
||||
// Prisma schema file
|
||||
// See https://www.prisma.io/docs/concepts/components/prisma-schema
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
{% if options.auth %}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
password String
|
||||
token String @unique
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
lastLoginAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
|
||||
@@index([email])
|
||||
@@index([token])
|
||||
}
|
||||
{% endif %}
|
||||
0
core/templates/tree/react_express/public/.gitkeep
Normal file
0
core/templates/tree/react_express/public/.gitkeep
Normal file
34
core/templates/tree/react_express/server.js
Normal file
34
core/templates/tree/react_express/server.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import http from 'http';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
import app from './api/app.js';
|
||||
import logger from './api/utils/log.js';
|
||||
{% if options.db_type == 'nosql' %}
|
||||
import mongoInit from './api/models/init.js';
|
||||
{% endif %}
|
||||
|
||||
const log = logger('server');
|
||||
const server = http.createServer(app);
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
log.fatal({ err }, `Unhandled error ${err}`);
|
||||
server.close();
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
log.error(`Unhandled error (in promise): ${reason}`);
|
||||
});
|
||||
|
||||
// Main entry point to the application
|
||||
const main = async () => {
|
||||
{% if options.db_type == 'nosql' %}
|
||||
await mongoInit();
|
||||
{% endif %}
|
||||
const port = parseInt(process.env.PORT) || 3000;
|
||||
log.info(`Listening on http://localhost:${port}/`);
|
||||
await server.listen(port);
|
||||
};
|
||||
|
||||
main();
|
||||
76
core/templates/tree/react_express/tailwind.config.js
Normal file
76
core/templates/tree/react_express/tailwind.config.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./ui/main.jsx',
|
||||
'./ui/pages/**/*.{js,jsx}',
|
||||
'./ui/components/**/*.{js,jsx}',
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
||||
10
core/templates/tree/react_express/tsconfig.json
Normal file
10
core/templates/tree/react_express/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./ui/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
11
core/templates/tree/react_express/ui/components/ui/alert.jsx
Normal file
11
core/templates/tree/react_express/ui/components/ui/alert.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
|
||||
export function AlertDestructive({ title, description }) {
|
||||
return (
|
||||
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
|
||||
<p className="font-bold"><AlertCircle className="inline-block mr-2 h-5 w-5" />{title}</p>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
(<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
50
core/templates/tree/react_express/ui/components/ui/card.jsx
Normal file
50
core/templates/tree/react_express/ui/components/ui/card.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
|
||||
{...props} />
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
19
core/templates/tree/react_express/ui/components/ui/input.jsx
Normal file
19
core/templates/tree/react_express/ui/components/ui/input.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
(<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
16
core/templates/tree/react_express/ui/components/ui/label.jsx
Normal file
16
core/templates/tree/react_express/ui/components/ui/label.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
82
core/templates/tree/react_express/ui/index.css
Normal file
82
core/templates/tree/react_express/ui/index.css
Normal file
@@ -0,0 +1,82 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 100% 50%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 215 20.2% 65.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71% 4%;
|
||||
--foreground: 213 31% 91%;
|
||||
|
||||
--muted: 223 47% 11%;
|
||||
--muted-foreground: 215.4 16.3% 56.9%;
|
||||
|
||||
--accent: 216 34% 17%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 224 71% 4%;
|
||||
--popover-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--border: 216 34% 17%;
|
||||
--input: 216 34% 17%;
|
||||
|
||||
--card: 224 71% 4%;
|
||||
--card-foreground: 213 31% 91%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 1.2%;
|
||||
|
||||
--secondary: 222.2 47.4% 11.2%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 63% 31%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--ring: 216 34% 17%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
}
|
||||
|
||||
6
core/templates/tree/react_express/ui/lib/utils.js
Normal file
6
core/templates/tree/react_express/ui/lib/utils.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
53
core/templates/tree/react_express/ui/main.jsx
Normal file
53
core/templates/tree/react_express/ui/main.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { createBrowserRouter, RouterProvider, useLocation } from "react-router-dom"
|
||||
|
||||
import './index.css'
|
||||
|
||||
// Pages in the app
|
||||
import Home from './pages/Home.jsx'
|
||||
{% if options.auth %}
|
||||
import Register from './pages/Register.jsx'
|
||||
import Login from './pages/Login.jsx'
|
||||
{% endif %}
|
||||
|
||||
function PageNotFound() {
|
||||
const { pathname } = useLocation()
|
||||
return (
|
||||
<div className="w-full h-screen flex flex-col items-center justify-center px-4">
|
||||
<h1 className="font-bold">Page Not Found</h1>
|
||||
<p className="text-center">
|
||||
Page <code>{pathname}</code> does not exist.
|
||||
<br />
|
||||
<a href="/" className="underline">Go home</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <Home />,
|
||||
},
|
||||
{% if options.auth %}
|
||||
{
|
||||
path: "/register/",
|
||||
element: <Register />,
|
||||
},
|
||||
{
|
||||
path: "/login/",
|
||||
element: <Login />,
|
||||
},
|
||||
{% endif %}
|
||||
{
|
||||
path: "*",
|
||||
element: <PageNotFound />,
|
||||
}
|
||||
])
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
6
core/templates/tree/react_express/ui/pages/Home.css
Normal file
6
core/templates/tree/react_express/ui/pages/Home.css
Normal file
@@ -0,0 +1,6 @@
|
||||
#homePage {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
11
core/templates/tree/react_express/ui/pages/Home.jsx
Normal file
11
core/templates/tree/react_express/ui/pages/Home.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import './Home.css'
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<div id="homePage">
|
||||
<h1>{{ project_name }}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
98
core/templates/tree/react_express/ui/pages/Login.jsx
Normal file
98
core/templates/tree/react_express/ui/pages/Login.jsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AlertDestructive } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export default function Login() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setLoading('');
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login', { email, password });
|
||||
localStorage.setItem('token', response.data.token); // Save token to local storage
|
||||
navigate('/'); // Redirect to Home
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
setError(error.response?.data?.error || 'An unexpected error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="loginPage" className="w-full h-screen flex items-center justify-center px-4">
|
||||
<Card className="mx-auto max-w-sm">
|
||||
{error && <AlertDestructive title="Login Error" description={error} />}
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
{false && <a href="#" className="ml-auto inline-block text-sm underline">
|
||||
Forgot your password?
|
||||
</a>}
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
'Login'
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<a href="/register/" className="underline">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
96
core/templates/tree/react_express/ui/pages/Register.jsx
Normal file
96
core/templates/tree/react_express/ui/pages/Register.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AlertDestructive } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export default function Register() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const response = await axios.post('/api/auth/register', { email, password });
|
||||
if (response.data) {
|
||||
navigate('/login');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
setError(error.response?.data?.error || 'An unexpected error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="registerPage" className="w-full h-screen flex items-center justify-center px-4">
|
||||
<Card className="mx-auto max-w-sm">
|
||||
{error && <AlertDestructive title="Registration Error" description={error} />}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Sign Up</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your information to create an account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
'Create an account'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Already have an account?{" "}
|
||||
<a href="/login" className="underline">
|
||||
Sign in
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
core/templates/tree/react_express/vite.config.js
Normal file
21
core/templates/tree/react_express/vite.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./ui"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2,7 +2,7 @@ from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from core.agents.architect import Architect, Architecture, PackageDependency, SystemDependency
|
||||
from core.agents.architect import Architect, Architecture, PackageDependency, SystemDependency, TemplateSelection
|
||||
from core.agents.response import ResponseType
|
||||
from core.ui.base import UserInput
|
||||
|
||||
@@ -16,28 +16,32 @@ async def test_run(agentcontext):
|
||||
|
||||
arch = Architect(sm, ui, process_manager=pm)
|
||||
arch.get_llm = mock_get_llm(
|
||||
return_value=Architecture(
|
||||
architecture="dummy arch",
|
||||
system_dependencies=[
|
||||
SystemDependency(
|
||||
name="docker",
|
||||
description="Docker is a containerization platform.",
|
||||
test="docker --version",
|
||||
required_locally=True,
|
||||
)
|
||||
],
|
||||
package_dependencies=[
|
||||
PackageDependency(
|
||||
name="express",
|
||||
description="Express is a Node.js framework.",
|
||||
)
|
||||
],
|
||||
template="javascript_react",
|
||||
)
|
||||
side_effect=[
|
||||
TemplateSelection(
|
||||
architecture="dummy arch",
|
||||
template="javascript_react",
|
||||
),
|
||||
Architecture(
|
||||
system_dependencies=[
|
||||
SystemDependency(
|
||||
name="docker",
|
||||
description="Docker is a containerization platform.",
|
||||
test="docker --version",
|
||||
required_locally=True,
|
||||
)
|
||||
],
|
||||
package_dependencies=[
|
||||
PackageDependency(
|
||||
name="express",
|
||||
description="Express is a Node.js framework.",
|
||||
)
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
response = await arch.run()
|
||||
|
||||
arch.get_llm.return_value.assert_awaited_once()
|
||||
arch.get_llm.return_value.assert_awaited()
|
||||
ui.ask_question.assert_awaited_once()
|
||||
pm.run_command.assert_awaited_once_with("docker --version")
|
||||
|
||||
@@ -48,4 +52,4 @@ async def test_run(agentcontext):
|
||||
assert sm.current_state.specification.architecture == "dummy arch"
|
||||
assert sm.current_state.specification.system_dependencies[0]["name"] == "docker"
|
||||
assert sm.current_state.specification.package_dependencies[0]["name"] == "express"
|
||||
assert sm.current_state.specification.template == "javascript_react"
|
||||
assert "javascript_react" in sm.current_state.specification.templates
|
||||
|
||||
@@ -31,7 +31,7 @@ async def test_create_initial_epic(agentcontext):
|
||||
async def test_apply_project_template(agentcontext):
|
||||
sm, _, ui, _ = agentcontext
|
||||
|
||||
sm.current_state.specification.template = "javascript_react"
|
||||
sm.current_state.specification.templates = {"javascript_react": {}}
|
||||
sm.current_state.epics = [{"name": "Initial Project"}]
|
||||
|
||||
await sm.commit()
|
||||
|
||||
@@ -56,6 +56,7 @@ def test_parse_arguments(mock_ArgumentParser):
|
||||
"--import-v0",
|
||||
"--email",
|
||||
"--extension-version",
|
||||
"--no-check",
|
||||
}
|
||||
|
||||
parser.parse_args.assert_called_once_with()
|
||||
|
||||
@@ -3,7 +3,56 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from core.state.state_manager import StateManager
|
||||
from core.templates.registry import apply_project_template
|
||||
from core.templates.registry import PROJECT_TEMPLATES
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("core.state.state_manager.get_config")
|
||||
async def test_render_react_express_sql(mock_get_config, testmanager):
|
||||
mock_get_config.return_value.fs.type = "memory"
|
||||
sm = StateManager(testmanager)
|
||||
pm = MagicMock(run_command=AsyncMock())
|
||||
|
||||
await sm.create_project("TestProjectName")
|
||||
await sm.commit()
|
||||
|
||||
TemplateClass = PROJECT_TEMPLATES["react_express"]
|
||||
options = TemplateClass.options_class(db_type="sql", auth=True)
|
||||
template = TemplateClass(options, sm, pm)
|
||||
|
||||
assert template.options_dict == {"db_type": "sql", "auth": True}
|
||||
|
||||
await template.apply()
|
||||
|
||||
files = sm.file_system.list()
|
||||
for f in ["server.js", "index.html", "prisma/schema.prisma", "api/routes/authRoutes.js", "ui/pages/Register.jsx"]:
|
||||
assert f in files
|
||||
assert "api/models/user.js" not in files
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("core.state.state_manager.get_config")
|
||||
async def test_render_react_express_nosql(mock_get_config, testmanager):
|
||||
mock_get_config.return_value.fs.type = "memory"
|
||||
sm = StateManager(testmanager)
|
||||
pm = MagicMock(run_command=AsyncMock())
|
||||
|
||||
await sm.create_project("TestProjectName")
|
||||
await sm.commit()
|
||||
|
||||
TemplateClass = PROJECT_TEMPLATES["react_express"]
|
||||
options = TemplateClass.options_class(db_type="nosql", auth=True)
|
||||
template = TemplateClass(options, sm, pm)
|
||||
|
||||
assert template.options_dict == {"db_type": "nosql", "auth": True}
|
||||
|
||||
await template.apply()
|
||||
|
||||
files = sm.file_system.list()
|
||||
print(files)
|
||||
for f in ["server.js", "index.html", "api/models/user.js", "api/routes/authRoutes.js", "ui/pages/Register.jsx"]:
|
||||
assert f in files
|
||||
assert "prisma/schema.prisma" not in files
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -16,19 +65,16 @@ async def test_render_javascript_react(mock_get_config, testmanager):
|
||||
await sm.create_project("TestProjectName")
|
||||
await sm.commit()
|
||||
|
||||
summary = await apply_project_template("javascript_react", sm, pm)
|
||||
sm.next_state.specification.description = summary
|
||||
await sm.commit()
|
||||
TemplateClass = PROJECT_TEMPLATES["javascript_react"]
|
||||
template = TemplateClass(TemplateClass.options_class(), sm, pm)
|
||||
|
||||
files = [f.path for f in sm.current_state.files]
|
||||
assert "React" in sm.current_state.specification.description
|
||||
assert "package.json" in files
|
||||
assert template.options_dict == {}
|
||||
|
||||
package_json = await sm.get_file_by_path("package.json")
|
||||
assert package_json is not None
|
||||
assert "TestProjectName" in package_json.content.content
|
||||
await template.apply()
|
||||
|
||||
pm.run_command.assert_awaited_once_with("npm install")
|
||||
files = sm.file_system.list()
|
||||
for f in ["src/App.jsx", "vite.config.js"]:
|
||||
assert f in files
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -41,16 +87,13 @@ async def test_render_node_express_mongoose(mock_get_config, testmanager):
|
||||
await sm.create_project("TestProjectName")
|
||||
await sm.commit()
|
||||
|
||||
summary = await apply_project_template("node_express_mongoose", sm, pm)
|
||||
sm.next_state.specification.description = summary
|
||||
await sm.commit()
|
||||
TemplateClass = PROJECT_TEMPLATES["node_express_mongoose"]
|
||||
template = TemplateClass(TemplateClass.options_class(), sm, pm)
|
||||
|
||||
files = [f.path for f in sm.current_state.files]
|
||||
assert "Mongoose" in sm.current_state.specification.description
|
||||
assert "server.js" in files
|
||||
assert template.options_dict == {}
|
||||
|
||||
package_json = await sm.get_file_by_path("package.json")
|
||||
assert package_json is not None
|
||||
assert "TestProjectName" in package_json.content.content
|
||||
await template.apply()
|
||||
|
||||
pm.run_command.assert_awaited_once_with("npm install")
|
||||
files = sm.file_system.list()
|
||||
for f in ["server.js", "models/User.js"]:
|
||||
assert f in files
|
||||
|
||||
Reference in New Issue
Block a user