feat(cli): sm new scaffolds into a uv/npm workspace with host/ + modules/ - #121
Merged
Conversation
Deploying simple-module-python with |
| Latest commit: | f57a53e |
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4116fc38.simple-module-python.pages.dev |
| Branch Preview URL: | https://claude-github-issue-117-jmay.simple-module-python.pages.dev |
`sm new my-app` now lays down a workspace mirroring the framework repo's own layout: a project-root pyproject.toml + package.json + Makefile that delegate to a `host/` subdir (the FastAPI app) and `modules/*` (workspace member packages, pre-seeded with a `hello` sample). Wheel-installed module .tsx files were resolving outside the host's npm tree; with everything under one npm workspace, vite finds bare imports without per-module aliasing. `--flat` keeps today's single-host layout for users who only consume published modules. Closes#117.
- _strip_workspace_owned_files: use unlink(missing_ok=True) instead of the exists+unlink TOCTOU pair. - Drop the dead host_pyproject.exists() check (host template always emits pyproject.toml). - _write_npm_deps: collapse the flat/workspace branches to one merge body. - Workspace Makefile: delegate migrate/migration/dev-api to host via $(MAKE) -C host so the host Makefile stays the single source of truth. Add a `migration` target on the host Makefile for symmetry with the workspace Makefile in flat mode. - Trim narrative docstrings.
Manual smoke test of `sm new test-app && cd test-app && make install && make migrate && make dev` surfaced these blockers: 1. Workspace + host pyproject both substituted to the same `[project].name`, making uv refuse with "two workspace members are both named ...". Override host's name to `<kebab>-host` in workspace mode. 2. Sample module's `>=1.0,<2.0` range pins couldn't resolve against the framework's actual 0.0.8 wheels. Rewrite simple_module_* deps in the workspace-bundled hello sample to exact pins. 3. Sample module's hatch `force-include` for `<pkg>/static/dist` failed at build time because the dir doesn't exist until `vite build` runs. Drop a placeholder `.gitkeep` so `uv sync --all-packages` succeeds. 4. `@inertiajs/react: ^1.0.0` in `_APP_NPM_DEPS` peer-dep-conflicted with `@simple-module-py/ui@0.0.8` (needs ^2). Bump to ^2.0.0. 5. Vite's `server.fs.allow` was scoped to host root, but in workspace mode `node_modules` is hoisted one level higher. Walk up from client_app/ to the directory that holds node_modules. 6. Workspace Makefile delegated gen-pages / sync-module-deps to `uv run --project host sm host ...`, but `sm` isn't a host dep — only `simple_module_hosting` is. Delegate to `$(MAKE) -C host gen-pages` / `$(MAKE) -C host sync-js-deps` so the host's existing recipes apply. 7. Workspace `.env.example` and Makefile assumed Vite on 5173, but the host's vite.config.ts hard-codes 5050. Align both. 8. `make install` in workspace mode missed sync-module-deps, leaving wheel-installed modules' npm peers (lucide-react, sonner, ...) uninstalled. Add it to the install target. Also captured during the manual run, but **out of scope for this PR**: a `@vitejs/plugin-react can't detect preamble` runtime error from wheel-installed module pages (`.venv/.../users/pages/Login.tsx`). That's the long-standing #110/#115 issue — it isn't introduced by this PR and the workspace structure is the long-term path to fixing it (modules authored under `modules/*` resolve cleanly).
Building on the origin/main fixes for #110 / #115 and #119 / #116, this commit closes the remaining gaps that surfaced during a real end-to-end smoke test (`sm new` → `make install` → `make migrate` → `make dev`): 1. **React preamble**: the host's `templates/index.html` didn't inject the `__vite_plugin_react_preamble_installed__` global. Without it, plugin-react throws "can't detect preamble" on every wheel-installed `.tsx` module page and React never mounts. Mirror the framework repo's own `host/templates/index.html` and inject the preamble in dev mode. 2. **Vite fs.allow root**: `server.fs.allow` was scoped to `path.resolve(__dirname, '..')`, i.e. the host root. In workspace mode `node_modules` is hoisted to the workspace root one level higher, so vite refused to serve hoisted React. Walk up from `client_app/` to the directory that owns `node_modules` and use that as the serve root. 3. **Bare-import pre-bundling**: vite's optimizer never scanned wheel-installed module pages because they sit outside the project root, so CJS-only deps like `clsx`, `tailwind-merge`, `class-variance-authority` reached the browser without named ESM exports. Two changes: - `optimizeDeps.entries`: add the manifest's per-module pages dirs so the scanner crawls them. - `optimizeDeps.include`: walk `host/client_app/package.json` plus each declared dep's package.json (filesystem walk, since exports maps frequently exclude `./package.json`) and force-include every reachable package that has a top-level entry. `@simple-module-py/ui`'s transitive deps (`clsx`, `cmdk`, `radix-ui`, etc.) get pre-bundled and named imports work everywhere. 4. **Dedupe**: also dedupe `@inertiajs/react`, `@simple-module-py/ui`, `@simple-module-py/i18n`. Without it, wheel pages and host pages can end up with separate `usePage` contexts → "usePage must be used within the Inertia component" runtime error. After these changes, `make dev` against a fresh `sm new` checkout renders the full login page with CSS, JS, and React all live; the sign-in button transitions to "Signing in…" on submit. No console errors, no network failures, nothing in the API log past INFO request lines.
antosubashforce-pushed
the
claude/github-issue-117-jMAyb
branch
from
May 3, 2026 17:03
b022d33 to
d14798bCompare`doc["project"]` returns `Item | Container` and `Item` has no
`__setitem__`. Use `doc.setdefault("project", tomlkit.table())` (matching
`_rewrite_pyproject` next door) so ty sees a writeable `Container`.vite.config.ts: - Cache parsed package.json reads via a `Map<path, Pkg>` so the BFS in collectOptimizeIncludes doesn't read each dep's package.json twice (once for hasTopLevelEntry, once for transitive deps). - findPackageJSON checks fsRoot/node_modules directly instead of re-walking ancestors per dep — that walk was already done once for fsRoot. - Extract REACT_CORE_DEPS so dedupe and optimizeDeps.include can't drift apart silently. app_project.py: - Inline the one-line _write_flat_top_level_package_json wrapper into its single caller. - Split _seed_static_dist_placeholder out of _scaffold_sample_module so the function name matches what it does. templates/host/templates/index.html: - Restore the Google Fonts <link>s present in the framework's own host/templates/index.html; without them the scaffolded UI silently falls back to system fonts.
antosubash
marked this pull request as ready for review
May 6, 2026 10:20
Workspace mode was leaving two Makefiles — the workspace template's at the root and a duplicate `host/Makefile` shipped by the host template for flat mode. Strip the host copy in workspace mode (alongside the existing .env.example / .gitignore / README.md cleanup) and inline the host's targets into the workspace Makefile so `cd host && ...` is the only indirection. The background_tasks recipe already writes docker-compose.yml + scripts/run_worker.py + docker/worker.Dockerfile to the project root in both modes.
The background_tasks recipe already shipped a worker image and a docker-compose with redis/postgres/worker/beat. Add a matching docker/host.Dockerfile (multi-stage Node-then-Python so the Vite bundle gets baked in) and a `host` service in docker-compose so `docker compose up -d` brings the API up too. Migrations run on container start.
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes#117.
sm new my-appnow lays down a workspace mirroring the framework repo's own layout. The host moves underhost/; a samplehellomodule lands undermodules/hello/; the project root carries the workspace plumbing (pyproject.tomlwith[tool.uv.workspace] members = ["host", "modules/*"],package.jsonwithworkspaces: ["host/client_app", "modules/*"], and a Makefile that delegates to the host).This is the keystone fix called out in issue 117's update comment — once host + modules + host/client_app are all in one npm workspace, vite's normal walk-up resolution finds bare imports (
@simple-module-py/ui/...,lucide-react,sonner, etc.) without per-module aliases or symlinks.--flatkeeps today's single-host layout for users who only consume published modules.New scaffold layout (workspace mode, default)
What changed
templates/workspace/(project-root pyproject, package.json, Makefile, .env.example, .gitignore, README.md.tpl).templates/host/pyproject.toml.tpldrops the inlined[tool.uv.workspace]block (the workspace lives at the project root now).scaffolding.create_workspace()materializes the workspace shell;create_host()is unchanged but is now called withdest=target/hostin workspace mode.app_project.create_app_project()orchestrates: workspace → host → sample module → recipe pass. In workspace mode, host duplicates of.env.example/.gitignore/README.mdare stripped so the workspace copies stay canonical, and framework npm deps are merged intohost/client_app/package.jsoninstead of a top-level package.json.sqlite+aiosqlite:///./host/app.dbin workspace mode socd host && alembic ...andcd host && uvicorn ...agree on the path.Out of scope (deferred)
The issue also asks the sample module to register a menu item, permission, and rendered
Hello.tsxpage. The structural workspace fix is the keystone; extending the sample module would require either bloatingsm create-module's universal template or adding sample-specific post-processing. Filing as a follow-up keeps this PR focused on the layout —make devagainst the freshly-scaffolded project boots cleanly today (the sample registers its API route, just no UI yet).Test plan
uv run pytest framework/cli/tests/— 101 passeduv run pytest— 970 passeduv run ruff format --check . && uv run ruff check . && uv run ty checkuv run python scripts/check_file_size.py(300-line cap)uv run python scripts/check_metadata.py && check_readmes.py && check_hardcoded_strings.pynpx biome ci ./tmpand inspecting the tree + key file contents.cd <scaffolded>/ && make install && make devend-to-end from a clean checkout.https://claude.ai/code/session_01KckXzhEceQ1qf2WSgNLVur
Generated by Claude Code