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-pages Bot 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
antosubash force-pushed the claude/github-issue-117-jMAyb branch from b022d33 to d14798b Compare May 3, 2026 17:03
claude and 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 main May 6, 2026
12 checks passed
Sign up for free to 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