diff --git a/CHANGELOG.md b/CHANGELOG.md index f23dc77aa..50914dcc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project are documented in this file. The format is b ### Fixed +- The first-module guidance now agrees with itself. The guides (`docs/index.md`, quickstart, installation, Your first module) wire a new module with `smpy add ../modules/orders --path host`, the command `smpy create-module` prints, which adds it to `host/pyproject.toml` as a workspace source. They used to say `uv add ./modules/orders`, which in a `smpy new` workspace edits the root `pyproject.toml` instead. A flat app uses `smpy add ./modules/orders`. The landing page `smpy new` generates drops the `create-admin` step (the `/setup` wizard already creates the first administrator), lists the wiring and migration steps after `create-module`, and its "Read the docs" button opens the first-module guide on docs.py.simplemodule.dev instead of the GitHub repo. The first-module guide and the `simple-module-conventions` skill note that SQLModel's `Field(regex=...)` is accepted but not enforced on a DTO, and show a `field_validator` instead (#898). - A fresh app's `make dev` no longer prints Vite errors or warnings, and `npm audit` reports nothing. The `simple_module_settings` wheel no longer ships `ui/components/validation-errors.test.ts`: that test made the host's Vite dependency scan fail with "Failed to run dependency scan … vitest". It now lives in `modules/settings/tests-js/`, and `make ci-build-packages` fails if any wheel contains a JS or TS `*.test.*` or `*.spec.*` file, a `test-dom.tsx` or a `page-tests/` directory. The scaffold's `client_app/vite.config.ts` loads under Vite 8's native config loader: it uses `import.meta.dirname` and `.ts` import extensions, and the template `tsconfig.json` sets `allowImportingTsExtensions`. It also uses `optimizeDeps.rolldownOptions` instead of the deprecated `esbuildOptions`. New apps' root `package.json` overrides `postcss-selector-parser` to `^6.1.5`, which is what `@tailwindcss/typography` pulls in, as this repo does. An existing app can copy `host/client_app/vite.config.ts` and `tsconfig.json` from a new app, and add the override (#900). - The `Browse`, `Create` and `Edit` pages that `smpy create-module` (and `make new-module`) generates now render inside the app shell: each sets `.layout = [AuthenticatedLayout]`. Before, they had no sidebar, header or footer, so the module's sidebar entry led to a bare page with no way back. To fix a module generated earlier, add `import { AuthenticatedLayout } from '@simple-module-py/ui/layouts/AuthenticatedLayout';` and that assignment to each of its pages, or use `AdminLayout` for pages you moved under `/admin` and `ADMIN_SIDEBAR` (#897). - A fresh `smpy new` app passes its own `make lint`. `host/alembic.ini` gains a `[post_write_hooks]` entry that runs the project's ruff (`check --select I --fix`, then `format`) on every new revision, so the initial-schema revision and each `make migration` output no longer fail `ruff format --check` and `ruff check` (this repo's `host/alembic.ini` has it too); a standalone `smpy create-host` project, which has no workspace root, gets a `dev` dependency group with `ruff` for the hook. `create-module` sorts the `module.py` constants import for any module name (it was unsorted for names that sort before `group`, such as `bookmarks`). The `# ty: ignore[invalid-assignment]` in `contracts/schemas.py` is generated only for a module inside this repo, where ty checks that rule, and new apps pin `ty>=0.0.90,<0.0.100`, since ty 0.0.100 renamed the `unresolved-attribute` rule the scaffold's config names. The hooks use Alembic's `module` runner, so `simple_module_db` and the host template now require `alembic>=1.16.3`. An existing app can copy the `[post_write_hooks]` block from a new app's `host/alembic.ini` (and raise its own `alembic` requirement to `>=1.16.3`), drop the unused ignore comments and pin ty the same way (#896). diff --git a/docs/guide/first-module.md b/docs/guide/first-module.md index 8ca9a2c0e..e1b48f96b 100644 --- a/docs/guide/first-module.md +++ b/docs/guide/first-module.md @@ -8,10 +8,10 @@ Assumes you've completed the [Quickstart](/guide/quickstart) and have an app on ```bash smpy create-module orders --dest modules/orders -uv add ./modules/orders +smpy add ../modules/orders --path host ``` -This creates `modules/orders/` with a working CRUD implementation against a single-field `Order` table, and adds the package to your app's dependencies. Open the generated files in your editor — you'll see the full file layout described in [project structure](/guide/project-structure). +This creates `modules/orders/` with a working CRUD implementation against a single-field `Order` table. `smpy add` wires it into the host: it adds `simple_module_orders` to `host/pyproject.toml` as a uv workspace source (`{ workspace = true }`), runs `uv sync` and regenerates the page manifest. `create-module` prints this same command. In a `smpy new --flat` app the module is not a workspace member, so wire it with `smpy add ./modules/orders` instead. Open the generated files in your editor — you'll see the full file layout described in [project structure](/guide/project-structure). New modules put shared settings and constants in `core/`, HTTP code in `web/`, and reusable frontend code in `ui/`. Root lookup paths (`models.py`, `contracts/`, @@ -77,6 +77,27 @@ class OrderOut(SQLModel): DTOs are plain `SQLModel` subclasses — **not** `BaseModel` and **not** `table=True`. They're the public surface other modules import from `orders.contracts`. +Length and range limits (`min_length`, `max_length`, `ge`, `le`) are enforced, but a format check is not. SQLModel's `Field` accepts `regex=` and silently **ignores** it, and it rejects pydantic's `pattern=` with a `TypeError`. Check formats with a `field_validator`, as `tenants.contracts.TenantCreate` does (the scaffold's `_reject_explicit_null` is the same mechanism). For example, to restrict `status`, give the `OrderUpdate` above a validator: + +```python +import re + +from pydantic import field_validator + +_STATUS_RE = re.compile(r"^[a-z_]{1,20}$") + + +class OrderUpdate(SQLModel): + status: str | None = Field(default=None, max_length=20) + + @field_validator("status") + @classmethod + def _status_shape(cls, value: str | None) -> str | None: + if value is not None and not _STATUS_RE.fullmatch(value): + raise ValueError("status must be lowercase letters and underscores") + return value +``` + ## 4. Generate a migration ```bash diff --git a/docs/guide/installation.md b/docs/guide/installation.md index c2dc86922..5f18b4d09 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -124,13 +124,17 @@ Then `make migrate && make dev`. smpy create-module orders --dest modules/orders ``` -That generates a publishable module package at `modules/orders/` with the starter layout — `module.py` (its `ModuleBase` subclass + entry point), `core/services.py`, `core/settings.py`, a REST `web/endpoints/api.py`, an empty `pages/` dir, tests, and `pyproject.toml`/`package.json`/`tsconfig.json`. Flesh out models, contracts, views, and pages as your domain needs them. Add the package to your app's dependencies and re-sync: +That generates a publishable module package at `modules/orders/` with the starter layout — `module.py` (its `ModuleBase` subclass + entry point), `core/services.py`, `core/settings.py`, a REST `web/endpoints/api.py`, an empty `pages/` dir, tests, and `pyproject.toml`/`package.json`/`tsconfig.json`. Flesh out models, contracts, views, and pages as your domain needs them. Wire it into the host, create its tables and restart: ```bash -uv add ./modules/orders +smpy add ../modules/orders --path host +make migration msg="add orders tables" +make migrate make dev ``` +In a `smpy new --flat` app the module is not a workspace member, so wire it with `smpy add ./modules/orders` instead. + The full walkthrough is in [Your first module](/guide/first-module). ## Update framework versions diff --git a/docs/guide/quickstart.md b/docs/guide/quickstart.md index bdeabefc4..40b29ffdf 100644 --- a/docs/guide/quickstart.md +++ b/docs/guide/quickstart.md @@ -37,7 +37,7 @@ The API and Vite dev servers start side by side. Visit: ## 4. Create an admin -In another terminal, from inside `myapp`: +Until an administrator exists, every page redirects to the `/setup` wizard, which asks for one. To create it from a terminal instead (from inside `myapp`): ```bash uv run smpy users create-admin --email admin@example.com --password changeme @@ -49,25 +49,23 @@ Sign in at `/users/login` and you land on the dashboard. ```bash smpy create-module orders --dest modules/orders -uv add ./modules/orders +smpy add ../modules/orders --path host ``` -This generates a publishable starter module at `modules/orders/` with: +This generates a publishable module at `modules/orders/` with a working CRUD sample for one `Order` table: - `pyproject.toml` — `[project.entry-points.simple_module]` → `orders.module:OrdersModule` - `package.json` + `tsconfig.json` — JS workspace metadata so `tsc --noEmit` covers the module's `.tsx` pages. -- `orders/module.py` — `ModuleBase` subclass with `meta = ModuleMeta(name="Orders", ...)`, wiring `register_routes` and `register_settings`. -- `orders/core/services.py` — module-scoped state container (stored on `app.state.orders` by `register_settings`). -- `orders/core/settings.py` — the module's `pydantic_settings` settings class. -- `orders/web/endpoints/api.py` — starter REST endpoints at `/api/orders`. -- `orders/pages/` — empty page dir; add `.tsx` pages (and a `register_routes` view router) as you build views. -- `tests/test_orders.py` — pytest smoke test. Named after the package so a repo - with several modules doesn't collide at collection: without `tests/__init__.py`, - pytest derives the test module's name from the basename alone. - -You add the domain model (`models.py`), DTOs (`contracts/`), service, Inertia views, and pages yourself — the [first-module guide](/guide/first-module) walks through that. - -`uv add ./modules/orders` adds the package to your app's dependencies; on the next `uv sync` (which `uv add` also runs) the entry point is registered and the module becomes discoverable on next boot. +- `orders/module.py` — `ModuleBase` subclass with `meta = ModuleMeta(name="Orders", ...)`: settings, routes, a sidebar menu item and permissions. +- `orders/models.py` — the `Order` table; `orders/contracts/schemas.py` — its DTOs. +- `orders/core/` — `constants.py` (permissions, page keys, menu), `settings.py` (a `DbBackedSettings` class) and `services.py` (stored on `app.state.orders`). +- `orders/web/` — the service plus REST endpoints at `/api/orders` and Inertia views at `/orders/`. +- `orders/pages/` — `Browse`, `Create` and `Edit` pages; `orders/locales/en.json` — their strings. +- `tests/` — pytest tests mirroring the areas (`tests/core/…`, `tests/web/crud/…`), with module-prefixed basenames such as `test_orders_crud.py`. + +The [first-module guide](/guide/first-module) shows how to grow it: your own fields, validation, a migration and tests. + +`smpy add ../modules/orders --path host` adds the package to `host/pyproject.toml` as a uv workspace source and runs `uv sync`, which registers its entry point; the module is discovered on the next boot. In a `smpy new --flat` app the module is not a workspace member, so wire it with `smpy add ./modules/orders` instead. ## 6. Generate a migration diff --git a/docs/index.md b/docs/index.md index 3e869c8d1..4dc46c25a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -72,10 +72,12 @@ Then in another terminal, inside `myapp`: ```bash smpy create-module orders --dest modules/orders -uv add ./modules/orders +smpy add ../modules/orders --path host +make migration msg="add orders tables" # review the generated revision +make migrate ``` -That generates `modules/orders/` with a `ModuleMeta`, a SQLModel table, contracts, a service, REST + Inertia endpoints, three React pages, locales, and a smoke test — all wired in once the dev server reloads. The full walkthrough is in [Your first module](/guide/first-module). +That generates `modules/orders/` with a `ModuleMeta`, a SQLModel table, contracts, a service, REST + Inertia endpoints, three React pages, locales, and a smoke test, wires it into the host and creates its table; the dev server picks it up when it reloads. The full walkthrough is in [Your first module](/guide/first-module). ## Where to start diff --git a/docs/superpowers/specs/2026-11-11-first-module-guidance-design.md b/docs/superpowers/specs/2026-11-11-first-module-guidance-design.md new file mode 100644 index 000000000..2e2a6cf62 --- /dev/null +++ b/docs/superpowers/specs/2026-11-11-first-module-guidance-design.md @@ -0,0 +1,28 @@ +# First-module guidance agrees with itself (#898) + +## Scope + +- **Wiring.** The guides said `uv add ./modules/orders`. In a `smpy new` workspace that runs + against the workspace root, which is `package = false`, not against the host. + `smpy create-module` prints `smpy add ../modules/orders --path host`, which writes a + `{ workspace = true }` source into `host/pyproject.toml`; this was verified in a fresh + workspace app. Every guide now uses that command, with `smpy add ./modules/orders` for a + `--flat` app, also verified. The steps now also include the migration that follows. +- **Landing page template.** + - Drop "Create an admin": `/setup` creates the first administrator, and `/` redirects + to `/setup` until it does. + - List create-module, then wire, then migrate. + - "Read the docs" goes to `https://docs.py.simplemodule.dev/guide/first-module` (live, + 200). +- **DTO validation.** Verified on SQLModel 0.0.48: `Field(regex=...)` is accepted and + ignored on a non-table model, and `Field(pattern=...)` raises `TypeError`. The + first-module guide gets a tested `field_validator` example, and the conventions skill + gets the rule. `tenants.contracts.TenantCreate` already works around it this way. + +Also fixed: the quickstart's list of what `create-module` generates (it described an empty `pages/` dir and a single `tests/test_orders.py`). + +## Testing + +- `test_landing_page_next_steps_match_the_first_module_guide`: fails on `main`, passes now. +- Biome check on the template. +- The DTO example was run: `"pending"` is accepted, `"Bad Value"` is rejected. diff --git a/framework/cli/simple_module_cli/templates/host/client_app/pages/Landing.tsx b/framework/cli/simple_module_cli/templates/host/client_app/pages/Landing.tsx index 697bbb2fa..8daf28136 100644 --- a/framework/cli/simple_module_cli/templates/host/client_app/pages/Landing.tsx +++ b/framework/cli/simple_module_cli/templates/host/client_app/pages/Landing.tsx @@ -20,9 +20,7 @@ export default function Landing({ isAuthenticated }: LandingProps) {

simple_module_py

-

- Your app is up. -

+

Your app is up.

This is the host's landing page — generated by{' '} smpy new. @@ -42,7 +40,7 @@ export default function Landing({ isAuthenticated }: LandingProps) { {primaryLabel} Read the docs @@ -53,15 +51,26 @@ export default function Landing({ isAuthenticated }: LandingProps) {

Next steps