Skip to content

feat(cli): sm new scaffolds into a uv/npm workspace with host/ + modules/ - #121

Merged
antosubash merged 9 commits into
mainfrom
claude/github-issue-117-jMAyb
May 6, 2026
Merged

feat(cli): sm new scaffolds into a uv/npm workspace with host/ + modules/#121
antosubash merged 9 commits into
mainfrom
claude/github-issue-117-jMAyb

Conversation

@antosubash

Copy link
Copy Markdown
Owner

Summary

Closes#117. sm new my-app now lays down a workspace mirroring the framework repo's own layout. The host moves under host/; a sample hello module lands under modules/hello/; the project root carries the workspace plumbing (pyproject.toml with [tool.uv.workspace] members = ["host", "modules/*"], package.json with workspaces: ["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.

--flat keeps today's single-host layout for users who only consume published modules.

New scaffold layout (workspace mode, default)

my-app/
├── package.json # workspaces: ["host/client_app", "modules/*"]
├── pyproject.toml # [tool.uv.workspace] members = ["host", "modules/*"]
├── Makefile # delegates to host/ via cd host or uv run --project host
├── .env.example
├── host/
│ ├── pyproject.toml # framework deps + [tool.uv.sources] for sample
│ ├── main.py
│ ├── alembic.ini
│ ├── migrations/
│ └── client_app/ # vite root; npm workspace member
└── modules/
└── hello/ # sample module — copy/rename to add your own

What changed

  • New templates/workspace/ (project-root pyproject, package.json, Makefile, .env.example, .gitignore, README.md.tpl).
  • templates/host/pyproject.toml.tpl drops 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 with dest=target/host in workspace mode.
  • app_project.create_app_project() orchestrates: workspace → host → sample module → recipe pass. In workspace mode, host duplicates of .env.example / .gitignore / README.md are stripped so the workspace copies stay canonical, and framework npm deps are merged into host/client_app/package.json instead of a top-level package.json.
  • SQLite default URL becomes sqlite+aiosqlite:///./host/app.db in workspace mode so cd host && alembic ... and cd 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.tsx page. The structural workspace fix is the keystone; extending the sample module would require either bloating sm create-module's universal template or adding sample-specific post-processing. Filing as a follow-up keeps this PR focused on the layout — make dev against 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 passed
  • uv run pytest — 970 passed
  • uv run ruff format --check . && uv run ruff check . && uv run ty check
  • uv run python scripts/check_file_size.py (300-line cap)
  • uv run python scripts/check_metadata.py && check_readmes.py && check_hardcoded_strings.py
  • npx biome ci .
  • Smoke-tested both modes by scaffolding into /tmp and inspecting the tree + key file contents.
  • Manual: cd <scaffolded>/ && make install && make dev end-to-end from a clean checkout.

https://claude.ai/code/session_01KckXzhEceQ1qf2WSgNLVur


Generated by Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented May 3, 2026

Copy link
Copy Markdown

Deploying simple-module-python with Cloudflare Pages Cloudflare Pages

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

View logs

claude added 4 commits May 3, 2026 16:40
`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.
@antosubash
antosubashforce-pushed the claude/github-issue-117-jMAyb branch from b022d33 to d14798bCompareMay 3, 2026 17:03
claudeand others added 3 commits May 3, 2026 17:04
`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
antosubash marked this pull request as ready for review May 6, 2026 10:20
claude added 2 commits May 6, 2026 10:38
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.
@antosubash
antosubash merged commit 85b49eb into mainMay 6, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sm new should scaffold host + a working sample module in a uv/npm workspace, matching the framework repo's own layout

2 participants

@antosubash@claude