simple-module-cli 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. simple_module_cli/__init__.py +0 -0
  2. simple_module_cli/_env.py +12 -0
  3. simple_module_cli/app_project.py +123 -0
  4. simple_module_cli/case.py +30 -0
  5. simple_module_cli/catalog.py +107 -0
  6. simple_module_cli/cli.py +104 -0
  7. simple_module_cli/new.py +124 -0
  8. simple_module_cli/plugins.py +56 -0
  9. simple_module_cli/recipes.py +93 -0
  10. simple_module_cli/scaffolding.py +124 -0
  11. simple_module_cli/templates/host/.env.example +20 -0
  12. simple_module_cli/templates/host/.gitignore +19 -0
  13. simple_module_cli/templates/host/Makefile +24 -0
  14. simple_module_cli/templates/host/README.md.tpl +59 -0
  15. simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +12 -0
  16. simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +60 -0
  17. simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +17 -0
  18. simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +37 -0
  19. simple_module_cli/templates/host/alembic.ini +36 -0
  20. simple_module_cli/templates/host/client_app/app.tsx +16 -0
  21. simple_module_cli/templates/host/client_app/main.tsx +2 -0
  22. simple_module_cli/templates/host/client_app/package.json.tpl +23 -0
  23. simple_module_cli/templates/host/client_app/pages/Error.tsx +13 -0
  24. simple_module_cli/templates/host/client_app/pages.ts +47 -0
  25. simple_module_cli/templates/host/client_app/styles.css +7 -0
  26. simple_module_cli/templates/host/client_app/tsconfig.json +16 -0
  27. simple_module_cli/templates/host/client_app/vite.config.ts +39 -0
  28. simple_module_cli/templates/host/main.py +27 -0
  29. simple_module_cli/templates/host/migrations/env.py +80 -0
  30. simple_module_cli/templates/host/migrations/script.py.mako +26 -0
  31. simple_module_cli/templates/host/migrations/versions/.gitkeep +1 -0
  32. simple_module_cli/templates/host/pyproject.toml.tpl +17 -0
  33. simple_module_cli/templates/host/templates/index.html +12 -0
  34. simple_module_cli/templates/module/.github/workflows/ci.yml +32 -0
  35. simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +52 -0
  36. simple_module_cli/templates/module/.gitignore +14 -0
  37. simple_module_cli/templates/module/README.md.tpl +82 -0
  38. simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
  39. simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
  40. simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +11 -0
  41. simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +46 -0
  42. simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +1 -0
  43. simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +22 -0
  44. simple_module_cli/templates/module/package.json.tpl +16 -0
  45. simple_module_cli/templates/module/pyproject.toml.tpl +39 -0
  46. simple_module_cli/templates/module/tests/__init__.py +0 -0
  47. simple_module_cli/templates/module/tests/test_module.py.tpl +27 -0
  48. simple_module_cli/templates/module/tsconfig.json.tpl +11 -0
  49. simple_module_cli/wizard.py +48 -0
  50. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/.env.example +20 -0
  51. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/.gitignore +19 -0
  52. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/Makefile +24 -0
  53. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/README.md.tpl +59 -0
  54. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +12 -0
  55. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +60 -0
  56. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +17 -0
  57. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +37 -0
  58. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/alembic.ini +36 -0
  59. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/app.tsx +16 -0
  60. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/main.tsx +2 -0
  61. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/package.json.tpl +23 -0
  62. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/pages/Error.tsx +13 -0
  63. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/pages.ts +47 -0
  64. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/styles.css +7 -0
  65. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/tsconfig.json +16 -0
  66. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/client_app/vite.config.ts +39 -0
  67. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/main.py +27 -0
  68. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/migrations/env.py +80 -0
  69. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/migrations/script.py.mako +26 -0
  70. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/migrations/versions/.gitkeep +1 -0
  71. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/pyproject.toml.tpl +17 -0
  72. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/host/templates/index.html +12 -0
  73. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/.github/workflows/ci.yml +32 -0
  74. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +52 -0
  75. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/.gitignore +14 -0
  76. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/README.md.tpl +82 -0
  77. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
  78. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
  79. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +11 -0
  80. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +46 -0
  81. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +1 -0
  82. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +22 -0
  83. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/package.json.tpl +16 -0
  84. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/pyproject.toml.tpl +39 -0
  85. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/tests/__init__.py +0 -0
  86. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/tests/test_module.py.tpl +27 -0
  87. simple_module_cli-0.0.2.data/data/simple_module_cli/templates/module/tsconfig.json.tpl +11 -0
  88. simple_module_cli-0.0.2.dist-info/METADATA +63 -0
  89. simple_module_cli-0.0.2.dist-info/RECORD +92 -0
  90. simple_module_cli-0.0.2.dist-info/WHEEL +4 -0
  91. simple_module_cli-0.0.2.dist-info/entry_points.txt +3 -0
  92. simple_module_cli-0.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,124 @@
1
+ """Host + module scaffolding via package-data templates.
2
+
3
+ * :func:`create_host` materializes a new host project from the templates
4
+ under ``simple_module/templates/host/``.
5
+ * :func:`create_module` materializes a new module package from
6
+ ``simple_module/templates/module/``.
7
+
8
+ The frontend pages manifest + per-module JS dep discovery live in
9
+ :mod:`simple_module_hosting.manifest` (those need module-discovery and
10
+ stay in hosting).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import importlib.resources
16
+ import logging
17
+ import shutil
18
+ from collections.abc import Mapping, Sequence
19
+ from pathlib import Path
20
+
21
+ from simple_module_cli.case import to_kebab_case, to_pascal_case, to_snake_case
22
+
23
+ __all__ = ["create_host", "create_module"]
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ _TEMPLATES_PACKAGE = "simple_module_cli.templates"
28
+ _PACKAGE_PATH_TOKEN = "__PACKAGE__"
29
+
30
+
31
+ def _module_to_pypi_name(name: str) -> str:
32
+ return f"simple_module_{name.lower()}"
33
+
34
+
35
+ def _iter_template_files(template_root: Path):
36
+ """Yield every file under ``template_root``. Skips ``_optional/`` paths."""
37
+ for path in template_root.rglob("*"):
38
+ if not path.is_file():
39
+ continue
40
+ if "_optional" in path.relative_to(template_root).parts:
41
+ continue
42
+ yield path
43
+
44
+
45
+ def _require_empty_dest(dest: Path) -> None:
46
+ if dest.exists() and any(dest.iterdir()):
47
+ raise FileExistsError(
48
+ f"Destination {dest} already exists and is non-empty. "
49
+ "Choose a new path or remove the contents first."
50
+ )
51
+ dest.mkdir(parents=True, exist_ok=True)
52
+
53
+
54
+ def _resolve_template_root(subdir: str, override: Path | None) -> Path:
55
+ if override is not None:
56
+ return Path(override)
57
+ return Path(str(importlib.resources.files(_TEMPLATES_PACKAGE) / subdir))
58
+
59
+
60
+ def _apply_template_files(
61
+ src_root: Path,
62
+ dest: Path,
63
+ substitutions: Mapping[str, str],
64
+ *,
65
+ path_rewrites: Mapping[str, str] | None = None,
66
+ ) -> None:
67
+ for src in _iter_template_files(src_root):
68
+ rel_str = str(src.relative_to(src_root))
69
+ for old, new in (path_rewrites or {}).items():
70
+ rel_str = rel_str.replace(old, new)
71
+ rel_str = rel_str.removesuffix(".tpl")
72
+ target = dest / rel_str
73
+ target.parent.mkdir(parents=True, exist_ok=True)
74
+ if src.suffix == ".tpl":
75
+ text = src.read_text(encoding="utf-8")
76
+ for placeholder, value in substitutions.items():
77
+ text = text.replace(placeholder, value)
78
+ target.write_text(text, encoding="utf-8")
79
+ else:
80
+ shutil.copy2(src, target)
81
+
82
+
83
+ def create_host(
84
+ dest: Path,
85
+ name: str,
86
+ modules: Sequence[str],
87
+ template_root: Path | None = None,
88
+ ) -> Path:
89
+ dest = Path(dest)
90
+ _require_empty_dest(dest)
91
+ module_dep_lines = "\n".join(f' "{_module_to_pypi_name(m)}>=0.1,<1.0",' for m in modules)
92
+ _apply_template_files(
93
+ _resolve_template_root("host", template_root),
94
+ dest,
95
+ {"{{HOST_NAME}}": name, "{{MODULE_DEPS}}": module_dep_lines},
96
+ )
97
+ logger.info(
98
+ "Scaffolded host '%s' at %s (modules: %s)", name, dest, ", ".join(modules) or "<none>"
99
+ )
100
+ return dest
101
+
102
+
103
+ def create_module(
104
+ dest: Path,
105
+ name: str,
106
+ template_root: Path | None = None,
107
+ ) -> Path:
108
+ dest = Path(dest)
109
+ _require_empty_dest(dest)
110
+ display_name = to_pascal_case(name)
111
+ slug = to_kebab_case(name)
112
+ package_name = to_snake_case(name)
113
+ _apply_template_files(
114
+ _resolve_template_root("module", template_root),
115
+ dest,
116
+ substitutions={
117
+ "{{MODULE_NAME}}": display_name,
118
+ "{{MODULE_SLUG}}": slug,
119
+ "{{PACKAGE_NAME}}": package_name,
120
+ },
121
+ path_rewrites={_PACKAGE_PATH_TOKEN: package_name},
122
+ )
123
+ logger.info("Scaffolded module '%s' at %s (package: %s)", display_name, dest, package_name)
124
+ return dest
@@ -0,0 +1,20 @@
1
+ # Database — defaults to a local SQLite file.
2
+ # For PostgreSQL use: postgresql+asyncpg://user:pass@host:5432/dbname
3
+ SM_DATABASE_URL=sqlite+aiosqlite:///./app.db
4
+
5
+ # Environment: development | production
6
+ SM_ENVIRONMENT=development
7
+
8
+ # Secret key for session middleware — change before deploying.
9
+ SM_SECRET_KEY=change-me-in-production
10
+
11
+ # Vite dev server URL (only used in development).
12
+ SM_VITE_DEV_URL=http://localhost:5050
13
+
14
+ # Optional: JSON array to restrict which installed modules load at boot.
15
+ # SM_MODULES_ENABLED=["Auth","Products"]
16
+
17
+ # First-boot admin seed (optional). Only applied when the users table is empty.
18
+ # Leave unset and use `uv run sm-users create-admin` instead if you prefer.
19
+ # SM_USERS_BOOTSTRAP_EMAIL=admin@example.com
20
+ # SM_USERS_BOOTSTRAP_PASSWORD=changeme
@@ -0,0 +1,19 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+
6
+ uv.lock
7
+
8
+ node_modules/
9
+
10
+ .env
11
+
12
+ *.db
13
+ *.sqlite3
14
+
15
+ static/dist/
16
+
17
+ # Auto-generated by the host at boot / `sm gen-pages`.
18
+ client_app/modules.manifest.json
19
+ client_app/modules.generated.ts
@@ -0,0 +1,24 @@
1
+ .PHONY: install dev dev-api dev-ui build migrate gen-pages
2
+
3
+ install:
4
+ uv sync
5
+ cd client_app && npm install
6
+
7
+ dev: gen-pages
8
+ @echo "Starting API and UI dev servers..."
9
+ $(MAKE) -j2 dev-api dev-ui
10
+
11
+ dev-api:
12
+ uv run uvicorn main:app --reload --port 8000
13
+
14
+ dev-ui:
15
+ cd client_app && npm run dev
16
+
17
+ build:
18
+ cd client_app && npm run build
19
+
20
+ migrate:
21
+ uv run alembic upgrade head
22
+
23
+ gen-pages:
24
+ uv run sm gen-pages --host-dir=client_app
@@ -0,0 +1,59 @@
1
+ # {{HOST_NAME}}
2
+
3
+ A SimpleModule host application, generated by `sm create-host`.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ # Install Python deps (framework + any modules listed in pyproject.toml)
9
+ uv sync
10
+
11
+ # Copy example env and adjust
12
+ cp .env.example .env
13
+
14
+ # First time only — initialize the migration history for the modules you
15
+ # picked. Inspect the generated file, then apply it.
16
+ alembic revision --autogenerate -m "initial schema"
17
+ alembic upgrade head
18
+
19
+ # Run the API
20
+ python main.py
21
+ ```
22
+
23
+ Dev UI + API together (when you have a `client_app/` alongside this file):
24
+
25
+ ```bash
26
+ make dev # if you ship a Makefile; otherwise run vite + uvicorn in two shells
27
+ ```
28
+
29
+ ## Adding a module
30
+
31
+ ```bash
32
+ # 1) install the module package (e.g. from PyPI)
33
+ uv add simple_module_my_module
34
+
35
+ # 2) generate & apply the migration (new tables + any schema changes)
36
+ alembic revision --autogenerate -m "add my-module"
37
+ alembic upgrade head
38
+
39
+ # 3) restart the host — the module's routes, menu items, permissions,
40
+ # feature flags, events, and health checks register automatically
41
+ ```
42
+
43
+ ## Disabling a module without uninstalling it
44
+
45
+ Set `SM_MODULES_ENABLED` to a JSON array of the modules you want loaded:
46
+
47
+ ```
48
+ SM_MODULES_ENABLED=["Auth","Products"]
49
+ ```
50
+
51
+ Unlisted modules stay installed (so the existing DB schema is untouched)
52
+ but don't contribute any routes, registries, or lifecycle work.
53
+
54
+ ## Upgrading the framework
55
+
56
+ Modules declare `requires_framework` in their `Meta`. If a module can't
57
+ work with your installed `simple_module_core` version, the host refuses
58
+ to boot with `FrameworkVersionError` listing the offending modules.
59
+ Upgrade or pin as appropriate.
@@ -0,0 +1,12 @@
1
+ # --- background_tasks ----------------------------------------------------
2
+ .PHONY: worker beat worker-docker
3
+
4
+ worker: ## Run a Celery worker locally against $(SM_BG_TASKS_BROKER_URL)
5
+ uv run celery -A scripts.run_worker:celery worker -l info
6
+
7
+ beat: ## Run the Celery beat scheduler locally
8
+ uv run celery -A scripts.run_worker:celery beat -l info
9
+
10
+ worker-docker: ## Build + run the worker + beat services in docker
11
+ docker compose up --build worker beat
12
+ # --- end background_tasks ------------------------------------------------
@@ -0,0 +1,60 @@
1
+ services:
2
+ redis:
3
+ image: redis:7-alpine
4
+ ports:
5
+ - "6379:6379"
6
+ volumes:
7
+ - redisdata:/data
8
+ healthcheck:
9
+ test: ["CMD", "redis-cli", "ping"]
10
+ interval: 5s
11
+ timeout: 3s
12
+ retries: 10
13
+
14
+ worker:
15
+ build:
16
+ context: .
17
+ dockerfile: docker/worker.Dockerfile
18
+ env_file: .env
19
+ environment:
20
+ SM_BG_TASKS_BROKER_URL: redis://redis:6379/0
21
+ SM_BG_TASKS_RESULT_BACKEND: redis://redis:6379/1
22
+ depends_on:
23
+ redis:
24
+ condition: service_healthy
25
+ command:
26
+ - "uv"
27
+ - "run"
28
+ - "celery"
29
+ - "-A"
30
+ - "scripts.run_worker:celery"
31
+ - "worker"
32
+ - "-l"
33
+ - "info"
34
+ - "--concurrency=4"
35
+
36
+ beat:
37
+ build:
38
+ context: .
39
+ dockerfile: docker/worker.Dockerfile
40
+ env_file: .env
41
+ environment:
42
+ SM_BG_TASKS_BROKER_URL: redis://redis:6379/0
43
+ SM_BG_TASKS_RESULT_BACKEND: redis://redis:6379/1
44
+ depends_on:
45
+ redis:
46
+ condition: service_healthy
47
+ worker:
48
+ condition: service_started
49
+ command:
50
+ - "uv"
51
+ - "run"
52
+ - "celery"
53
+ - "-A"
54
+ - "scripts.run_worker:celery"
55
+ - "beat"
56
+ - "-l"
57
+ - "info"
58
+
59
+ volumes:
60
+ redisdata:
@@ -0,0 +1,17 @@
1
+ """Entry point for the Celery worker and beat services.
2
+
3
+ Both the web process and the worker go through the same
4
+ ``background_tasks.celery_app.build_celery`` factory so the broker
5
+ config, autodiscovered tasks, and signal handlers stay in lockstep.
6
+
7
+ Run locally:
8
+ uv run celery -A scripts.run_worker:celery worker -l info
9
+ uv run celery -A scripts.run_worker:celery beat -l info
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from background_tasks.celery_app import build_celery
15
+ from background_tasks.settings import BackgroundTasksSettings
16
+
17
+ celery = build_celery(BackgroundTasksSettings())
@@ -0,0 +1,37 @@
1
+ # Celery worker image for the BackgroundTasks module.
2
+ # Serves both the worker and beat services in docker-compose — they
3
+ # differ only by command.
4
+
5
+ FROM python:3.12-slim AS base
6
+
7
+ ENV PYTHONDONTWRITEBYTECODE=1 \
8
+ PYTHONUNBUFFERED=1 \
9
+ UV_LINK_MODE=copy \
10
+ UV_COMPILE_BYTECODE=1 \
11
+ UV_SYSTEM_PYTHON=1
12
+
13
+ RUN apt-get update \
14
+ && apt-get install -y --no-install-recommends \
15
+ curl \
16
+ ca-certificates \
17
+ build-essential \
18
+ && rm -rf /var/lib/apt/lists/* \
19
+ && pip install --no-cache-dir uv
20
+
21
+ WORKDIR /app
22
+
23
+ COPY pyproject.toml uv.lock ./
24
+ COPY scripts/ scripts/
25
+ COPY client_app/ client_app/
26
+
27
+ RUN uv sync --frozen --no-dev
28
+
29
+ RUN useradd --system --uid 10001 --home /app --shell /usr/sbin/nologin worker \
30
+ && chown -R worker:worker /app
31
+ USER worker
32
+
33
+ ENV CELERY_APP=scripts.run_worker:celery
34
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
35
+ CMD uv run celery -A $CELERY_APP inspect ping -d celery@$HOSTNAME || exit 1
36
+
37
+ CMD ["uv", "run", "celery", "-A", "scripts.run_worker:celery", "worker", "-l", "info"]
@@ -0,0 +1,36 @@
1
+ [alembic]
2
+ script_location = migrations
3
+ sqlalchemy.url =
4
+
5
+ [loggers]
6
+ keys = root,sqlalchemy,alembic
7
+
8
+ [handlers]
9
+ keys = console
10
+
11
+ [formatters]
12
+ keys = generic
13
+
14
+ [logger_root]
15
+ level = WARN
16
+ handlers = console
17
+
18
+ [logger_sqlalchemy]
19
+ level = WARN
20
+ handlers =
21
+ qualname = sqlalchemy.engine
22
+
23
+ [logger_alembic]
24
+ level = INFO
25
+ handlers =
26
+ qualname = alembic
27
+
28
+ [handler_console]
29
+ class = StreamHandler
30
+ args = (sys.stderr,)
31
+ level = NOTSET
32
+ formatter = generic
33
+
34
+ [formatter_generic]
35
+ format = %(levelname)-5.5s [%(name)s] %(message)s
36
+ datefmt = %H:%M:%S
@@ -0,0 +1,16 @@
1
+ import { createInertiaApp } from '@inertiajs/react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { resolvePage } from './pages';
4
+
5
+ createInertiaApp({
6
+ resolve: async (name) => {
7
+ return await resolvePage(name);
8
+ },
9
+ setup({ el, App, props }) {
10
+ createRoot(el).render(<App {...props} />);
11
+ },
12
+ progress: {
13
+ color: '#4B5563',
14
+ delay: 150,
15
+ },
16
+ });
@@ -0,0 +1,2 @@
1
+ import './styles.css';
2
+ import './app';
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "{{HOST_NAME}}-client-app",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "tsc && vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "@inertiajs/react": "^2.0.0",
12
+ "react": "^19.0.0",
13
+ "react-dom": "^19.0.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^22.0.0",
17
+ "@types/react": "^19.0.0",
18
+ "@types/react-dom": "^19.0.0",
19
+ "@vitejs/plugin-react": "^5.0.0",
20
+ "typescript": "^5.7.0",
21
+ "vite": "^6.0.0"
22
+ }
23
+ }
@@ -0,0 +1,13 @@
1
+ type ErrorProps = {
2
+ status: number;
3
+ message?: string;
4
+ };
5
+
6
+ export default function Error({ status, message }: ErrorProps) {
7
+ return (
8
+ <main style={{ padding: '2rem', maxWidth: '40rem', margin: '0 auto' }}>
9
+ <h1>{status}</h1>
10
+ <p>{message || 'Something went wrong.'}</p>
11
+ </main>
12
+ );
13
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Inertia page resolver.
3
+ *
4
+ * Module pages are discovered via a generated file (modules.generated.ts)
5
+ * emitted by the Python host at boot, or manually via `sm gen-pages`. Each
6
+ * installed module contributes an import.meta.glob() call with an absolute
7
+ * path, so pages shipped inside pip-installed module wheels resolve.
8
+ *
9
+ * Host-level pages live in client_app/pages/{PageName}.tsx and are
10
+ * registered under just "{PageName}" (e.g. "Error").
11
+ */
12
+
13
+ import { moduleGlobs } from './modules.generated';
14
+
15
+ type PageModule = { default: React.ComponentType<Record<string, unknown>> };
16
+ type PageLoader = () => Promise<PageModule>;
17
+
18
+ const hostPages = import.meta.glob<PageModule>('./pages/*.tsx');
19
+
20
+ const pages: Record<string, PageLoader> = {};
21
+
22
+ for (const [moduleName, globEntries] of Object.entries(moduleGlobs)) {
23
+ for (const [filePath, loader] of Object.entries(globEntries)) {
24
+ const match = filePath.match(/\/pages\/(\w+)\.tsx$/);
25
+ if (match) {
26
+ pages[`${moduleName}/${match[1]}`] = loader;
27
+ }
28
+ }
29
+ }
30
+
31
+ for (const [filePath, loader] of Object.entries(hostPages)) {
32
+ const match = filePath.match(/\.\/pages\/(\w+)\.tsx$/);
33
+ if (match) {
34
+ pages[match[1]] = loader;
35
+ }
36
+ }
37
+
38
+ export async function resolvePage(
39
+ name: string,
40
+ ): Promise<React.ComponentType<Record<string, unknown>>> {
41
+ const loader = pages[name];
42
+ if (!loader) {
43
+ throw new Error(`Page "${name}" not found. Available pages: ${Object.keys(pages).join(', ')}`);
44
+ }
45
+ const module = await loader();
46
+ return module.default;
47
+ }
@@ -0,0 +1,7 @@
1
+ body {
2
+ margin: 0;
3
+ font-family:
4
+ system-ui,
5
+ -apple-system,
6
+ sans-serif;
7
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "types": ["vite/client", "node"],
13
+ "allowImportingTsExtensions": false
14
+ },
15
+ "include": ["**/*.ts", "**/*.tsx"]
16
+ }
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import react from '@vitejs/plugin-react';
4
+ import { defineConfig } from 'vite';
5
+
6
+ const projectRoot = path.resolve(__dirname, '..');
7
+
8
+ // Load the module pages manifest written by the Python host at boot.
9
+ // Each entry points at an absolute pages/ directory — typically inside a
10
+ // pip-installed module wheel. Vite needs these in server.fs.allow so the
11
+ // dev server can read files outside the host root.
12
+ const manifestPath = path.resolve(__dirname, 'modules.manifest.json');
13
+ const moduleFsAllow: string[] = [];
14
+ if (fs.existsSync(manifestPath)) {
15
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as Record<string, string>;
16
+ for (const pagesDir of Object.values(manifest)) {
17
+ moduleFsAllow.push(path.dirname(pagesDir));
18
+ }
19
+ }
20
+
21
+ export default defineConfig({
22
+ plugins: [react()],
23
+ root: __dirname,
24
+ build: {
25
+ outDir: '../static/dist',
26
+ manifest: true,
27
+ rollupOptions: {
28
+ input: path.resolve(__dirname, 'main.tsx'),
29
+ },
30
+ },
31
+ server: {
32
+ port: 5050,
33
+ strictPort: true,
34
+ origin: 'http://localhost:5050',
35
+ fs: {
36
+ allow: [projectRoot, ...moduleFsAllow],
37
+ },
38
+ },
39
+ });
@@ -0,0 +1,27 @@
1
+ """Host application entry point.
2
+
3
+ This file was generated by `sm create-host`. Modules are discovered at boot
4
+ via entry_points; add them to this host's pyproject.toml to install them.
5
+ """
6
+
7
+ from simple_module_hosting import Settings, create_app
8
+ from simple_module_hosting.logging import setup_logging
9
+
10
+ settings = Settings()
11
+
12
+ setup_logging(
13
+ level=settings.log_level,
14
+ json_format=settings.log_format == "json",
15
+ )
16
+
17
+ app = create_app(settings)
18
+
19
+ if __name__ == "__main__":
20
+ import uvicorn
21
+
22
+ uvicorn.run(
23
+ "main:app",
24
+ host="0.0.0.0",
25
+ port=8000,
26
+ reload=settings.is_development,
27
+ )