simple-module-cli 0.0.25__tar.gz → 0.0.27__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.
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/.gitignore +1 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/PKG-INFO +1 -1
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/pyproject.toml +1 -1
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/catalog.py +6 -0
- simple_module_cli-0.0.27/simple_module_cli/templates/host/client_app/styles.css +27 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/client_app/vite.config.ts +34 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/README.md.tpl +18 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -1
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_cli_wizard.py +7 -1
- simple_module_cli-0.0.27/tests/test_module_css_assets.py +223 -0
- simple_module_cli-0.0.27/tests/test_module_css_build.py +72 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_module_pages_manifest.py +9 -2
- simple_module_cli-0.0.25/simple_module_cli/templates/host/client_app/styles.css +0 -12
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/LICENSE +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/README.md +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/_env.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/app_project.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/case.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/cli.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/new.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/package_update.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/pins.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/plugins.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/recipes.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/scaffolding.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/skills/README.md +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/skills/simple-module-cli/SKILL.md +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/skills/simple-module-creating/SKILL.md +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/skills/simple-module-doctor/SKILL.md +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/skills/simple-module-registries/SKILL.md +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/skills_cmd.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/Makefile +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/README.md.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/_optional/background_tasks/docker-compose.yml +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/_optional/background_tasks/host.Dockerfile +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/_optional/background_tasks/worker.Dockerfile +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/main.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/migrations/env.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/routes.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/templates/index.html +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/__PACKAGE__/pages/.gitkeep +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/tests/test_module.py.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/workspace/.env.example +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/workspace/.gitignore +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/workspace/Makefile +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/workspace/README.md.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/wizard.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_build_packaging.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_case.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_cli_catalog.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_cli_new.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_cli_new_dest_tolerance.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_cli_new_regressions.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_cli_new_scaffold_layout.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_cli_package_update.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_cli_recipes.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_create_module_ci_skip.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_env_helper.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_plugin_discovery.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_scaffold_rollback.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_scaffolding_host.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_scaffolding_module.py +0 -0
- {simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/tests/test_skills_cmd.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple_module_cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.27
|
|
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
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
@import "@simple-module-py/ui/styles/globals.css";
|
|
2
|
+
@import "./modules.generated.css";
|
|
3
|
+
|
|
4
|
+
/* App-level source scanning — host code + workspace modules.
|
|
5
|
+
* Wheel-installed modules are covered by modules.generated.css.
|
|
6
|
+
*
|
|
7
|
+
* The module glob deliberately covers the whole package, not just pages/:
|
|
8
|
+
* shared components, hooks and Puck blocks carry Tailwind classes too, and
|
|
9
|
+
* scanning pages/ alone meant such a class only reached the bundle when some
|
|
10
|
+
* other scanned file happened to use it as well. The {ts,tsx} filter is what
|
|
11
|
+
* keeps .py files out of the scan. */
|
|
12
|
+
@source "./**/*.{ts,tsx}";
|
|
13
|
+
@source "../../modules/*/*/**/*.{ts,tsx}";
|
|
14
|
+
/* The second `*` above matches any directory under modules/<name>/, not just
|
|
15
|
+
* the Python package — so a module's sibling tests/ would be scanned too and
|
|
16
|
+
* test-only classes would land in the production bundle.
|
|
17
|
+
*
|
|
18
|
+
* Consequence: a module's Python package must not itself be named `tests`
|
|
19
|
+
* (i.e. modules/tests/tests/), or this line would exclude the package too. No
|
|
20
|
+
* glob can tell a package directory from a sibling, and `tests` is not a
|
|
21
|
+
* viable package name anyway — it collides with pytest collection. */
|
|
22
|
+
@source not "../../modules/*/tests/**";
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
margin: 0;
|
|
26
|
+
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
|
|
27
|
+
}
|
|
@@ -68,6 +68,37 @@ if (fs.existsSync(manifestPath)) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
// Per-module aliases, so generated CSS can say
|
|
73
|
+
// @import "#module/gis/styles.css"
|
|
74
|
+
// instead of a ../../../.venv/lib/python3.12/site-packages/gis/styles.css
|
|
75
|
+
// path that breaks the moment the interpreter version changes.
|
|
76
|
+
//
|
|
77
|
+
// `@tailwindcss/vite` builds its CSS import resolver with
|
|
78
|
+
// `createResolver({ ...config.resolve, ... })`, so `resolve.alias` governs
|
|
79
|
+
// CSS `@import` as well as JS — verified against @tailwindcss/vite 4.2.4.
|
|
80
|
+
//
|
|
81
|
+
// Read from modules.assets.json rather than modules.manifest.json: the
|
|
82
|
+
// manifest is keyed off `pages/`, so a module shipping only CSS never
|
|
83
|
+
// appears in it.
|
|
84
|
+
type ModuleAsset = { package_name: string; package: string };
|
|
85
|
+
const moduleAliases: { find: string; replacement: string }[] = [];
|
|
86
|
+
const assetsPath = path.resolve(__dirname, 'modules.assets.json');
|
|
87
|
+
let moduleAssets: Record<string, ModuleAsset> = {};
|
|
88
|
+
try {
|
|
89
|
+
moduleAssets = JSON.parse(fs.readFileSync(assetsPath, 'utf-8'));
|
|
90
|
+
} catch {
|
|
91
|
+
// Absent until `smpy gen-pages` runs — proceed with no aliases.
|
|
92
|
+
}
|
|
93
|
+
for (const entry of Object.values(moduleAssets)) {
|
|
94
|
+
moduleAliases.push({ find: `#module/${entry.package_name}`, replacement: entry.package });
|
|
95
|
+
if (!moduleFsAllow.includes(entry.package)) moduleFsAllow.push(entry.package);
|
|
96
|
+
}
|
|
97
|
+
// Keep the alias list in a stable, longest-first order. Vite matches a string
|
|
98
|
+
// `find` on exact equality or a `/`-bounded prefix, so `#module/gis` could not
|
|
99
|
+
// swallow `#module/gis_extra` in any order — this is just determinism, not a
|
|
100
|
+
// correctness fix.
|
|
101
|
+
moduleAliases.sort((a, b) => b.find.length - a.find.length);
|
|
71
102
|
const fakeWorkspaceImporter = path.join(fsRoot, 'package.json');
|
|
72
103
|
|
|
73
104
|
// CJS-only deps like `clsx`, `tailwind-merge`, `class-variance-authority`
|
|
@@ -200,6 +231,9 @@ export default defineConfig({
|
|
|
200
231
|
plugins: [moduleBareImportResolver(), react(), tailwindcss()],
|
|
201
232
|
root: __dirname,
|
|
202
233
|
resolve: {
|
|
234
|
+
// `#module/<pkg>` -> that module's package directory. Consumed by the
|
|
235
|
+
// @import lines in modules.generated.css.
|
|
236
|
+
alias: moduleAliases,
|
|
203
237
|
dedupe: [...REACT_CORE_DEPS, '@simple-module-py/ui', '@simple-module-py/i18n'],
|
|
204
238
|
},
|
|
205
239
|
optimizeDeps: {
|
|
@@ -39,6 +39,24 @@ Hosts mount the bundle automatically at
|
|
|
39
39
|
when it exists, an empty dict otherwise (so dev without a build step
|
|
40
40
|
doesn't fail).
|
|
41
41
|
|
|
42
|
+
## Styling (optional)
|
|
43
|
+
|
|
44
|
+
This module can ship CSS that lands in the host's Tailwind build with no
|
|
45
|
+
wiring on the host side. Create either file beside `pages/` and it is
|
|
46
|
+
picked up automatically — the scaffold deliberately does not create them
|
|
47
|
+
empty, since a file that is always imported but usually empty is noise:
|
|
48
|
+
|
|
49
|
+
- `{{PACKAGE_NAME}}/theme.css` — `@theme` tokens, `@custom-variant`,
|
|
50
|
+
`@font-face`. Imported **unlayered**, because a `@theme` block inside a
|
|
51
|
+
cascade layer registers nothing.
|
|
52
|
+
- `{{PACKAGE_NAME}}/styles.css` — component rules, keyframes, vendor CSS.
|
|
53
|
+
Imported into **`layer(components)`**, so these rules always lose to a
|
|
54
|
+
Tailwind utility and a consuming app can still override them.
|
|
55
|
+
|
|
56
|
+
Putting a construct in the wrong file builds fine but cascades in a way you
|
|
57
|
+
probably did not intend, so `make doctor` warns: `SM022` for `@theme` in
|
|
58
|
+
`styles.css`, `SM023` for an unlayered rule in `theme.css`.
|
|
59
|
+
|
|
42
60
|
## Continuous integration
|
|
43
61
|
|
|
44
62
|
Two workflows live under `.github/workflows/`:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
|
+
from simple_module_cli.catalog import CATALOG
|
|
6
7
|
from simple_module_cli.wizard import run_wizard
|
|
7
8
|
from typer.testing import CliRunner
|
|
8
9
|
|
|
@@ -52,7 +53,12 @@ def test_wizard_full_preset_includes_background_tasks() -> None:
|
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
def test_wizard_custom_picks_only_yes_answers() -> None:
|
|
55
|
-
|
|
56
|
+
# The custom path prompts once per catalog module, in CATALOG order.
|
|
57
|
+
# Derive the answers from CATALOG rather than hardcoding a count: a
|
|
58
|
+
# hardcoded sequence silently shifts onto the wrong module the next time
|
|
59
|
+
# one is added, which is how this test broke when `site_lock` landed.
|
|
60
|
+
picks = ["y" if name == "background_tasks" else "n" for name in CATALOG]
|
|
61
|
+
answers = ["", "", "4", *picks, ""]
|
|
56
62
|
_, _, selected, out = _drive(answers)
|
|
57
63
|
assert set(selected) == {"background_tasks", "users", "auth"}
|
|
58
64
|
assert "Added users (required by background_tasks)" in out
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Tests for module CSS asset discovery and emission (`smpy gen-pages`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _make_importable_module(tmp_path: Path, pkg_name: str, klass_name: str):
|
|
10
|
+
"""Create a real importable package on disk and return a ModuleBase bound to it.
|
|
11
|
+
|
|
12
|
+
``get_module_package_name`` derives the package from the class's
|
|
13
|
+
``__module__``, and ``compute_module_assets`` then resolves that package
|
|
14
|
+
via ``importlib.resources.files``. So the package has to genuinely exist
|
|
15
|
+
on ``sys.path`` — a mock won't exercise the code path we care about.
|
|
16
|
+
"""
|
|
17
|
+
from simple_module_core import ModuleBase, ModuleMeta
|
|
18
|
+
|
|
19
|
+
pkg = tmp_path / pkg_name
|
|
20
|
+
pkg.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
(pkg / "__init__.py").write_text("", encoding="utf-8")
|
|
22
|
+
if str(tmp_path) not in sys.path:
|
|
23
|
+
sys.path.insert(0, str(tmp_path))
|
|
24
|
+
sys.modules.pop(pkg_name, None)
|
|
25
|
+
|
|
26
|
+
klass = type(
|
|
27
|
+
klass_name,
|
|
28
|
+
(ModuleBase,),
|
|
29
|
+
{"meta": ModuleMeta(name=klass_name), "__module__": f"{pkg_name}.module"},
|
|
30
|
+
)
|
|
31
|
+
return klass(), pkg
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestComputeModuleAssets:
|
|
35
|
+
async def test_detects_theme_and_styles(self, tmp_path):
|
|
36
|
+
"""A module shipping theme.css/styles.css has both detected."""
|
|
37
|
+
from simple_module_hosting.assets import compute_module_assets
|
|
38
|
+
|
|
39
|
+
mod, pkg = _make_importable_module(tmp_path, "styled_mod", "Styled")
|
|
40
|
+
(pkg / "pages").mkdir()
|
|
41
|
+
(pkg / "theme.css").write_text("@theme { --color-x: red; }\n", encoding="utf-8")
|
|
42
|
+
(pkg / "styles.css").write_text(".x { color: red; }\n", encoding="utf-8")
|
|
43
|
+
|
|
44
|
+
result = compute_module_assets([mod])
|
|
45
|
+
|
|
46
|
+
assert len(result) == 1
|
|
47
|
+
entry = result[0]
|
|
48
|
+
assert entry.package_name == "styled_mod"
|
|
49
|
+
assert entry.theme_css is not None and entry.theme_css.name == "theme.css"
|
|
50
|
+
assert entry.styles_css is not None and entry.styles_css.name == "styles.css"
|
|
51
|
+
assert entry.pages_dir is not None
|
|
52
|
+
|
|
53
|
+
async def test_css_only_module_is_included(self, tmp_path):
|
|
54
|
+
"""A module with CSS but no pages/ still appears — the manifest.json gap."""
|
|
55
|
+
from simple_module_hosting.assets import compute_module_assets
|
|
56
|
+
|
|
57
|
+
mod, pkg = _make_importable_module(tmp_path, "cssonly_mod", "CssOnly")
|
|
58
|
+
(pkg / "styles.css").write_text(".y { color: blue; }\n", encoding="utf-8")
|
|
59
|
+
|
|
60
|
+
result = compute_module_assets([mod])
|
|
61
|
+
|
|
62
|
+
assert [e.name for e in result] == ["CssOnly"]
|
|
63
|
+
assert result[0].pages_dir is None
|
|
64
|
+
assert result[0].theme_css is None
|
|
65
|
+
assert result[0].styles_css is not None
|
|
66
|
+
|
|
67
|
+
async def test_module_with_no_assets_is_omitted(self, tmp_path):
|
|
68
|
+
"""A module contributing nothing frontend-ish is skipped, not an error."""
|
|
69
|
+
from simple_module_hosting.assets import compute_module_assets
|
|
70
|
+
|
|
71
|
+
mod, _pkg = _make_importable_module(tmp_path, "headless_mod", "Headless")
|
|
72
|
+
|
|
73
|
+
assert compute_module_assets([mod]) == []
|
|
74
|
+
|
|
75
|
+
async def test_preserves_discovery_order(self):
|
|
76
|
+
"""Order follows discover_modules() (topological), not alphabetical."""
|
|
77
|
+
from simple_module_core import discover_modules
|
|
78
|
+
from simple_module_hosting.assets import compute_module_assets
|
|
79
|
+
|
|
80
|
+
modules = discover_modules()
|
|
81
|
+
result = compute_module_assets(modules)
|
|
82
|
+
|
|
83
|
+
names = [e.name for e in result]
|
|
84
|
+
discovery_order = [m.meta.name for m in modules]
|
|
85
|
+
assert names == [n for n in discovery_order if n in set(names)]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _assets(tmp_path: Path, **overrides):
|
|
89
|
+
"""Build a single ModuleAssets entry with sensible defaults."""
|
|
90
|
+
from simple_module_hosting.assets import ModuleAssets
|
|
91
|
+
|
|
92
|
+
defaults = {
|
|
93
|
+
"name": "Gis",
|
|
94
|
+
"package_name": "gis",
|
|
95
|
+
"package_dir": tmp_path / "gis",
|
|
96
|
+
"pages_dir": None,
|
|
97
|
+
"theme_css": None,
|
|
98
|
+
"styles_css": None,
|
|
99
|
+
}
|
|
100
|
+
return ModuleAssets(**{**defaults, **overrides})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestCssEmission:
|
|
104
|
+
async def test_styles_layered_theme_unlayered(self, tmp_path):
|
|
105
|
+
"""theme.css imports unlayered; styles.css imports into layer(components)."""
|
|
106
|
+
from simple_module_hosting.assets import render_modules_css
|
|
107
|
+
|
|
108
|
+
entry = _assets(
|
|
109
|
+
tmp_path,
|
|
110
|
+
theme_css=tmp_path / "gis" / "theme.css",
|
|
111
|
+
styles_css=tmp_path / "gis" / "styles.css",
|
|
112
|
+
)
|
|
113
|
+
css = render_modules_css([entry], in_repo=lambda _p: False)
|
|
114
|
+
|
|
115
|
+
assert '@import "#module/gis/theme.css";' in css
|
|
116
|
+
assert '@import "#module/gis/styles.css" layer(components);' in css
|
|
117
|
+
# Tokens must be declared before the rules that consume them.
|
|
118
|
+
assert css.index("theme.css") < css.index("styles.css")
|
|
119
|
+
|
|
120
|
+
async def test_source_skips_in_repo_but_import_does_not(self, tmp_path):
|
|
121
|
+
"""@source is wheel-only; @import is emitted for every module."""
|
|
122
|
+
from simple_module_hosting.assets import render_modules_css
|
|
123
|
+
|
|
124
|
+
entry = _assets(
|
|
125
|
+
tmp_path,
|
|
126
|
+
name="Local",
|
|
127
|
+
package_name="local",
|
|
128
|
+
pages_dir=tmp_path / "local" / "pages",
|
|
129
|
+
styles_css=tmp_path / "local" / "styles.css",
|
|
130
|
+
)
|
|
131
|
+
css = render_modules_css([entry], in_repo=lambda _p: True)
|
|
132
|
+
|
|
133
|
+
assert "@source" not in css, "in-repo pages are covered by the static glob"
|
|
134
|
+
assert '@import "#module/local/styles.css" layer(components);' in css
|
|
135
|
+
|
|
136
|
+
async def test_source_emitted_for_wheel_modules(self, tmp_path):
|
|
137
|
+
"""A wheel-installed module gets an absolute @source glob."""
|
|
138
|
+
from simple_module_hosting.assets import render_modules_css
|
|
139
|
+
|
|
140
|
+
pages = tmp_path / "gis" / "pages"
|
|
141
|
+
css = render_modules_css([_assets(tmp_path, pages_dir=pages)], in_repo=lambda _p: False)
|
|
142
|
+
|
|
143
|
+
assert f'@source "{pages.as_posix()}/**/*.{{ts,tsx}}";' in css
|
|
144
|
+
|
|
145
|
+
async def test_module_without_css_emits_no_import(self, tmp_path):
|
|
146
|
+
"""Pages-only modules contribute @source but no @import."""
|
|
147
|
+
from simple_module_hosting.assets import render_modules_css
|
|
148
|
+
|
|
149
|
+
entry = _assets(tmp_path, pages_dir=tmp_path / "gis" / "pages")
|
|
150
|
+
css = render_modules_css([entry], in_repo=lambda _p: False)
|
|
151
|
+
|
|
152
|
+
assert "@import" not in css
|
|
153
|
+
|
|
154
|
+
async def test_output_is_formatter_clean(self, tmp_path):
|
|
155
|
+
"""No doubled blank lines, exactly one trailing newline.
|
|
156
|
+
|
|
157
|
+
`biome ci .` lints the whole tree, and `modules.generated.css` is
|
|
158
|
+
untracked but not exempt — so a stray blank line here fails `make lint`
|
|
159
|
+
for anyone who has run `gen-pages`.
|
|
160
|
+
"""
|
|
161
|
+
from simple_module_hosting.assets import render_modules_css
|
|
162
|
+
|
|
163
|
+
for entry in (
|
|
164
|
+
_assets(tmp_path, styles_css=tmp_path / "gis" / "styles.css"),
|
|
165
|
+
_assets(tmp_path, pages_dir=tmp_path / "gis" / "pages"),
|
|
166
|
+
):
|
|
167
|
+
css = render_modules_css([entry], in_repo=lambda _p: False)
|
|
168
|
+
assert "\n\n\n" not in css, f"doubled blank line in:\n{css!r}"
|
|
169
|
+
assert css.endswith("\n") and not css.endswith("\n\n"), repr(css[-20:])
|
|
170
|
+
|
|
171
|
+
empty = render_modules_css([], in_repo=lambda _p: False)
|
|
172
|
+
assert "\n\n\n" not in empty and empty.endswith("\n")
|
|
173
|
+
|
|
174
|
+
async def test_no_relative_paths_anywhere(self, tmp_path):
|
|
175
|
+
"""The whole point of aliases: generated @import lines never use ../.."""
|
|
176
|
+
from simple_module_core import discover_modules
|
|
177
|
+
from simple_module_hosting.manifest import write_module_pages_manifest
|
|
178
|
+
|
|
179
|
+
written = write_module_pages_manifest(discover_modules(), tmp_path)
|
|
180
|
+
css = written["css"].read_text(encoding="utf-8")
|
|
181
|
+
|
|
182
|
+
for line in css.splitlines():
|
|
183
|
+
if line.startswith("@import"):
|
|
184
|
+
assert "../" not in line, f"@import must not use a relative path: {line}"
|
|
185
|
+
assert '"#module/' in line, f"@import must use the alias prefix: {line}"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestAssetsManifest:
|
|
189
|
+
async def test_writes_assets_json(self, tmp_path):
|
|
190
|
+
"""modules.assets.json is emitted alongside the existing three files."""
|
|
191
|
+
import json
|
|
192
|
+
|
|
193
|
+
from simple_module_core import discover_modules
|
|
194
|
+
from simple_module_hosting.manifest import write_module_pages_manifest
|
|
195
|
+
|
|
196
|
+
written = write_module_pages_manifest(discover_modules(), tmp_path)
|
|
197
|
+
|
|
198
|
+
assets_path = tmp_path / "modules.assets.json"
|
|
199
|
+
assert assets_path.is_file()
|
|
200
|
+
assert written["assets"] == assets_path
|
|
201
|
+
|
|
202
|
+
data = json.loads(assets_path.read_text(encoding="utf-8"))
|
|
203
|
+
entry = data["Dashboard"]
|
|
204
|
+
assert entry["package_name"] == "dashboard"
|
|
205
|
+
assert entry["package"].endswith("dashboard")
|
|
206
|
+
assert set(entry) == {"package_name", "package", "pages", "theme", "styles"}
|
|
207
|
+
|
|
208
|
+
async def test_manifest_json_shape_unchanged(self, tmp_path):
|
|
209
|
+
"""modules.manifest.json stays {name: pages_dir} for downstream vite configs.
|
|
210
|
+
|
|
211
|
+
Every downstream app holds its own copy of vite.config.ts and reads
|
|
212
|
+
this file; changing its value shape would break them all at once.
|
|
213
|
+
"""
|
|
214
|
+
import json
|
|
215
|
+
|
|
216
|
+
from simple_module_core import discover_modules
|
|
217
|
+
from simple_module_hosting.manifest import write_module_pages_manifest
|
|
218
|
+
|
|
219
|
+
write_module_pages_manifest(discover_modules(), tmp_path)
|
|
220
|
+
|
|
221
|
+
data = json.loads((tmp_path / "modules.manifest.json").read_text(encoding="utf-8"))
|
|
222
|
+
assert data, "manifest should not be empty"
|
|
223
|
+
assert all(isinstance(v, str) for v in data.values())
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Proves module-shipped CSS survives a real Tailwind build.
|
|
2
|
+
|
|
3
|
+
Every unit test around `render_modules_css` can stay green while Tailwind
|
|
4
|
+
emits nothing at all — the generated `@import` only pays off if Vite resolves
|
|
5
|
+
the `#module/<pkg>` alias and Tailwind keeps the rule. This is the one
|
|
6
|
+
assertion that exercises that whole chain, so it drives a genuine build
|
|
7
|
+
rather than inspecting the generated text.
|
|
8
|
+
|
|
9
|
+
Skipped where a build is impossible: CI's `python-tests` job installs Python
|
|
10
|
+
deps only (`make install-py`), so `node_modules` is absent there. The JS jobs
|
|
11
|
+
and local runs do have it.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
23
|
+
|
|
24
|
+
pytestmark = [
|
|
25
|
+
pytest.mark.skipif(
|
|
26
|
+
not (REPO_ROOT / "node_modules").is_dir(),
|
|
27
|
+
reason="node_modules absent — run `npm install` to exercise the real build",
|
|
28
|
+
),
|
|
29
|
+
pytest.mark.skipif(shutil.which("npm") is None, reason="npm not on PATH"),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestModuleCssReachesBundle:
|
|
34
|
+
def test_module_class_is_emitted_by_vite_build(self):
|
|
35
|
+
"""A class defined only in a module's styles.css appears in the built CSS.
|
|
36
|
+
|
|
37
|
+
`.dashboard-stat-grid` is declared in modules/dashboard/dashboard/styles.css
|
|
38
|
+
and referenced by no TSX anywhere, so it can only reach the bundle via the
|
|
39
|
+
generated `@import "#module/dashboard/styles.css"`.
|
|
40
|
+
"""
|
|
41
|
+
subprocess.run(
|
|
42
|
+
[
|
|
43
|
+
"uv",
|
|
44
|
+
"run",
|
|
45
|
+
"--project",
|
|
46
|
+
"host",
|
|
47
|
+
"smpy",
|
|
48
|
+
"host",
|
|
49
|
+
"gen-pages",
|
|
50
|
+
"--host-dir=host/client_app",
|
|
51
|
+
],
|
|
52
|
+
cwd=REPO_ROOT,
|
|
53
|
+
check=True,
|
|
54
|
+
capture_output=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
generated = (REPO_ROOT / "host/client_app/modules.generated.css").read_text(
|
|
58
|
+
encoding="utf-8"
|
|
59
|
+
)
|
|
60
|
+
assert '@import "#module/dashboard/styles.css" layer(components);' in generated, (
|
|
61
|
+
f"gen-pages did not emit the dashboard stylesheet import:\n{generated}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
subprocess.run(["npm", "run", "build"], cwd=REPO_ROOT, check=True, capture_output=True)
|
|
65
|
+
|
|
66
|
+
built = list((REPO_ROOT / "host/static/dist/assets").glob("*.css"))
|
|
67
|
+
assert built, "no CSS emitted by the build"
|
|
68
|
+
combined = "\n".join(p.read_text(encoding="utf-8") for p in built)
|
|
69
|
+
assert "dashboard-stat-grid" in combined, (
|
|
70
|
+
"module-shipped CSS did not reach the bundle — the #module alias "
|
|
71
|
+
"most likely failed to resolve"
|
|
72
|
+
)
|
|
@@ -33,7 +33,7 @@ class TestModulePagesManifest:
|
|
|
33
33
|
assert "Headless" not in result
|
|
34
34
|
|
|
35
35
|
async def test_write_manifest_emits_json_and_ts(self, tmp_path):
|
|
36
|
-
"""write_module_pages_manifest emits the JSON manifest, TS glob, and
|
|
36
|
+
"""write_module_pages_manifest emits the JSON manifest, TS glob, CSS and assets files."""
|
|
37
37
|
import json
|
|
38
38
|
|
|
39
39
|
from simple_module_core import discover_modules
|
|
@@ -45,10 +45,17 @@ class TestModulePagesManifest:
|
|
|
45
45
|
manifest = tmp_path / "modules.manifest.json"
|
|
46
46
|
generated = tmp_path / "modules.generated.ts"
|
|
47
47
|
css = tmp_path / "modules.generated.css"
|
|
48
|
+
assets = tmp_path / "modules.assets.json"
|
|
48
49
|
assert manifest.is_file()
|
|
49
50
|
assert generated.is_file()
|
|
50
51
|
assert css.is_file()
|
|
51
|
-
assert
|
|
52
|
+
assert assets.is_file()
|
|
53
|
+
assert written == {
|
|
54
|
+
"manifest": manifest,
|
|
55
|
+
"generated": generated,
|
|
56
|
+
"css": css,
|
|
57
|
+
"assets": assets,
|
|
58
|
+
}
|
|
52
59
|
|
|
53
60
|
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
54
61
|
assert "Dashboard" in data
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
@import "@simple-module-py/ui/styles/globals.css";
|
|
2
|
-
@import "./modules.generated.css";
|
|
3
|
-
|
|
4
|
-
/* App-level source scanning — host code + workspace module pages.
|
|
5
|
-
* Wheel-installed modules are covered by modules.generated.css. */
|
|
6
|
-
@source "./**/*.{ts,tsx}";
|
|
7
|
-
@source "../../modules/*/*/pages/**/*.{ts,tsx}";
|
|
8
|
-
|
|
9
|
-
body {
|
|
10
|
-
margin: 0;
|
|
11
|
-
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
|
|
12
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/.env.example
RENAMED
|
File without changes
|
{simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/.gitignore
RENAMED
|
File without changes
|
{simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/Makefile
RENAMED
|
File without changes
|
{simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/README.md.tpl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/alembic.ini
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/main.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/host/routes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/module/.gitignore
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.25 → simple_module_cli-0.0.27}/simple_module_cli/templates/workspace/Makefile
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|