Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ All notable changes to this project are documented in this file. The format is b

### Fixed

- The `simple-module-creating` and `simple-module-conventions` skills, which ship inside `simple_module_cli`, linked the module layout standard as `../../docs/module-layout.md`. That path does not exist in an installed CLI. They now link https://docs.py.simplemodule.dev/module-layout, and a test fails if a shipped skill links a file outside `skills/`. `docs/module-layout.md` gains a "Downstream modules" section on copying this repo's root-layout test (`ALLOWED_ROOT`, `DOMAIN_AREAS`) into an app or a standalone module repo (#875).
- `smpy new` warns when the CLI running it is older than the latest `simple_module_cli` on PyPI. A new app pins the framework to the CLI's own version, and `uvx --from simple_module_cli` quietly reuses a copy installed with `uv tool install`, so an old tool used to scaffold an outdated app without saying so. The warning names both fixes: `uv tool upgrade simple_module_cli`, or `uvx --from simple_module_cli@latest`. The check is best effort, with a 3-second timeout; offline or on any PyPI error it is skipped, and it never blocks the scaffold. A CLI that is itself a pre-release, dev or local build is never warned. The README, the CLI and hosting READMEs and the landing page's copy command now use `uvx --from simple_module_cli@latest`, and the installation and quickstart guides, the docs home and the make-commands reference say how to upgrade an installed CLI, with `pipx upgrade` as the alternative (#899).
- 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).
Expand Down
19 changes: 19 additions & 0 deletions docs/module-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,25 @@ root-layout test too. A shim that is a plain re-export instead (not an alias) mu
by hand to the expiry test's `_SHIM_MODULES` tuple and `_EXPECTED` set and to
the root-layout test's `DEPRECATED_ROOT` map until removed.

## Downstream modules

A module created with `smpy create-module` already follows this layout. The
check that keeps it that way lives in this repo's
[`host/tests/test_module_layout.py`](https://github.com/antosubash/simple_module_python/blob/main/host/tests/test_module_layout.py),
which an app or a standalone module repo does not have. To keep the same ratchet:

1. Copy that test into your own tests, for example `tests/test_module_layout.py`. <!-- path-check: ignore -->
2. Point `MODULES` at your modules directory (`modules/` in a `smpy new` app). The
copy computes it with `parents[2]`, which is right for `host/tests/`; from
`tests/test_module_layout.py` use `parents[1]`. <!-- path-check: ignore -->
A standalone module repo has the package at its root, with no `modules/<name>/`
level, so change the `package`, `pyproject.toml` and `tests/` paths in the test too.
3. Replace `DOMAIN_AREAS` with your own modules' named areas, and empty
`DEPRECATED_ROOT` unless you keep root-level shims.

`ALLOWED_ROOT` is the root lookup list in [Package root](#package-root-lookup-paths);
keep the two in step when you upgrade the framework.

## Moving an existing module

One module per PR; no logic changes in a move PR.
Expand Down
16 changes: 16 additions & 0 deletions docs/superpowers/specs/2026-11-11-skill-links-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Shipped skills link to files the wheel contains (#875)

`skills/` is installed as `simple_module_cli/skills/` (a symlink in the repo). Two skills
linked `../../docs/module-layout.md`, which points outside the installed package.

## Decisions

- Link the docs site, `https://docs.py.simplemodule.dev/module-layout` (returns 200),
instead of shipping `docs/` in the wheel. The site follows `main` and needs no new
packaging. A GitHub link at a tag would need a version stamp.
- Add `framework/cli/tests/test_cli_skill_links.py`: every relative link (inline,
titled, angle-bracket or reference-style, outside code fences) in any Markdown file
under `skills/` must resolve inside `skills/` and exist. It fails on `main` and passes now.
- Add a "Downstream modules" section to `docs/module-layout.md`: copy
`host/tests/test_module_layout.py`, point `MODULES` at the app's modules, and set your
own `DOMAIN_AREAS`. The issue asked for this.
32 changes: 32 additions & 0 deletions framework/cli/tests/test_cli_skill_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Shipped skills link only to files the CLI wheel contains (GH #875).

`skills/` is installed as `simple_module_cli/skills/`, so a relative link that
leaves it (`../../docs/...`) points nowhere in a downstream app. Repo docs are
linked through the docs site instead.
"""

from __future__ import annotations

import re
from pathlib import Path

import pytest

_SKILLS = Path(__file__).resolve().parents[3] / "skills"
_LINK = re.compile(r"\]\(<?([^)\s>]+)>?(?:\s+\"[^\"]*\")?\)|^\s*\[[^\]]+\]:\s*<?(\S+?)>?\s*$", re.M)


def _relative_links(skill_md: Path) -> list[str]:
text = re.sub(r"^(```|~~~).*?^\1", "", skill_md.read_text(), flags=re.M | re.S)
targets = [(m.group(1) or m.group(2)).split("#", 1)[0] for m in _LINK.finditer(text)]
return [t for t in targets if t and not re.match(r"[a-z][a-z0-9+.-]*:", t, re.I)]


@pytest.mark.parametrize(
"skill_md", sorted(_SKILLS.glob("**/*.md")), ids=lambda p: str(p.relative_to(_SKILLS))
)
def test_relative_links_stay_inside_the_shipped_skills(skill_md: Path) -> None:
for target in _relative_links(skill_md):
resolved = (skill_md.parent / target).resolve()
assert resolved.is_relative_to(_SKILLS.resolve()), f"{target} leaves skills/"
assert resolved.exists(), f"{target} does not exist"
2 changes: 1 addition & 1 deletion skills/simple-module-conventions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Treat 300 lines as the design pressure on every `.py`, `.ts`, and `.tsx` file (t
New module settings and their app-state container live in `core/settings.py`
and `core/services.py`; HTTP dependencies and services live in `web/`. Keep
root lookup files in place and put non-page frontend code in `ui/`. See
[Module layout](../../docs/module-layout.md).
[Module layout](https://docs.py.simplemodule.dev/module-layout).

### 3. Per-module settings: `register_module_settings` → `app.state.<module>`

Expand Down
2 changes: 1 addition & 1 deletion skills/simple-module-creating/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Modules use this area layout.
Area initializers hold only a docstring. Private helpers stay in their area.
Tests have no `__init__.py`, keep `conftest.py` at their root and use globally
unique `test_orders_*.py` basenames; JS tests stay in `tests-js/`.
See [Module layout](../../docs/module-layout.md) for the lookup consumers and shim policy.
See [Module layout](https://docs.py.simplemodule.dev/module-layout) for the lookup consumers and shim policy.

## The contract: pyproject.toml + module.py

Expand Down
Loading