From f03b7be8901ccfe31b65593673f9c72e59bbf400 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Sat, 2 May 2026 12:46:03 +0200 Subject: [PATCH 1/3] fix(scaffold,ui): unbreak fresh-install module pages + scaffold workspace (#110-#117) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * packages/ui: move lucide-react, sonner, radix-ui, and 11 other runtime imports out of peerDependencies into dependencies so a host that pulls in @simple-module-py/ui actually gets them installed; only React/Inertia stay as peers (singletons). Drop the catch-all "./*": "./src/*" exports entry and pin every subpath ("./components/*", "./layouts/*", etc.) to its real .tsx/.ts target with a types conditional, fixing vite resolve failures from module pages. (#114, #115, #110) * modules/{users,dashboard,permissions,feature_flags,background_tasks, file_storage}: declare lucide-react and/or sonner as dependencies — the modules import these directly, but every wheel was shipping "dependencies": {} so `sm host sync-js-deps` had nothing to install. Wire sync-js-deps into the scaffolded Makefile's `install` target so a fresh `sm new` host gets module JS deps without a manual extra step. (#116) * sm new: default to a workspace layout — scaffold modules/hello/ as a working authoring template (module.py + settings.py + endpoints/api.py), add [tool.uv.workspace] members=["modules/*"] + a workspace source for the sample, and "workspaces": ["client_app", "modules/*"] in package.json so npm + vite resolve module .tsx files naturally. New --flat flag preserves the legacy single-host layout. The module template gained a settings.py.tpl (module.py.tpl was already importing it) and pydantic-settings as a dep. (#117) #111 (SM017 pointing inside .venv/site-packages) was already fixed in #113 and is covered by test_silent_when_module_lives_in_site_packages — no code change needed. --- .../cli/simple_module_cli/app_project.py | 64 +++++++++++++++++-- framework/cli/simple_module_cli/new.py | 19 +++++- .../cli/simple_module_cli/scaffolding.py | 1 + .../simple_module_cli/templates/host/Makefile | 6 +- .../templates/host/pyproject.toml.tpl | 7 ++ .../module/__PACKAGE__/settings.py.tpl | 14 ++++ .../templates/module/pyproject.toml.tpl | 1 + framework/cli/tests/test_cli_new.py | 60 +++++++++++++++++ modules/background_tasks/package.json | 5 +- modules/dashboard/package.json | 4 +- modules/feature_flags/package.json | 5 +- modules/file_storage/package.json | 5 +- modules/permissions/package.json | 5 +- modules/users/package.json | 5 +- packages/ui/package.json | 47 ++++++++++---- 15 files changed, 222 insertions(+), 26 deletions(-) create mode 100644 framework/cli/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl diff --git a/framework/cli/simple_module_cli/app_project.py b/framework/cli/simple_module_cli/app_project.py index caa51d8a..2562bbb9 100644 --- a/framework/cli/simple_module_cli/app_project.py +++ b/framework/cli/simple_module_cli/app_project.py @@ -23,10 +23,12 @@ from simple_module_cli.case import to_kebab_case, to_pascal_case from simple_module_cli.catalog import CATALOG, PRESETS, expand_deps from simple_module_cli.recipes import RECIPES, ScaffoldCtx -from simple_module_cli.scaffolding import create_host +from simple_module_cli.scaffolding import create_host, create_module __all__ = ["create_app_project"] +_SAMPLE_MODULE_NAME = "hello" + def _resolve_framework_version() -> str: """Resolve the framework version to pin scaffolded apps against. @@ -69,6 +71,7 @@ def create_app_project( db: str = "sqlite", tenancy: bool = False, selected: Sequence[str] | None = None, + flat: bool = False, ) -> None: """Greenfield ``simple-module new`` scaffold. @@ -101,10 +104,22 @@ def create_app_project( env_text = set_env_key(env_text, "SM_MULTI_TENANT", "true" if tenancy else "false") env_path.write_text(env_text, encoding="utf-8") + if not flat: + _scaffold_sample_module(target) + py_deps.append(f"simple_module_{_SAMPLE_MODULE_NAME}") + pyproject = target / "pyproject.toml" if pyproject.exists(): text = pyproject.read_text(encoding="utf-8") - text = _inject_py_deps(text, py_deps, _APP_PY_DEV_DEPS) + text = _inject_py_deps( + text, + py_deps, + _APP_PY_DEV_DEPS, + workspace_sources=( + {f"simple_module_{_SAMPLE_MODULE_NAME}": True} if not flat else None + ), + drop_workspace=flat, + ) pyproject.write_text(text, encoding="utf-8") pkg_path = target / "package.json" @@ -114,6 +129,8 @@ def create_app_project( data = {"name": to_kebab_case(name), "private": True, "type": "module"} data.setdefault("dependencies", {}).update(_APP_NPM_DEPS) data.setdefault("devDependencies", {}).update(_APP_NPM_DEV_DEPS) + if not flat: + data["workspaces"] = ["client_app", "modules/*"] pkg_path.write_text(_json.dumps(data, indent=2) + "\n", encoding="utf-8") ctx = ScaffoldCtx(name=name, db=db, tenancy=tenancy, selected=tuple(resolved)) @@ -123,14 +140,42 @@ def create_app_project( RECIPES[recipe_key].apply(target, ctx) +def _scaffold_sample_module(target: Path) -> None: + """Drop a working ``modules/hello/`` package as an authoring template. + + Gives the user a place to copy when they want to add a feature module — + the alternative is reverse-engineering one of the wheel-installed + framework modules from ``.venv/site-packages/``. + """ + sample_dest = target / "modules" / _SAMPLE_MODULE_NAME + if sample_dest.exists(): + return + create_module(sample_dest, name=_SAMPLE_MODULE_NAME) + + def _db_url(db: str, slug: str) -> str: if db == "postgres": return f"postgresql+asyncpg://postgres:postgres@localhost:5432/{slug}" return "sqlite+aiosqlite:///./app.db" -def _inject_py_deps(text: str, deps: list[str], dev_deps: list[str]) -> str: - """Replace project.dependencies + dependency-groups.dev in a pyproject.toml.""" +def _inject_py_deps( + text: str, + deps: list[str], + dev_deps: list[str], + workspace_sources: dict[str, bool] | None = None, + drop_workspace: bool = False, +) -> str: + """Replace project.dependencies + dependency-groups.dev in a pyproject.toml. + + ``workspace_sources`` adds ``[tool.uv.sources]`` entries pointing at + workspace members (e.g. the bundled sample module) so uv resolves + them locally instead of from PyPI. + + ``drop_workspace`` strips the static ``[tool.uv.workspace]`` block + inherited from the template — used for ``--flat`` mode, where there + is no ``modules/`` tree to scan. + """ import tomlkit doc = tomlkit.parse(text) @@ -138,4 +183,15 @@ def _inject_py_deps(text: str, deps: list[str], dev_deps: list[str]) -> str: project["dependencies"] = list(deps) groups = doc.setdefault("dependency-groups", tomlkit.table()) groups["dev"] = list(dev_deps) + if drop_workspace: + tool = doc.get("tool") + uv_table = tool.get("uv") if isinstance(tool, dict) else None + if isinstance(uv_table, dict) and "workspace" in uv_table: + del uv_table["workspace"] + if workspace_sources: + tool = doc.setdefault("tool", tomlkit.table()) + uv_table = tool.setdefault("uv", tomlkit.table()) + sources = uv_table.setdefault("sources", tomlkit.table()) + for pkg in workspace_sources: + sources[pkg] = {"workspace": True} return tomlkit.dumps(doc) diff --git a/framework/cli/simple_module_cli/new.py b/framework/cli/simple_module_cli/new.py index 5a7507a1..1bfdd8dc 100644 --- a/framework/cli/simple_module_cli/new.py +++ b/framework/cli/simple_module_cli/new.py @@ -64,6 +64,16 @@ def new_project( help="Skip 'uv sync' / 'npm install' / 'alembic upgrade head' after scaffolding.", ), ] = False, + flat: Annotated[ + bool, + typer.Option( + "--flat", + help=( + "Skip the modules/ directory and sample module. Use when the host " + "will only consume published modules and never author its own." + ), + ), + ] = False, ) -> None: """Scaffold a new SimpleModule app, optionally with background jobs.""" target = dest or Path.cwd() / name @@ -90,7 +100,14 @@ def new_project( raise typer.Exit(code=1) from None try: - create_app_project(target, name=name, db=db_final, tenancy=tenancy_final, selected=resolved) + create_app_project( + target, + name=name, + db=db_final, + tenancy=tenancy_final, + selected=resolved, + flat=flat, + ) except FileExistsError as exc: typer.echo(f"ERROR: {exc}", err=True) raise typer.Exit(code=1) from exc diff --git a/framework/cli/simple_module_cli/scaffolding.py b/framework/cli/simple_module_cli/scaffolding.py index ae877e90..5fec6173 100644 --- a/framework/cli/simple_module_cli/scaffolding.py +++ b/framework/cli/simple_module_cli/scaffolding.py @@ -117,6 +117,7 @@ def create_module( "{{MODULE_NAME}}": display_name, "{{MODULE_SLUG}}": slug, "{{PACKAGE_NAME}}": package_name, + "{{PACKAGE_NAME_UPPER}}": package_name.upper(), }, path_rewrites={_PACKAGE_PATH_TOKEN: package_name}, ) diff --git a/framework/cli/simple_module_cli/templates/host/Makefile b/framework/cli/simple_module_cli/templates/host/Makefile index ca64c1bd..6b122480 100644 --- a/framework/cli/simple_module_cli/templates/host/Makefile +++ b/framework/cli/simple_module_cli/templates/host/Makefile @@ -1,8 +1,9 @@ -.PHONY: install dev dev-api dev-ui build migrate gen-pages +.PHONY: install dev dev-api dev-ui build migrate gen-pages sync-js-deps install: uv sync cd client_app && npm install + $(MAKE) sync-js-deps dev: gen-pages @echo "Starting API and UI dev servers..." @@ -22,3 +23,6 @@ migrate: gen-pages: uv run python -m simple_module_hosting gen-pages --host-dir=client_app + +sync-js-deps: + uv run python -m simple_module_hosting sync-js-deps --host-client-app=client_app diff --git a/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl b/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl index c1282d99..0a557483 100644 --- a/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl +++ b/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl @@ -16,3 +16,10 @@ dependencies = [ # Host is an application, not a distributable package. [tool.uv] package = false + +# Workspace plumbing — local modules under modules// are picked up +# automatically. Add a new module with `sm create-module ` (writes to +# modules//), then add `simple_module_` to the dependency list +# above. uv resolves it from the workspace instead of PyPI. +[tool.uv.workspace] +members = ["modules/*"] diff --git a/framework/cli/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl b/framework/cli/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl new file mode 100644 index 00000000..e83724a9 --- /dev/null +++ b/framework/cli/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl @@ -0,0 +1,14 @@ +"""{{MODULE_NAME}} module settings. + +Per-module env-var prefix is ``SM_{{PACKAGE_NAME_UPPER}}_*``. Add fields here +as the module grows; the framework wires them onto +``app.state.{{PACKAGE_NAME}}`` via ``register_settings``. +""" + +from __future__ import annotations + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class {{MODULE_NAME}}Settings(BaseSettings): + model_config = SettingsConfigDict(env_prefix="SM_{{PACKAGE_NAME_UPPER}}_", extra="ignore") diff --git a/framework/cli/simple_module_cli/templates/module/pyproject.toml.tpl b/framework/cli/simple_module_cli/templates/module/pyproject.toml.tpl index edb2072f..c9a9c726 100644 --- a/framework/cli/simple_module_cli/templates/module/pyproject.toml.tpl +++ b/framework/cli/simple_module_cli/templates/module/pyproject.toml.tpl @@ -7,6 +7,7 @@ dependencies = [ "simple_module_core>=1.0,<2.0", "simple_module_db>=1.0,<2.0", "simple_module_hosting>=1.0,<2.0", + "pydantic-settings>=2.0", "sqlalchemy>=2.0", ] diff --git a/framework/cli/tests/test_cli_new.py b/framework/cli/tests/test_cli_new.py index e82a966f..264fd802 100644 --- a/framework/cli/tests/test_cli_new.py +++ b/framework/cli/tests/test_cli_new.py @@ -196,6 +196,66 @@ def test_sm_new_interactive_full_preset(tmp_path: Path) -> None: assert (target / "docker-compose.yml").is_file() +def test_sm_new_default_scaffolds_sample_hello_module(tmp_path: Path) -> None: + """Default (workspace) mode lays down modules/hello/ as an authoring template.""" + runner = CliRunner() + target = tmp_path / "demo" + result = runner.invoke( + app, + ["new", "demo", "--yes", "--db", "sqlite", "--no-install", "--dest", str(target)], + ) + assert result.exit_code == 0, result.output + assert (target / "modules" / "hello" / "pyproject.toml").is_file() + assert (target / "modules" / "hello" / "hello" / "module.py").is_file() + + +def test_sm_new_default_wires_workspace_in_pyproject(tmp_path: Path) -> None: + """Default mode adds [tool.uv.workspace] members + a workspace source for the sample.""" + runner = CliRunner() + target = tmp_path / "demo" + runner.invoke( + app, + ["new", "demo", "--yes", "--db", "sqlite", "--no-install", "--dest", str(target)], + ) + pyproject_text = (target / "pyproject.toml").read_text() + assert "[tool.uv.workspace]" in pyproject_text + assert 'members = ["modules/*"]' in pyproject_text + assert "simple_module_hello" in pyproject_text + # Sample module is a workspace source, not pulled from PyPI. + assert "[tool.uv.sources" in pyproject_text + assert "workspace = true" in pyproject_text + + +def test_sm_new_default_adds_npm_workspaces_field(tmp_path: Path) -> None: + """Default mode declares ``workspaces`` so vite picks up modules//.""" + runner = CliRunner() + target = tmp_path / "demo" + runner.invoke( + app, + ["new", "demo", "--yes", "--db", "sqlite", "--no-install", "--dest", str(target)], + ) + data = json.loads((target / "package.json").read_text()) + assert data.get("workspaces") == ["client_app", "modules/*"] + + +def test_sm_new_flat_skips_modules_dir(tmp_path: Path) -> None: + """``--flat`` keeps the legacy single-host layout: no modules/ tree, no sample.""" + runner = CliRunner() + target = tmp_path / "demo" + result = runner.invoke( + app, + ["new", "demo", "--yes", "--flat", "--no-install", "--dest", str(target)], + ) + assert result.exit_code == 0, result.output + assert not (target / "modules").exists() + pyproject_text = (target / "pyproject.toml").read_text() + assert "simple_module_hello" not in pyproject_text + # No workspace plumbing pointing at a non-existent modules/ tree. + assert "[tool.uv.workspace]" not in pyproject_text + data = json.loads((target / "package.json").read_text()) + assert "workspaces" not in data + + def test_sm_new_refuses_to_overwrite(tmp_path: Path) -> None: target = tmp_path / "my-app" target.mkdir() diff --git a/modules/background_tasks/package.json b/modules/background_tasks/package.json index 6064e254..ac198a43 100644 --- a/modules/background_tasks/package.json +++ b/modules/background_tasks/package.json @@ -12,5 +12,8 @@ "devDependencies": { "@simple-module-py/tsconfig": "*" }, - "dependencies": {} + "dependencies": { + "lucide-react": "^1.8.0", + "sonner": "^2.0.0" + } } diff --git a/modules/dashboard/package.json b/modules/dashboard/package.json index 6e9d327d..415c7270 100644 --- a/modules/dashboard/package.json +++ b/modules/dashboard/package.json @@ -12,5 +12,7 @@ "devDependencies": { "@simple-module-py/tsconfig": "*" }, - "dependencies": {} + "dependencies": { + "lucide-react": "^1.8.0" + } } diff --git a/modules/feature_flags/package.json b/modules/feature_flags/package.json index 2a4209a3..5e810be9 100644 --- a/modules/feature_flags/package.json +++ b/modules/feature_flags/package.json @@ -12,5 +12,8 @@ "devDependencies": { "@simple-module-py/tsconfig": "*" }, - "dependencies": {} + "dependencies": { + "lucide-react": "^1.8.0", + "sonner": "^2.0.0" + } } diff --git a/modules/file_storage/package.json b/modules/file_storage/package.json index 5b66d583..2576406e 100644 --- a/modules/file_storage/package.json +++ b/modules/file_storage/package.json @@ -12,5 +12,8 @@ "devDependencies": { "@simple-module-py/tsconfig": "*" }, - "dependencies": {} + "dependencies": { + "lucide-react": "^1.8.0", + "sonner": "^2.0.0" + } } diff --git a/modules/permissions/package.json b/modules/permissions/package.json index 00e49ad3..5f1b2e68 100644 --- a/modules/permissions/package.json +++ b/modules/permissions/package.json @@ -12,5 +12,8 @@ "devDependencies": { "@simple-module-py/tsconfig": "*" }, - "dependencies": {} + "dependencies": { + "lucide-react": "^1.8.0", + "sonner": "^2.0.0" + } } diff --git a/modules/users/package.json b/modules/users/package.json index d1eb5bf9..8e8ec7ca 100644 --- a/modules/users/package.json +++ b/modules/users/package.json @@ -12,5 +12,8 @@ "devDependencies": { "@simple-module-py/tsconfig": "*" }, - "dependencies": {} + "dependencies": { + "lucide-react": "^1.8.0", + "sonner": "^2.0.0" + } } diff --git a/packages/ui/package.json b/packages/ui/package.json index ddc9d3b9..62f503c1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -26,13 +26,31 @@ "types": "./src/index.ts", "default": "./src/index.ts" }, - "./components/ui/*": "./src/components/ui/*.tsx", - "./components/*": "./src/components/*.tsx", - "./layouts/*": "./src/layouts/*.tsx", - "./hooks/*": "./src/hooks/*.ts", - "./lib/*": "./src/lib/*.ts", - "./styles/*": "./src/styles/*", - "./*": "./src/*" + "./components/ui/*": { + "types": "./src/components/ui/*.tsx", + "default": "./src/components/ui/*.tsx" + }, + "./components/*": { + "types": "./src/components/*.tsx", + "default": "./src/components/*.tsx" + }, + "./layouts/*": { + "types": "./src/layouts/*.tsx", + "default": "./src/layouts/*.tsx" + }, + "./hooks/*": { + "types": "./src/hooks/*.ts", + "default": "./src/hooks/*.ts" + }, + "./lib/*": { + "types": "./src/lib/*.ts", + "default": "./src/lib/*.ts" + }, + "./types": { + "types": "./src/types.ts", + "default": "./src/types.ts" + }, + "./styles/*": "./src/styles/*" }, "files": [ "src", @@ -42,17 +60,21 @@ "access": "public" }, "peerDependencies": { - "@base-ui/react": "^1.0.0", "@inertiajs/react": "^2.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "dependencies": { + "@base-ui/react": "^1.0.0", + "@simple-module-py/i18n": "0.0.7", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^1.0.0", "embla-carousel-react": "^8.0.0", "input-otp": "^1.0.0", - "lucide-react": "*", + "lucide-react": "^1.8.0", "next-themes": "^0.4.0", - "radix-ui": "*", - "react": "^19.0.0", + "radix-ui": "^1.4.0", "react-day-picker": "^9.0.0", "react-resizable-panels": "^4.0.0", "recharts": "^3.0.0", @@ -60,9 +82,6 @@ "tailwind-merge": "^3.0.0", "vaul": "^1.0.0" }, - "dependencies": { - "@simple-module-py/i18n": "0.0.7" - }, "devDependencies": { "@simple-module-py/tsconfig": "0.0.7" } From 3602f73d9c114cc5aec463fe7d54dc7080bcdff0 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Sat, 2 May 2026 12:50:28 +0200 Subject: [PATCH 2/3] refactor(scaffold): collapse workspace flags + reuse pypi-name helper Review feedback on #118: - _inject_py_deps had two correlated booleans (workspace_sources, drop_workspace); collapse to a single flat: bool and rename the helper to _rewrite_pyproject since it now configures the uv workspace too, not just deps. - Reuse _module_to_pypi_name from scaffolding.py instead of re-templating simple_module_{_SAMPLE_MODULE_NAME} inline. - Trim WHAT-narration comments in the host pyproject template and drop the duplicated lookup in the helper. --- .../cli/simple_module_cli/app_project.py | 56 ++++++------------- .../templates/host/pyproject.toml.tpl | 7 +-- 2 files changed, 21 insertions(+), 42 deletions(-) diff --git a/framework/cli/simple_module_cli/app_project.py b/framework/cli/simple_module_cli/app_project.py index 2562bbb9..cc5811a3 100644 --- a/framework/cli/simple_module_cli/app_project.py +++ b/framework/cli/simple_module_cli/app_project.py @@ -23,11 +23,12 @@ from simple_module_cli.case import to_kebab_case, to_pascal_case from simple_module_cli.catalog import CATALOG, PRESETS, expand_deps from simple_module_cli.recipes import RECIPES, ScaffoldCtx -from simple_module_cli.scaffolding import create_host, create_module +from simple_module_cli.scaffolding import _module_to_pypi_name, create_host, create_module __all__ = ["create_app_project"] _SAMPLE_MODULE_NAME = "hello" +_SAMPLE_MODULE_PKG = _module_to_pypi_name(_SAMPLE_MODULE_NAME) def _resolve_framework_version() -> str: @@ -106,20 +107,12 @@ def create_app_project( if not flat: _scaffold_sample_module(target) - py_deps.append(f"simple_module_{_SAMPLE_MODULE_NAME}") + py_deps.append(_SAMPLE_MODULE_PKG) pyproject = target / "pyproject.toml" if pyproject.exists(): text = pyproject.read_text(encoding="utf-8") - text = _inject_py_deps( - text, - py_deps, - _APP_PY_DEV_DEPS, - workspace_sources=( - {f"simple_module_{_SAMPLE_MODULE_NAME}": True} if not flat else None - ), - drop_workspace=flat, - ) + text = _rewrite_pyproject(text, py_deps, _APP_PY_DEV_DEPS, flat=flat) pyproject.write_text(text, encoding="utf-8") pkg_path = target / "package.json" @@ -141,10 +134,9 @@ def create_app_project( def _scaffold_sample_module(target: Path) -> None: - """Drop a working ``modules/hello/`` package as an authoring template. + """Give the user a place to copy when they want to add a feature module. - Gives the user a place to copy when they want to add a feature module — - the alternative is reverse-engineering one of the wheel-installed + The alternative is reverse-engineering one of the wheel-installed framework modules from ``.venv/site-packages/``. """ sample_dest = target / "modules" / _SAMPLE_MODULE_NAME @@ -159,22 +151,13 @@ def _db_url(db: str, slug: str) -> str: return "sqlite+aiosqlite:///./app.db" -def _inject_py_deps( - text: str, - deps: list[str], - dev_deps: list[str], - workspace_sources: dict[str, bool] | None = None, - drop_workspace: bool = False, -) -> str: - """Replace project.dependencies + dependency-groups.dev in a pyproject.toml. - - ``workspace_sources`` adds ``[tool.uv.sources]`` entries pointing at - workspace members (e.g. the bundled sample module) so uv resolves - them locally instead of from PyPI. +def _rewrite_pyproject(text: str, deps: list[str], dev_deps: list[str], *, flat: bool) -> str: + """Replace deps + wire uv workspace based on ``flat`` mode. - ``drop_workspace`` strips the static ``[tool.uv.workspace]`` block - inherited from the template — used for ``--flat`` mode, where there - is no ``modules/`` tree to scan. + Workspace mode (``flat=False``) adds a ``[tool.uv.sources]`` entry so uv + resolves the bundled sample module from the workspace, not PyPI. Flat + mode strips the static ``[tool.uv.workspace]`` block inherited from the + template — there is no ``modules/`` tree for it to point at. """ import tomlkit @@ -183,15 +166,12 @@ def _inject_py_deps( project["dependencies"] = list(deps) groups = doc.setdefault("dependency-groups", tomlkit.table()) groups["dev"] = list(dev_deps) - if drop_workspace: - tool = doc.get("tool") - uv_table = tool.get("uv") if isinstance(tool, dict) else None - if isinstance(uv_table, dict) and "workspace" in uv_table: + tool = doc.setdefault("tool", tomlkit.table()) + uv_table = tool.setdefault("uv", tomlkit.table()) + if flat: + if "workspace" in uv_table: del uv_table["workspace"] - if workspace_sources: - tool = doc.setdefault("tool", tomlkit.table()) - uv_table = tool.setdefault("uv", tomlkit.table()) + else: sources = uv_table.setdefault("sources", tomlkit.table()) - for pkg in workspace_sources: - sources[pkg] = {"workspace": True} + sources[_SAMPLE_MODULE_PKG] = {"workspace": True} return tomlkit.dumps(doc) diff --git a/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl b/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl index 0a557483..884a3824 100644 --- a/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl +++ b/framework/cli/simple_module_cli/templates/host/pyproject.toml.tpl @@ -17,9 +17,8 @@ dependencies = [ [tool.uv] package = false -# Workspace plumbing — local modules under modules// are picked up -# automatically. Add a new module with `sm create-module ` (writes to -# modules//), then add `simple_module_` to the dependency list -# above. uv resolves it from the workspace instead of PyPI. +# Add a new module with `sm create-module `, then add +# `simple_module_` to the dependency list above and to +# [tool.uv.sources] as `{ workspace = true }`. [tool.uv.workspace] members = ["modules/*"] From 3244e4c867f0ee4148acb374a7a76513361551ec Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Sat, 2 May 2026 12:52:30 +0200 Subject: [PATCH 3/3] fix(scaffold): annotate package.json data as dict[str, Any] for ty ty narrowed the literal-dict branch to dict[str, str | bool] and rejected the data['workspaces'] = list assignment. Explicit annotation widens it. --- framework/cli/simple_module_cli/app_project.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework/cli/simple_module_cli/app_project.py b/framework/cli/simple_module_cli/app_project.py index cc5811a3..1b7547fd 100644 --- a/framework/cli/simple_module_cli/app_project.py +++ b/framework/cli/simple_module_cli/app_project.py @@ -18,6 +18,7 @@ from importlib.metadata import PackageNotFoundError from importlib.metadata import version as _pkg_version from pathlib import Path +from typing import Any from simple_module_cli._env import set_env_key from simple_module_cli.case import to_kebab_case, to_pascal_case @@ -116,6 +117,7 @@ def create_app_project( pyproject.write_text(text, encoding="utf-8") pkg_path = target / "package.json" + data: dict[str, Any] if pkg_path.exists(): data = _json.loads(pkg_path.read_text(encoding="utf-8")) else: