simple-module-cli 0.0.34__tar.gz → 0.0.36__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/PKG-INFO +1 -1
  2. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/pyproject.toml +1 -1
  3. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/app_project.py +15 -4
  4. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/skills/simple-module-migrations/SKILL.md +7 -9
  5. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/docker/host-flat.Dockerfile +2 -2
  6. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/docker/host.Dockerfile +2 -2
  7. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/client_app/app.tsx +19 -5
  8. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/client_app/package.json.tpl +5 -5
  9. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/client_app/pages.ts +8 -1
  10. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/migrations/env.py +6 -3
  11. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/migrations/script.py.mako +1 -0
  12. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/routes.py +1 -1
  13. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/templates/index.html +2 -1
  14. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/.github/workflows/ci.yml +6 -6
  15. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +2 -2
  16. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/__PACKAGE__/endpoints/views.py.tpl +1 -1
  17. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/_optional/standalone/package.json.tpl +5 -3
  18. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/package.json.tpl +1 -1
  19. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/tests/test___PACKAGE__.py.tpl +5 -2
  20. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_new_regressions.py +57 -3
  21. simple_module_cli-0.0.36/tests/test_module_css_subdir_sources.py +47 -0
  22. simple_module_cli-0.0.36/tests/test_scaffolded_host_setup_wizard.py +57 -0
  23. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_scaffolding_host.py +11 -0
  24. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_scaffolding_module.py +10 -0
  25. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_scaffolding_module_js.py +31 -0
  26. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/.gitignore +0 -0
  27. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/LICENSE +0 -0
  28. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/README.md +0 -0
  29. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/__init__.py +0 -0
  30. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/_env.py +0 -0
  31. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/_module_build.py +0 -0
  32. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/_module_host.py +0 -0
  33. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/_templating.py +0 -0
  34. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/add_cmd.py +0 -0
  35. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/case.py +0 -0
  36. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/catalog.py +0 -0
  37. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/cli.py +0 -0
  38. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/docker_assets.py +0 -0
  39. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/git_source.py +0 -0
  40. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/module_cmd.py +0 -0
  41. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/new.py +0 -0
  42. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/package_update.py +0 -0
  43. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/pins.py +0 -0
  44. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/plugins.py +0 -0
  45. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/pypi.py +0 -0
  46. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/pyproject_edit.py +0 -0
  47. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/recipes.py +0 -0
  48. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/requirements.py +0 -0
  49. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/scaffolding.py +0 -0
  50. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/skills/README.md +0 -0
  51. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/skills/simple-module-cli/SKILL.md +0 -0
  52. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
  53. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/skills/simple-module-creating/SKILL.md +0 -0
  54. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
  55. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/skills/simple-module-doctor/SKILL.md +0 -0
  56. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
  57. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
  58. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/skills/simple-module-registries/SKILL.md +0 -0
  59. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
  60. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/skills_cmd.py +0 -0
  61. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/docker/Makefile.snippet +0 -0
  62. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/docker/compose-base-postgres.yml.tpl +0 -0
  63. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/docker/compose-base-sqlite.yml.tpl +0 -0
  64. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/docker/compose-tasks-postgres.yml.tpl +0 -0
  65. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/docker/compose-tasks-sqlite.yml.tpl +0 -0
  66. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/docker/dockerignore +0 -0
  67. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/.env.example +0 -0
  68. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/.gitignore +0 -0
  69. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/Makefile +0 -0
  70. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/README.md.tpl +0 -0
  71. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
  72. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
  73. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/alembic.ini +0 -0
  74. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
  75. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/client_app/module-assets.ts +0 -0
  76. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
  77. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
  78. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/client_app/styles.css +0 -0
  79. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
  80. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/client_app/vite.config.ts +0 -0
  81. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/client_app/vite.dev-url.ts +0 -0
  82. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/main.py +0 -0
  83. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
  84. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/host/pyproject.toml.tpl +0 -0
  85. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/.gitignore +0 -0
  86. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/README.md.tpl +0 -0
  87. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
  88. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
  89. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
  90. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
  91. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/__PACKAGE__/pages/Index.tsx.tpl +0 -0
  92. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
  93. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
  94. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/_optional/standalone/tsconfig.json.tpl +0 -0
  95. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
  96. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
  97. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/workspace/.env.example +0 -0
  98. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/workspace/.gitignore +0 -0
  99. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/workspace/Makefile +0 -0
  100. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/workspace/README.md.tpl +0 -0
  101. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
  102. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -0
  103. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/update_cmd.py +0 -0
  104. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/simple_module_cli/wizard.py +0 -0
  105. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/conftest.py +0 -0
  106. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_build_packaging.py +0 -0
  107. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_case.py +0 -0
  108. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_add_cmd.py +0 -0
  109. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_add_spec_parse.py +0 -0
  110. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_catalog.py +0 -0
  111. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_docker_assets.py +0 -0
  112. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_git_scan.py +0 -0
  113. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_new.py +0 -0
  114. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_new_dest_tolerance.py +0 -0
  115. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_new_git_module.py +0 -0
  116. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_new_migration_bootstrap.py +0 -0
  117. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_new_scaffold_layout.py +0 -0
  118. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_package_update.py +0 -0
  119. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_package_update_pins.py +0 -0
  120. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_pyproject_edit.py +0 -0
  121. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_recipes.py +0 -0
  122. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_update_sources.py +0 -0
  123. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_vite_port_env.py +0 -0
  124. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_wizard.py +0 -0
  125. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_cli_wizard_git.py +0 -0
  126. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_create_module_ci_skip.py +0 -0
  127. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_env_helper.py +0 -0
  128. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_module_cmd_build.py +0 -0
  129. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_module_cmd_e2e.py +0 -0
  130. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_module_cmd_verify.py +0 -0
  131. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_module_css_assets.py +0 -0
  132. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_module_css_build.py +0 -0
  133. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_module_npm_aliases.py +0 -0
  134. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_module_pages_manifest.py +0 -0
  135. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_no_framework_deps.py +0 -0
  136. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_plugin_discovery.py +0 -0
  137. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_scaffold_rollback.py +0 -0
  138. {simple_module_cli-0.0.34 → simple_module_cli-0.0.36}/tests/test_skills_cmd.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simple_module_cli
3
- Version: 0.0.34
3
+ Version: 0.0.36
4
4
  Summary: Standalone scaffolder for the SimpleModule framework — `smpy new`, `smpy create-module`, plugin host.
5
5
  Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
6
  Project-URL: Repository, https://github.com/antosubash/simple_module_python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_cli"
3
- version = "0.0.34"
3
+ version = "0.0.36"
4
4
  description = "Standalone scaffolder for the SimpleModule framework — `smpy new`, `smpy create-module`, plugin host."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -61,13 +61,24 @@ _APP_NPM_DEPS = {
61
61
  "@simple-module-py/i18n": _FRAMEWORK_VERSION,
62
62
  "react": "^19.0.0",
63
63
  "react-dom": "^19.0.0",
64
- "@inertiajs/react": "^2.0.0",
64
+ "@inertiajs/react": "^3.7.0",
65
65
  }
66
+ # Flat mode has no npm workspaces, so `client_app/package.json` is never
67
+ # installed — the root package.json seeded from these two dicts is the only
68
+ # manifest npm reads. It must therefore cover everything client_app needs to
69
+ # build, not just the framework pins: `vite.config.ts` imports
70
+ # `@tailwindcss/vite`, and `tsc` needs the @types packages. Keep this in step
71
+ # with templates/host/client_app/package.json.tpl.
66
72
  _APP_NPM_DEV_DEPS = {
67
73
  "@simple-module-py/tsconfig": _FRAMEWORK_VERSION,
68
- "@vitejs/plugin-react": "^5.0.0",
69
- "typescript": "^5.6.0",
70
- "vite": "^8.0.0",
74
+ "@tailwindcss/vite": "^4.0.0",
75
+ "@types/node": "^26.5.0",
76
+ "@types/react": "^19.0.0",
77
+ "@types/react-dom": "^19.0.0",
78
+ "@vitejs/plugin-react": "^6.1.1",
79
+ "tailwindcss": "^4.0.0",
80
+ "typescript": "^7.0.2",
81
+ "vite": "^8.2.2",
71
82
  }
72
83
 
73
84
  # Files the host template ships that the workspace template re-emits at
@@ -64,7 +64,7 @@ make migration msg="add orders module"
64
64
  make migrate
65
65
  ```
66
66
 
67
- `make migrate` runs `alembic -c host/alembic.ini upgrade heads` — note **`heads`** (plural). Branch labels create multiple independent heads, so `upgrade head` (singular) would error or under-apply.
67
+ `make migrate` runs `alembic -c host/alembic.ini upgrade heads` — note **`heads`** (plural). Histories with a branch label can end up with multiple heads, so `upgrade head` (singular) may error or under-apply.
68
68
 
69
69
  ### Branch labels — set on the FIRST revision per module
70
70
 
@@ -78,16 +78,14 @@ branch_labels = ("audit_log",) # ← add by hand on this first revision
78
78
  depends_on = None
79
79
  ```
80
80
 
81
- Why: it lets operators roll back **just one module's** schema, which is how a module is uninstalled cleanly:
81
+ Why: the label is a named target for that revision (`upgrade audit_log@head`, the module column in the Doctor). It does **not** isolate the module: autogenerate chains the revision off the current head, so history is one linear chain and `downgrade audit_log@base` walks the **entire chain beneath it, other modules included**. Never use it as a per-module rollback.
82
82
 
83
- ```bash
84
- make downgrade # back one revision; OR, to a target:
85
- uv run --project host alembic -c host/alembic.ini downgrade audit_log@base
86
- ```
83
+ To remove one module's schema:
87
84
 
88
- `audit_log@base` drops everything that module owns and leaves the others. Without the label, downgrading to a bare revision id walks linear history and rolls back unrelated modules' migrations sitting between.
85
+ - If the module's revisions are the latest in the chain, downgrade to the `down_revision` of its first revision: `uv run --project host alembic -c host/alembic.ini downgrade <that down_revision id>`.
86
+ - Otherwise write a dedicated migration that drops that module's tables.
89
87
 
90
- Subsequent revisions for the same module **don't** need a `branch_labels` — only the first; they inherit the branch.
88
+ Subsequent revisions for the same module **don't** need a `branch_labels` — only the first; they chain off it as ordinary revisions.
91
89
 
92
90
  > Exception: the repo's bootstrap `initial_schema` revision bundles several core modules (users, permissions, settings, file_storage, feature_flags, background_tasks) into one unlabeled root migration. The per-module `branch_labels` convention applies to modules added **after** that bootstrap.
93
91
 
@@ -112,7 +110,7 @@ SM010/SM011 are **not** emitted at boot or by `make doctor`. They only surface w
112
110
 
113
111
  ## Pitfalls
114
112
 
115
- - **Skipped the branch label on the first revision.** `alembic downgrade <module>@base` then fails or silently rolls back unrelated revisions. Fix by editing the migration file before applying; if already deployed, write a no-op revision that adds the label retroactively.
113
+ - **Treating `downgrade <module>@base` as a per-module rollback.** It rolls back every revision beneath the labelled one, other modules included. See the recipe under "Branch labels".
116
114
  - **Renamed a column.** Autogenerate emits `drop_column` + `add_column`, which loses data. Edit to `op.alter_column(..., new_column_name=...)` and write the matching `downgrade()`.
117
115
  - **Hand-edited operations with stubbed `downgrade()`.** Server defaults, CHECK constraints, expression indexes — Alembic can't always infer these. When you fill in `upgrade()`, fill in `downgrade()` too.
118
116
  - **Concurrent `make migrate` (`upgrade heads`) from multiple processes.** The `alembic_version` table isn't race-safe across all backends. Run upgrades from one place (a release pipeline step), not from the booting app.
@@ -15,8 +15,8 @@ ENV UV_LINK_MODE=copy \
15
15
 
16
16
  WORKDIR /app
17
17
 
18
- # Node 22 for the Vite build.
19
- RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
18
+ # Node 24 for the Vite build.
19
+ RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
20
20
  && apt-get install -y --no-install-recommends nodejs \
21
21
  && rm -rf /var/lib/apt/lists/*
22
22
 
@@ -15,8 +15,8 @@ ENV UV_LINK_MODE=copy \
15
15
 
16
16
  WORKDIR /app
17
17
 
18
- # Node 22 for the Vite build.
19
- RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
18
+ # Node 24 for the Vite build.
19
+ RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
20
20
  && apt-get install -y --no-install-recommends nodejs \
21
21
  && rm -rf /var/lib/apt/lists/*
22
22
 
@@ -1,6 +1,8 @@
1
1
  import { createInertiaApp, router } from '@inertiajs/react';
2
2
  import { configureI18n, updateI18n } from '@simple-module-py/i18n';
3
+ import { formatTitle, setTitleAppName } from '@simple-module-py/ui/lib/app-title';
3
4
  import { startSpaLinkInterception } from '@simple-module-py/ui/lib/spa-links';
5
+ import { initTheme } from '@simple-module-py/ui/lib/theme';
4
6
  import { createRoot } from 'react-dom/client';
5
7
  import { resolvePage } from './pages';
6
8
 
@@ -11,22 +13,34 @@ type I18nBlock = {
11
13
  };
12
14
 
13
15
  createInertiaApp({
16
+ title: (title) => formatTitle(title),
14
17
  resolve: async (name) => {
15
18
  return await resolvePage(name);
16
19
  },
17
20
  setup({ el, App, props }) {
18
- const initial = (props.initialPage.props as { i18n?: I18nBlock }).i18n;
21
+ const pageProps = props.initialPage.props as {
22
+ i18n?: I18nBlock;
23
+ branding?: { appName?: string | null };
24
+ };
25
+ setTitleAppName(pageProps.branding?.appName);
26
+ // Before the first render, so a dark-theme user never sees a light frame.
27
+ initTheme();
28
+ const initial = pageProps.i18n;
19
29
  configureI18n({
20
30
  locale: initial?.locale ?? 'en',
21
31
  messages: initial?.messages ?? {},
22
32
  });
23
- let activeLocale = initial?.locale ?? null;
24
33
  router.on('success', (event) => {
25
34
  const block = (event.detail.page.props as { i18n?: I18nBlock }).i18n;
26
- if (!block) return;
27
- if (block.locale !== activeLocale && block.messages) {
35
+ // A non-null `messages` payload IS the server's signal that the client
36
+ // needs it — the backend sends `null` whenever the catalog the client
37
+ // already holds is still good. Gating on a locale change instead drops
38
+ // the catalog that arrives when the *audience* changes: signing in swaps
39
+ // the anonymous snapshot for one including admin-only modules, at the
40
+ // same locale, so every admin screen rendered raw keys
41
+ // ("dashboard.home.title") until a hard refresh.
42
+ if (block?.messages) {
28
43
  updateI18n({ locale: block.locale, messages: block.messages });
29
- activeLocale = block.locale;
30
44
  }
31
45
  });
32
46
  // Authored content (pagebuilder widgets, markdown and rich-text fields)
@@ -8,7 +8,7 @@
8
8
  "preview": "vite preview"
9
9
  },
10
10
  "dependencies": {
11
- "@inertiajs/react": "^2.0.0",
11
+ "@inertiajs/react": "^3.7.0",
12
12
  "@simple-module-py/i18n": "{{FRAMEWORK_VERSION}}",
13
13
  "@simple-module-py/ui": "{{FRAMEWORK_VERSION}}",
14
14
  "react": "^19.0.0",
@@ -17,12 +17,12 @@
17
17
  "devDependencies": {
18
18
  "@simple-module-py/tsconfig": "{{FRAMEWORK_VERSION}}",
19
19
  "@tailwindcss/vite": "^4.0.0",
20
- "@types/node": "^22.0.0",
20
+ "@types/node": "^26.5.0",
21
21
  "@types/react": "^19.0.0",
22
22
  "@types/react-dom": "^19.0.0",
23
- "@vitejs/plugin-react": "^5.0.0",
23
+ "@vitejs/plugin-react": "^6.1.1",
24
24
  "tailwindcss": "^4.0.0",
25
- "typescript": "^5.7.0",
26
- "vite": "^6.0.0"
25
+ "typescript": "^7.0.2",
26
+ "vite": "^8.2.2"
27
27
  }
28
28
  }
@@ -15,7 +15,14 @@ import { moduleGlobs } from './modules.generated';
15
15
  type PageModule = { default: React.ComponentType<Record<string, unknown>> };
16
16
  type PageLoader = () => Promise<PageModule>;
17
17
 
18
- const hostPages = import.meta.glob<PageModule>('./pages/**/*.tsx');
18
+ // A `*.test.tsx` beside a page is a test, not a page — without these
19
+ // negations it registers as a phantom page and Vite follows the import into
20
+ // the production bundle.
21
+ const hostPages = import.meta.glob<PageModule>([
22
+ './pages/**/*.tsx',
23
+ '!./pages/**/*.test.tsx',
24
+ '!./pages/**/*.spec.tsx',
25
+ ]);
19
26
 
20
27
  const pages: Record<string, PageLoader> = {};
21
28
 
@@ -27,12 +27,15 @@ logger = logging.getLogger("alembic.env")
27
27
  config = context.config
28
28
 
29
29
  if config.config_file_name is not None:
30
- fileConfig(config.config_file_name)
30
+ # Not the default disable_existing_loggers=True: the setup wizard runs this
31
+ # in-process, and that default would silence every app logger until restart.
32
+ fileConfig(config.config_file_name, disable_existing_loggers=False)
31
33
 
32
34
  target_metadata = build_module_metadata()
33
35
  include_object = make_include_object(target_metadata)
34
- # Re-emit expression-based indexes (e.g. ``lower(email)``) that autogenerate
35
- # silently drops under SQLite. See ``make_process_revision_directives`` docstring.
36
+ # Re-emit expression-based indexes that autogenerate drops under SQLite, rewrite
37
+ # Boolean 0/1 server defaults to sa.false()/sa.true(), and warn that SQLite cannot
38
+ # verify expression indexes (GH #342) — see make_process_revision_directives.
36
39
  process_revision_directives = make_process_revision_directives(target_metadata)
37
40
 
38
41
 
@@ -8,6 +8,7 @@ Create Date: ${create_date}
8
8
  from collections.abc import Sequence
9
9
 
10
10
  import sqlalchemy as sa
11
+ import sqlmodel # noqa: F401 (autogenerate may emit sqlmodel types)
11
12
  from alembic import op
12
13
  ${imports if imports else ""}
13
14
 
@@ -10,8 +10,8 @@ module is installed.
10
10
  from __future__ import annotations
11
11
 
12
12
  from fastapi import APIRouter, Request
13
- from inertia import InertiaResponse
14
13
  from simple_module_hosting.inertia_deps import InertiaDep
14
+ from simple_module_inertia import InertiaResponse
15
15
 
16
16
  router = APIRouter()
17
17
 
@@ -7,8 +7,9 @@
7
7
  crawlers and link previews see the configured site name before React
8
8
  hydrates; Inertia's <Head> takes over per-page titles after that. #}
9
9
  {% set brand = branding_head(request) %}
10
- <title>{{ brand.app_name }}</title>
10
+ <title data-inertia="">{{ brand.app_name }}</title>
11
11
  {% if brand.theme_color %}<meta name="theme-color" content="{{ brand.theme_color }}" />{% endif %}
12
+ {% if brand.favicon_url %}<link rel="icon" href="{{ brand.favicon_url }}" />{% endif %}
12
13
  <link rel="preconnect" href="https://fonts.googleapis.com" />
13
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
15
  <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
@@ -12,10 +12,10 @@ jobs:
12
12
  test:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/checkout@v4
15
+ - uses: actions/checkout@v7
16
16
 
17
17
  - name: Install uv
18
- uses: astral-sh/setup-uv@v3
18
+ uses: astral-sh/setup-uv@v9.0.0
19
19
  with:
20
20
  version: latest
21
21
 
@@ -34,14 +34,14 @@ jobs:
34
34
  frontend:
35
35
  runs-on: ubuntu-latest
36
36
  steps:
37
- - uses: actions/checkout@v4
37
+ - uses: actions/checkout@v7
38
38
 
39
- - uses: actions/setup-node@v4
39
+ - uses: actions/setup-node@v7
40
40
  with:
41
- node-version: '22'
41
+ node-version: '24'
42
42
 
43
43
  - name: Install uv
44
- uses: astral-sh/setup-uv@v3
44
+ uses: astral-sh/setup-uv@v9.0.0
45
45
  with:
46
46
  version: latest
47
47
 
@@ -29,10 +29,10 @@ jobs:
29
29
  contents: read
30
30
 
31
31
  steps:
32
- - uses: actions/checkout@v4
32
+ - uses: actions/checkout@v7
33
33
 
34
34
  - name: Install uv
35
- uses: astral-sh/setup-uv@v3
35
+ uses: astral-sh/setup-uv@v9.0.0
36
36
  with:
37
37
  version: latest
38
38
 
@@ -3,8 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from fastapi import APIRouter
6
- from inertia import InertiaResponse
7
6
  from simple_module_hosting.inertia_deps import InertiaDep
7
+ from simple_module_inertia import InertiaResponse
8
8
 
9
9
  router = APIRouter()
10
10
 
@@ -9,19 +9,21 @@
9
9
  "peerDependencies": {
10
10
  "react": "^19.0.0",
11
11
  "react-dom": "^19.0.0",
12
- "@inertiajs/react": "^2.0.0",
12
+ "@inertiajs/react": "^3.7.0",
13
13
  "@simple-module-py/ui": "*"
14
14
  },
15
15
  "devDependencies": {
16
- "@inertiajs/react": "^2.0.0",
16
+ "@inertiajs/react": "^3.7.0",
17
17
  "@simple-module-py/i18n": "{{FRAMEWORK_VERSION}}",
18
18
  "@simple-module-py/tsconfig": "{{FRAMEWORK_VERSION}}",
19
19
  "@simple-module-py/ui": "{{FRAMEWORK_VERSION}}",
20
+ "@types/node": "^26.5.0",
20
21
  "@types/react": "^19.0.0",
21
22
  "@types/react-dom": "^19.0.0",
22
23
  "react": "^19.0.0",
23
24
  "react-dom": "^19.0.0",
24
- "typescript": "^5.7.0"
25
+ "typescript": "^7.0.2",
26
+ "vite": "^8.2.2"
25
27
  },
26
28
  "dependencies": {}
27
29
  }
@@ -6,7 +6,7 @@
6
6
  "peerDependencies": {
7
7
  "react": "^19.0.0",
8
8
  "react-dom": "^19.0.0",
9
- "@inertiajs/react": "^2.0.0",
9
+ "@inertiajs/react": "^3.7.0",
10
10
  "@simple-module-py/ui": "*"
11
11
  },
12
12
  "devDependencies": {
@@ -8,6 +8,7 @@ that package is installed). No conftest.py is required.
8
8
  from __future__ import annotations
9
9
 
10
10
  from {{PACKAGE_NAME}}.module import {{MODULE_NAME}}Module
11
+ from simple_module_test import effective_route_paths
11
12
 
12
13
 
13
14
  class TestMeta:
@@ -22,6 +23,8 @@ class TestRoutes:
22
23
  async def test_app_boots_with_module(self, build_test_app):
23
24
  """Module registers cleanly into a minimal FastAPI host."""
24
25
  app = build_test_app({{MODULE_NAME}}Module)
25
- paths = {getattr(r, "path", None) for r in app.routes}
26
+ # effective_route_paths reads the OpenAPI schema: FastAPI >= 0.137
27
+ # includes routers lazily, so ``app.routes`` no longer lists them.
28
+ paths = effective_route_paths(app)
26
29
  # The placeholder GET / endpoint lives under the route_prefix.
27
- assert any(p and p.startswith("/api/{{MODULE_SLUG}}") for p in paths)
30
+ assert any(p.startswith("/api/{{MODULE_SLUG}}") for p in paths)
@@ -13,9 +13,9 @@ from simple_module_cli.cli import app
13
13
  from typer.testing import CliRunner
14
14
 
15
15
 
16
- def test_sm_new_flat_pins_inertia_react_to_v2(tmp_path: Path) -> None:
16
+ def test_sm_new_flat_pins_inertia_react_to_v3(tmp_path: Path) -> None:
17
17
  """Issue #128: flat scaffold's root package.json must peer-match
18
- @simple-module-py/ui's ``@inertiajs/react: ^2.0.0`` peer dep."""
18
+ @simple-module-py/ui's ``@inertiajs/react: ^3.0.0`` peer dep."""
19
19
  runner = CliRunner()
20
20
  target = tmp_path / "demo"
21
21
  runner.invoke(
@@ -25,7 +25,7 @@ def test_sm_new_flat_pins_inertia_react_to_v2(tmp_path: Path) -> None:
25
25
  data = json.loads((target / "package.json").read_text())
26
26
  inertia = data.get("dependencies", {}).get("@inertiajs/react")
27
27
  assert inertia is not None
28
- assert inertia.startswith("^2."), f"expected ^2.x, got {inertia!r}"
28
+ assert inertia.startswith("^3."), f"expected ^3.x, got {inertia!r}"
29
29
 
30
30
 
31
31
  def test_sm_new_sample_module_pins_match_framework_version(tmp_path: Path) -> None:
@@ -211,6 +211,38 @@ def test_sm_new_rejects_mixed_separators(tmp_path: Path) -> None:
211
211
  assert result.exit_code != 0
212
212
 
213
213
 
214
+ def test_scaffold_adopts_every_locale_catalog_the_server_sends(tmp_path: Path) -> None:
215
+ """Issue #321: the scaffold gated ``updateI18n`` on a locale *change*, which
216
+ drops the catalog that arrives when the audience changes instead. Signing in
217
+ swaps the anonymous snapshot for one including admin-only modules at the same
218
+ locale, so every admin screen rendered raw keys ("dashboard.home.title")
219
+ until a hard refresh. A non-null ``messages`` payload is itself the server's
220
+ signal that the client needs it."""
221
+ from simple_module_cli.scaffolding import create_host
222
+
223
+ dest = tmp_path / "demo"
224
+ create_host(dest, name="demo-host", modules=["Dashboard", "Auth"])
225
+
226
+ app_tsx = (dest / "client_app" / "app.tsx").read_text()
227
+ assert "updateI18n" in app_tsx
228
+ assert "activeLocale" not in app_tsx, (
229
+ "the locale gate is back — a same-locale catalog would be dropped again"
230
+ )
231
+
232
+
233
+ def test_sm_new_host_app_tsx_wires_theme_and_title(tmp_path: Path) -> None:
234
+ """Issue #429: the scaffolded app.tsx must init the theme and brand titles."""
235
+ from simple_module_cli.scaffolding import create_host
236
+
237
+ dest = tmp_path / "demo"
238
+ create_host(dest, name="demo-host", modules=["Dashboard", "Auth"])
239
+
240
+ app_tsx = (dest / "client_app" / "app.tsx").read_text()
241
+ assert "initTheme(" in app_tsx
242
+ assert "formatTitle" in app_tsx
243
+ assert "setTitleAppName" in app_tsx
244
+
245
+
214
246
  def test_sm_new_no_install_next_steps_include_initial_migration(tmp_path: Path) -> None:
215
247
  """Issue #135: ``host/migrations/versions/`` ships empty, so a fresh
216
248
  ``make migrate`` is a no-op. The printed next-steps must therefore
@@ -224,3 +256,25 @@ def test_sm_new_no_install_next_steps_include_initial_migration(tmp_path: Path)
224
256
  assert result.exit_code == 0, result.output
225
257
  assert 'make migration msg="initial schema"' in result.output
226
258
  assert "make migrate" in result.output
259
+
260
+
261
+ def test_sm_new_flat_root_covers_client_app_build_deps(tmp_path: Path) -> None:
262
+ """Flat scaffolds have no npm workspaces, so ``client_app/package.json`` is
263
+ never installed and the root manifest is the only one npm reads. It must
264
+ therefore carry everything the client build imports — ``vite.config.ts``
265
+ pulls in ``@tailwindcss/vite``, so a root missing it failed ``vite build``
266
+ with ``Could not resolve '@tailwindcss/vite'``."""
267
+ runner = CliRunner()
268
+ target = tmp_path / "demo"
269
+ runner.invoke(
270
+ app,
271
+ ["new", "demo", "--yes", "--flat", "--no-install", "--dest", str(target)],
272
+ )
273
+ root = json.loads((target / "package.json").read_text())
274
+ client_app = json.loads((target / "client_app" / "package.json").read_text())
275
+ assert "workspaces" not in root, "flat mode must stay workspace-free"
276
+
277
+ installed = {**root.get("dependencies", {}), **root.get("devDependencies", {})}
278
+ required = {**client_app.get("dependencies", {}), **client_app.get("devDependencies", {})}
279
+ missing = sorted(set(required) - set(installed))
280
+ assert not missing, f"flat root package.json is missing client_app deps: {missing}"
@@ -0,0 +1,47 @@
1
+ """uv's .venv/.gitignore hides subdirectories from Tailwind, so each gets its own @source (#419)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from simple_module_hosting.assets import ModuleAssets, render_modules_css
6
+
7
+
8
+ def _wheel(tmp_path, **overrides):
9
+ defaults = {
10
+ "name": "Pagebuilder",
11
+ "package_name": "pagebuilder",
12
+ "package_dir": tmp_path / "pagebuilder",
13
+ "pages_dir": None,
14
+ "theme_css": None,
15
+ "styles_css": None,
16
+ }
17
+ return ModuleAssets(**{**defaults, **overrides})
18
+
19
+
20
+ def test_each_subdirectory_with_sources_gets_its_own_line(tmp_path):
21
+ components = tmp_path / "pagebuilder" / "components"
22
+ (components / "widgets" / "quote").mkdir(parents=True)
23
+ (components / "Root.tsx").write_text("")
24
+ (components / "widgets" / "Gallery.tsx").write_text("")
25
+ (components / "widgets" / "quote" / "Quote.tsx").write_text("")
26
+ (components / "empty").mkdir()
27
+ (components / "__pycache__").mkdir()
28
+ (components / "__pycache__" / "x.ts").write_text("")
29
+
30
+ css = render_modules_css(
31
+ [_wheel(tmp_path, components_dir=components)], in_repo=lambda _p: False
32
+ )
33
+
34
+ lines = [line for line in css.splitlines() if line.startswith("@source")]
35
+ assert lines == [
36
+ f'@source "{components.as_posix()}/**/*.{{ts,tsx}}";',
37
+ f'@source "{(components / "widgets").as_posix()}/**/*.{{ts,tsx}}";',
38
+ f'@source "{(components / "widgets" / "quote").as_posix()}/**/*.{{ts,tsx}}";',
39
+ ]
40
+
41
+
42
+ def test_in_repo_module_subdirectories_get_nothing(tmp_path):
43
+ components = tmp_path / "local" / "components"
44
+ (components / "widgets").mkdir(parents=True)
45
+ (components / "widgets" / "A.tsx").write_text("")
46
+ css = render_modules_css([_wheel(tmp_path, components_dir=components)], in_repo=lambda _p: True)
47
+ assert "@source" not in css
@@ -0,0 +1,57 @@
1
+ """A freshly scaffolded host gets a working /setup with no files of its own.
2
+
3
+ GH #351: ``SetupMiddleware`` redirected every request to ``/setup`` while the
4
+ route and page lived only in the framework repo's unpublished host, so a host
5
+ made by ``smpy create-host`` answered ``/`` → ``/setup`` → 404. The wizard now
6
+ ships with ``simple_module_hosting``: ``create_app`` mounts the route and
7
+ ``gen-pages`` registers the page, so the scaffold must stay free of both.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+
15
+ import pytest
16
+ from simple_module_hosting.manifest import write_module_pages_manifest
17
+ from simple_module_hosting.setup_wizard import PAGES_NAME, pages_dir
18
+
19
+ pytestmark = pytest.mark.anyio
20
+
21
+
22
+ async def test_scaffold_carries_no_setup_code_and_resolves_the_wizard(tmp_path) -> None:
23
+ from simple_module_cli.scaffolding import create_host
24
+
25
+ dest = tmp_path / "demo"
26
+ create_host(dest, name="demo-host", modules=[])
27
+ client_app = dest / "client_app"
28
+
29
+ # Nothing host-side: no route module, no page.
30
+ assert not (dest / "routes_setup.py").exists()
31
+ assert not list((client_app / "pages").rglob("Setup*"))
32
+ assert "setup" not in (dest / "main.py").read_text(encoding="utf-8").replace(
33
+ "setup_logging", ""
34
+ )
35
+
36
+ # What `smpy gen-pages` writes for this host, with no modules installed.
37
+ write_module_pages_manifest([], client_app, repo_root=dest)
38
+
39
+ manifest = json.loads((client_app / "modules.manifest.json").read_text(encoding="utf-8"))
40
+ assert manifest[PAGES_NAME] == pages_dir().as_posix()
41
+ assert (pages_dir() / "Wizard.tsx").is_file()
42
+
43
+ generated = (client_app / "modules.generated.ts").read_text(encoding="utf-8")
44
+ assert f'"{PAGES_NAME}": import.meta.glob' in generated
45
+
46
+ # The scaffold's resolver keys a module glob entry as `<name>/<path under pages/>`,
47
+ # which is how `inertia.render("Setup/Wizard")` finds the page.
48
+ pages_ts = (client_app / "pages.ts").read_text(encoding="utf-8")
49
+ assert "pages[`${moduleName}/${match[1]}`]" in pages_ts
50
+ match = re.search(r"/pages/(.+)\.tsx$", (pages_dir() / "Wizard.tsx").as_posix())
51
+ assert match and f"{PAGES_NAME}/{match.group(1)}" == "Setup/Wizard"
52
+
53
+ # Tailwind must scan the wheel's wizard, and Vite must be allowed to serve it.
54
+ css = (client_app / "modules.generated.css").read_text(encoding="utf-8")
55
+ assert f'@source "{pages_dir().as_posix()}/**/*.{{ts,tsx}}";' in css
56
+ assets = json.loads((client_app / "modules.assets.json").read_text(encoding="utf-8"))
57
+ assert assets[PAGES_NAME]["pages"] == pages_dir().as_posix()
@@ -41,6 +41,17 @@ class TestCreateHost:
41
41
  ]:
42
42
  assert (dest / relpath).exists(), f"missing: {relpath}"
43
43
 
44
+ async def test_pages_ts_excludes_colocated_test_files(self, tmp_path):
45
+ """Guards template drift from host/client_app/pages.ts: without these
46
+ negations a colocated test file ships to the production bundle."""
47
+ from simple_module_cli.scaffolding import create_host
48
+
49
+ dest = tmp_path / "demo"
50
+ create_host(dest, name="demo-host", modules=[])
51
+ pages_ts = (dest / "client_app" / "pages.ts").read_text(encoding="utf-8")
52
+ assert "'!./pages/**/*.test.tsx'" in pages_ts
53
+ assert "'!./pages/**/*.spec.tsx'" in pages_ts
54
+
44
55
  async def test_package_json_carries_host_name(self, tmp_path):
45
56
  """client_app/package.json has its `name` prefixed with the host name."""
46
57
  from simple_module_cli.scaffolding import create_host
@@ -30,6 +30,16 @@ class TestCreateModule:
30
30
  # pytest try to register `tests.conftest` as a plugin twice.
31
31
  assert not (dest / "tests" / "__init__.py").exists()
32
32
 
33
+ async def test_generated_test_uses_effective_route_paths(self, tmp_path):
34
+ """#431: app.routes hides lazily-included routers on FastAPI >= 0.137."""
35
+ from simple_module_cli.scaffolding import create_module
36
+
37
+ dest = tmp_path / "simple-module-my-feature"
38
+ create_module(dest, name="MyFeature")
39
+ text = (dest / "tests" / "test_my_feature.py").read_text()
40
+ assert "effective_route_paths(app)" in text
41
+ assert "app.routes" not in text.replace("``app.routes``", "")
42
+
33
43
  async def test_test_file_basename_is_unique_per_module(self, tmp_path):
34
44
  """Two modules in one workspace must not ship same-named test files.
35
45
 
@@ -92,6 +92,37 @@ class TestStandaloneOverlay:
92
92
  assert '"@simple-module-py/tsconfig": "0.0.27"' in pkg
93
93
  assert '"typescript"' in pkg
94
94
 
95
+ async def test_standalone_ships_the_types_its_tsconfig_declares(self, tmp_path):
96
+ """The shared base.json sets ``types: ["vite/client", "node"]``, so a
97
+ standalone module that doesn't depend on `vite` and `@types/node` fails
98
+ its own CI `npm run typecheck` with TS2688 "Cannot find type definition
99
+ file for 'node'/'vite/client'" on the very first run."""
100
+ import json
101
+ from pathlib import Path
102
+
103
+ from simple_module_cli.scaffolding import create_module
104
+
105
+ dest = tmp_path / "simple-module-my-feature"
106
+ create_module(dest, name="MyFeature", standalone=True)
107
+
108
+ base = json.loads(
109
+ (Path(__file__).parents[3] / "packages" / "tsconfig" / "base.json").read_text(
110
+ encoding="utf-8"
111
+ )
112
+ )
113
+ declared = base.get("compilerOptions", {}).get("types", [])
114
+ # "vite/client" is shipped by `vite`; "node" by `@types/node`.
115
+ providers = {"vite/client": "vite", "node": "@types/node"}
116
+ pkg = json.loads((dest / "package.json").read_text(encoding="utf-8"))
117
+ dev_deps = pkg.get("devDependencies", {})
118
+ for entry in declared:
119
+ provider = providers.get(entry)
120
+ assert provider is not None, f"no known provider for tsconfig type {entry!r}"
121
+ assert provider in dev_deps, (
122
+ f"base.json declares types {entry!r} but the standalone module "
123
+ f"scaffold does not depend on {provider!r}"
124
+ )
125
+
95
126
  async def test_in_repo_keeps_workspace_configs(self, tmp_path):
96
127
  from simple_module_cli.scaffolding import create_module
97
128