simple-module-cli 0.0.28__tar.gz → 0.0.29__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.28 → simple_module_cli-0.0.29}/PKG-INFO +1 -1
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/pyproject.toml +1 -1
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/README.md.tpl +2 -2
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/vite.config.ts +34 -11
- simple_module_cli-0.0.29/tests/conftest.py +45 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_module_css_assets.py +50 -51
- simple_module_cli-0.0.29/tests/test_module_css_build.py +144 -0
- simple_module_cli-0.0.29/tests/test_module_npm_aliases.py +128 -0
- simple_module_cli-0.0.28/tests/test_module_css_build.py +0 -72
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/.gitignore +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/LICENSE +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/README.md +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/__init__.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/_env.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/_module_build.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/_module_host.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/_templating.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/app_project.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/case.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/catalog.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/cli.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/docker_assets.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/module_cmd.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/new.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/package_update.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/pins.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/plugins.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/recipes.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/scaffolding.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/skills/README.md +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-cli/SKILL.md +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-conventions/SKILL.md +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-creating/SKILL.md +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-database/SKILL.md +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-doctor/SKILL.md +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-inertia-pages/SKILL.md +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-locales/SKILL.md +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-migrations/SKILL.md +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-registries/SKILL.md +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/skills/simple-module-testing/SKILL.md +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/skills_cmd.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/docker/Makefile.snippet +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/docker/compose-base-postgres.yml.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/docker/compose-base-sqlite.yml.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/docker/compose-tasks-postgres.yml.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/docker/compose-tasks-sqlite.yml.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/docker/dockerignore +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/docker/host-flat.Dockerfile +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/docker/host.Dockerfile +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/.env.example +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/.gitignore +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/Makefile +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/_optional/background_tasks/Makefile.snippet +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/alembic.ini +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/app.tsx +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/main.tsx +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/package.json.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/pages/Error.tsx +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/pages/Landing.tsx +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/pages.ts +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/styles.css +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/client_app/tsconfig.json +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/main.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/migrations/env.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/migrations/script.py.mako +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/migrations/versions/.gitkeep +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/routes.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/templates/index.html +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/.github/workflows/ci.yml +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/.github/workflows/publish.yml.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/.gitignore +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/README.md.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/__init__.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/endpoints/__init__.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/endpoints/api.py.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/endpoints/views.py.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/module.py.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/pages/Index.tsx.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/services.py.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/__PACKAGE__/settings.py.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/_optional/standalone/package.json.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/_optional/standalone/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/package.json.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/tests/test_module.py.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/module/tsconfig.json.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/workspace/.env.example +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/workspace/.gitignore +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/workspace/Makefile +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/workspace/README.md.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/workspace/package.json.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/workspace/pyproject.toml.tpl +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/wizard.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_build_packaging.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_case.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_cli_catalog.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_cli_docker_assets.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_cli_new.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_cli_new_dest_tolerance.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_cli_new_regressions.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_cli_new_scaffold_layout.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_cli_package_update.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_cli_recipes.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_cli_wizard.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_create_module_ci_skip.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_env_helper.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_module_cmd_build.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_module_cmd_e2e.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_module_cmd_verify.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_module_pages_manifest.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_no_framework_deps.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_plugin_discovery.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_scaffold_rollback.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_scaffolding_host.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_scaffolding_module.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/tests/test_scaffolding_module_js.py +0 -0
- {simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/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.29
|
|
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
|
{simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/README.md.tpl
RENAMED
|
@@ -14,7 +14,7 @@ cp .env.example .env
|
|
|
14
14
|
# First time only — initialize the migration history for the modules you
|
|
15
15
|
# picked. Inspect the generated file, then apply it.
|
|
16
16
|
alembic revision --autogenerate -m "initial schema"
|
|
17
|
-
alembic upgrade
|
|
17
|
+
alembic upgrade heads
|
|
18
18
|
|
|
19
19
|
# Run the API
|
|
20
20
|
python main.py
|
|
@@ -39,7 +39,7 @@ uv add simple_module_my_module
|
|
|
39
39
|
|
|
40
40
|
# 2) generate & apply the migration (new tables + any schema changes)
|
|
41
41
|
alembic revision --autogenerate -m "add my-module"
|
|
42
|
-
alembic upgrade
|
|
42
|
+
alembic upgrade heads
|
|
43
43
|
|
|
44
44
|
# 3) restart the host — the module's routes, menu items, permissions,
|
|
45
45
|
# feature flags, events, and health checks register automatically
|
|
@@ -69,20 +69,35 @@ if (fs.existsSync(manifestPath)) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
72
|
+
// Three things come out of modules.assets.json.
|
|
73
|
+
//
|
|
74
|
+
// 1. `server.fs.allow` entries. The dev server must be allowed to read each
|
|
75
|
+
// module's package dir. Read from modules.assets.json rather than
|
|
76
|
+
// modules.manifest.json because the manifest is keyed off `pages/`, so a
|
|
77
|
+
// module shipping only CSS never appears in it.
|
|
78
|
+
//
|
|
79
|
+
// 2. A convenience `#module/<pkg>` alias. This is NOT required by
|
|
80
|
+
// `modules.generated.css` — that file imports module stylesheets by
|
|
81
|
+
// absolute path, so it resolves with no alias configured at all. Emitting
|
|
82
|
+
// an alias there made a generated file depend on this hand-owned config,
|
|
83
|
+
// and since `vite.config.ts` is scaffolded once and then owned by the app,
|
|
84
|
+
// a Python-only version bump broke every host scaffolded earlier
|
|
85
|
+
// (GH issue #253). The alias stays because it costs nothing.
|
|
86
|
+
//
|
|
87
|
+
// 3. An `<npm_name>` alias per module, so one module can import another's
|
|
88
|
+
// TS/TSX by package name. Aimed at the module's *Python package* dir —
|
|
89
|
+
// a wheel ships `site-packages/foo/**` and nothing above it, so the
|
|
90
|
+
// source-tree module root is not a target both layouts have. Needed in
|
|
91
|
+
// both: a wheel module is never in node_modules, and npm symlinks a
|
|
92
|
+
// workspace member onto the module root, one level too high.
|
|
93
|
+
// See docs/module-authoring.md § Importing another module's TS/TSX.
|
|
76
94
|
//
|
|
77
95
|
// `@tailwindcss/vite` builds its CSS import resolver with
|
|
78
96
|
// `createResolver({ ...config.resolve, ... })`, so `resolve.alias` governs
|
|
79
97
|
// 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 };
|
|
98
|
+
type ModuleAsset = { package_name: string; package: string; npm_name?: string | null };
|
|
85
99
|
const moduleAliases: { find: string; replacement: string }[] = [];
|
|
100
|
+
const moduleNpmNames = new Set<string>();
|
|
86
101
|
const assetsPath = path.resolve(__dirname, 'modules.assets.json');
|
|
87
102
|
let moduleAssets: Record<string, ModuleAsset> = {};
|
|
88
103
|
try {
|
|
@@ -92,6 +107,10 @@ try {
|
|
|
92
107
|
}
|
|
93
108
|
for (const entry of Object.values(moduleAssets)) {
|
|
94
109
|
moduleAliases.push({ find: `#module/${entry.package_name}`, replacement: entry.package });
|
|
110
|
+
if (entry.npm_name) {
|
|
111
|
+
moduleAliases.push({ find: entry.npm_name, replacement: entry.package });
|
|
112
|
+
moduleNpmNames.add(entry.npm_name);
|
|
113
|
+
}
|
|
95
114
|
if (!moduleFsAllow.includes(entry.package)) moduleFsAllow.push(entry.package);
|
|
96
115
|
}
|
|
97
116
|
// Keep the alias list in a stable, longest-first order. Vite matches a string
|
|
@@ -169,6 +188,10 @@ function collectOptimizeIncludes(): string[] {
|
|
|
169
188
|
for (const block of [pkg.dependencies, pkg.peerDependencies]) {
|
|
170
189
|
for (const name of Object.keys(block ?? {})) {
|
|
171
190
|
if (name.startsWith('@types/')) continue;
|
|
191
|
+
// A sibling module declared as a dep/peer dep is not a node_modules
|
|
192
|
+
// package — it is aliased to a source directory above. Pre-bundling
|
|
193
|
+
// it would point the optimizer at raw .tsx with no entry point.
|
|
194
|
+
if (moduleNpmNames.has(name)) continue;
|
|
172
195
|
const nested = findPackageJSON(name);
|
|
173
196
|
if (!nested) continue;
|
|
174
197
|
const nestedPkg = readPackageJSON(nested);
|
|
@@ -231,8 +254,8 @@ export default defineConfig({
|
|
|
231
254
|
plugins: [moduleBareImportResolver(), react(), tailwindcss()],
|
|
232
255
|
root: __dirname,
|
|
233
256
|
resolve: {
|
|
234
|
-
// `#module/<pkg>` -> that module's package directory.
|
|
235
|
-
//
|
|
257
|
+
// `#module/<pkg>` -> that module's package directory. Optional sugar for
|
|
258
|
+
// hand-written imports; modules.generated.css does not rely on it.
|
|
236
259
|
alias: moduleAliases,
|
|
237
260
|
dedupe: [...REACT_CORE_DEPS, '@simple-module-py/ui', '@simple-module-py/i18n'],
|
|
238
261
|
},
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Fixtures shared by the module-asset tests in this directory.
|
|
2
|
+
|
|
3
|
+
These test files have no ``__init__.py`` (test basenames are globally unique
|
|
4
|
+
instead), so a plain helper import across files is not available — a fixture is
|
|
5
|
+
how the factory below gets shared.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def make_importable_module(tmp_path: Path):
|
|
18
|
+
"""Return a factory creating a real importable package bound to a ModuleBase.
|
|
19
|
+
|
|
20
|
+
``get_module_package_name`` derives the package from the class's
|
|
21
|
+
``__module__``, and ``compute_module_assets`` then resolves that package via
|
|
22
|
+
``importlib.resources.files``. So the package has to genuinely exist on
|
|
23
|
+
``sys.path`` — a mock won't exercise the code path we care about.
|
|
24
|
+
|
|
25
|
+
Returns ``(module_instance, package_dir)``.
|
|
26
|
+
"""
|
|
27
|
+
from simple_module_core import ModuleBase, ModuleMeta
|
|
28
|
+
|
|
29
|
+
def _make(pkg_name: str, klass_name: str, *, root: Path | None = None):
|
|
30
|
+
base = root or tmp_path
|
|
31
|
+
pkg = base / pkg_name
|
|
32
|
+
pkg.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
(pkg / "__init__.py").write_text("", encoding="utf-8")
|
|
34
|
+
if str(base) not in sys.path:
|
|
35
|
+
sys.path.insert(0, str(base))
|
|
36
|
+
sys.modules.pop(pkg_name, None)
|
|
37
|
+
|
|
38
|
+
klass = type(
|
|
39
|
+
klass_name,
|
|
40
|
+
(ModuleBase,),
|
|
41
|
+
{"meta": ModuleMeta(name=klass_name), "__module__": f"{pkg_name}.module"},
|
|
42
|
+
)
|
|
43
|
+
return klass(), pkg
|
|
44
|
+
|
|
45
|
+
return _make
|
|
@@ -2,41 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import sys
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
8
7
|
|
|
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
8
|
class TestComputeModuleAssets:
|
|
35
|
-
async def test_detects_theme_and_styles(self,
|
|
9
|
+
async def test_detects_theme_and_styles(self, make_importable_module):
|
|
36
10
|
"""A module shipping theme.css/styles.css has both detected."""
|
|
37
11
|
from simple_module_hosting.assets import compute_module_assets
|
|
38
12
|
|
|
39
|
-
mod, pkg =
|
|
13
|
+
mod, pkg = make_importable_module("styled_mod", "Styled")
|
|
40
14
|
(pkg / "pages").mkdir()
|
|
41
15
|
(pkg / "theme.css").write_text("@theme { --color-x: red; }\n", encoding="utf-8")
|
|
42
16
|
(pkg / "styles.css").write_text(".x { color: red; }\n", encoding="utf-8")
|
|
@@ -50,11 +24,11 @@ class TestComputeModuleAssets:
|
|
|
50
24
|
assert entry.styles_css is not None and entry.styles_css.name == "styles.css"
|
|
51
25
|
assert entry.pages_dir is not None
|
|
52
26
|
|
|
53
|
-
async def test_css_only_module_is_included(self,
|
|
27
|
+
async def test_css_only_module_is_included(self, make_importable_module):
|
|
54
28
|
"""A module with CSS but no pages/ still appears — the manifest.json gap."""
|
|
55
29
|
from simple_module_hosting.assets import compute_module_assets
|
|
56
30
|
|
|
57
|
-
mod, pkg =
|
|
31
|
+
mod, pkg = make_importable_module("cssonly_mod", "CssOnly")
|
|
58
32
|
(pkg / "styles.css").write_text(".y { color: blue; }\n", encoding="utf-8")
|
|
59
33
|
|
|
60
34
|
result = compute_module_assets([mod])
|
|
@@ -64,11 +38,11 @@ class TestComputeModuleAssets:
|
|
|
64
38
|
assert result[0].theme_css is None
|
|
65
39
|
assert result[0].styles_css is not None
|
|
66
40
|
|
|
67
|
-
async def test_module_with_no_assets_is_omitted(self,
|
|
41
|
+
async def test_module_with_no_assets_is_omitted(self, make_importable_module):
|
|
68
42
|
"""A module contributing nothing frontend-ish is skipped, not an error."""
|
|
69
43
|
from simple_module_hosting.assets import compute_module_assets
|
|
70
44
|
|
|
71
|
-
mod, _pkg =
|
|
45
|
+
mod, _pkg = make_importable_module("headless_mod", "Headless")
|
|
72
46
|
|
|
73
47
|
assert compute_module_assets([mod]) == []
|
|
74
48
|
|
|
@@ -105,15 +79,13 @@ class TestCssEmission:
|
|
|
105
79
|
"""theme.css imports unlayered; styles.css imports into layer(components)."""
|
|
106
80
|
from simple_module_hosting.assets import render_modules_css
|
|
107
81
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
styles_css=tmp_path / "gis" / "styles.css",
|
|
112
|
-
)
|
|
82
|
+
theme = tmp_path / "gis" / "theme.css"
|
|
83
|
+
styles = tmp_path / "gis" / "styles.css"
|
|
84
|
+
entry = _assets(tmp_path, theme_css=theme, styles_css=styles)
|
|
113
85
|
css = render_modules_css([entry], in_repo=lambda _p: False)
|
|
114
86
|
|
|
115
|
-
assert '@import "
|
|
116
|
-
assert '@import "
|
|
87
|
+
assert f'@import "{theme.as_posix()}";' in css
|
|
88
|
+
assert f'@import "{styles.as_posix()}" layer(components);' in css
|
|
117
89
|
# Tokens must be declared before the rules that consume them.
|
|
118
90
|
assert css.index("theme.css") < css.index("styles.css")
|
|
119
91
|
|
|
@@ -121,17 +93,18 @@ class TestCssEmission:
|
|
|
121
93
|
"""@source is wheel-only; @import is emitted for every module."""
|
|
122
94
|
from simple_module_hosting.assets import render_modules_css
|
|
123
95
|
|
|
96
|
+
styles = tmp_path / "local" / "styles.css"
|
|
124
97
|
entry = _assets(
|
|
125
98
|
tmp_path,
|
|
126
99
|
name="Local",
|
|
127
100
|
package_name="local",
|
|
128
101
|
pages_dir=tmp_path / "local" / "pages",
|
|
129
|
-
styles_css=
|
|
102
|
+
styles_css=styles,
|
|
130
103
|
)
|
|
131
104
|
css = render_modules_css([entry], in_repo=lambda _p: True)
|
|
132
105
|
|
|
133
106
|
assert "@source" not in css, "in-repo pages are covered by the static glob"
|
|
134
|
-
assert '@import "
|
|
107
|
+
assert f'@import "{styles.as_posix()}" layer(components);' in css
|
|
135
108
|
|
|
136
109
|
async def test_source_emitted_for_wheel_modules(self, tmp_path):
|
|
137
110
|
"""A wheel-installed module gets an absolute @source glob."""
|
|
@@ -154,9 +127,11 @@ class TestCssEmission:
|
|
|
154
127
|
async def test_output_is_formatter_clean(self, tmp_path):
|
|
155
128
|
"""No doubled blank lines, exactly one trailing newline.
|
|
156
129
|
|
|
157
|
-
`biome ci .`
|
|
158
|
-
|
|
159
|
-
|
|
130
|
+
`modules.generated.css` is excluded from `biome ci .` (it is generated,
|
|
131
|
+
and its absolute paths make its formatting machine-dependent — whether
|
|
132
|
+
a line exceeds biome's 100-char `lineWidth` depends on where the repo
|
|
133
|
+
happens to live). Nothing enforces tidiness here but this test, and a
|
|
134
|
+
file humans read when debugging a missing style should stay readable.
|
|
160
135
|
"""
|
|
161
136
|
from simple_module_hosting.assets import render_modules_css
|
|
162
137
|
|
|
@@ -171,18 +146,34 @@ class TestCssEmission:
|
|
|
171
146
|
empty = render_modules_css([], in_repo=lambda _p: False)
|
|
172
147
|
assert "\n\n\n" not in empty and empty.endswith("\n")
|
|
173
148
|
|
|
174
|
-
async def
|
|
175
|
-
"""
|
|
149
|
+
async def test_generated_css_is_self_contained(self, tmp_path):
|
|
150
|
+
"""Every emitted path resolves without any host `vite.config.ts` help.
|
|
151
|
+
|
|
152
|
+
Regression guard for GH issue #253. `modules.generated.css` is
|
|
153
|
+
regenerated from the installed Python packages, but `vite.config.ts` is
|
|
154
|
+
scaffold output owned by the app — so anything here that needs a host
|
|
155
|
+
alias to resolve breaks every host scaffolded before that alias
|
|
156
|
+
existed, on a Python-only version bump.
|
|
157
|
+
|
|
158
|
+
Absolute, on-disk paths are the invariant that makes the two files
|
|
159
|
+
independently versionable.
|
|
160
|
+
"""
|
|
176
161
|
from simple_module_core import discover_modules
|
|
177
162
|
from simple_module_hosting.manifest import write_module_pages_manifest
|
|
178
163
|
|
|
179
164
|
written = write_module_pages_manifest(discover_modules(), tmp_path)
|
|
180
165
|
css = written["css"].read_text(encoding="utf-8")
|
|
181
166
|
|
|
182
|
-
for
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
167
|
+
imports = [ln for ln in css.splitlines() if ln.startswith("@import")]
|
|
168
|
+
assert imports, "expected at least one module stylesheet import"
|
|
169
|
+
for line in imports:
|
|
170
|
+
target = Path(line.split('"')[1])
|
|
171
|
+
assert target.is_absolute(), f"@import must be absolute: {line}"
|
|
172
|
+
assert "../" not in line, f"@import must not use a relative path: {line}"
|
|
173
|
+
assert not line.split('"')[1].startswith("#"), (
|
|
174
|
+
f"@import must not use an alias the host has to define: {line}"
|
|
175
|
+
)
|
|
176
|
+
assert target.is_file(), f"@import points at a file that does not exist: {line}"
|
|
186
177
|
|
|
187
178
|
|
|
188
179
|
class TestAssetsManifest:
|
|
@@ -203,7 +194,15 @@ class TestAssetsManifest:
|
|
|
203
194
|
entry = data["Dashboard"]
|
|
204
195
|
assert entry["package_name"] == "dashboard"
|
|
205
196
|
assert entry["package"].endswith("dashboard")
|
|
206
|
-
assert
|
|
197
|
+
assert entry["npm_name"] == "@simple-module-py/dashboard"
|
|
198
|
+
assert set(entry) == {
|
|
199
|
+
"package_name",
|
|
200
|
+
"package",
|
|
201
|
+
"pages",
|
|
202
|
+
"theme",
|
|
203
|
+
"styles",
|
|
204
|
+
"npm_name",
|
|
205
|
+
}
|
|
207
206
|
|
|
208
207
|
async def test_manifest_json_shape_unchanged(self, tmp_path):
|
|
209
208
|
"""modules.manifest.json stays {name: pages_dir} for downstream vite configs.
|
|
@@ -0,0 +1,144 @@
|
|
|
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 Tailwind
|
|
5
|
+
resolves the path and keeps the rule. This is the one assertion that exercises
|
|
6
|
+
that whole chain, so it drives a genuine build rather than inspecting the
|
|
7
|
+
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` of that stylesheet.
|
|
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
|
+
dashboard_css = REPO_ROOT / "modules/dashboard/dashboard/styles.css"
|
|
61
|
+
assert f'@import "{dashboard_css.as_posix()}" layer(components);' in generated, (
|
|
62
|
+
f"gen-pages did not emit the dashboard stylesheet import:\n{generated}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
subprocess.run(["npm", "run", "build"], cwd=REPO_ROOT, check=True, capture_output=True)
|
|
66
|
+
|
|
67
|
+
built = list((REPO_ROOT / "host/static/dist/assets").glob("*.css"))
|
|
68
|
+
assert built, "no CSS emitted by the build"
|
|
69
|
+
combined = "\n".join(p.read_text(encoding="utf-8") for p in built)
|
|
70
|
+
assert "dashboard-stat-grid" in combined, (
|
|
71
|
+
"module-shipped CSS did not reach the bundle — the generated "
|
|
72
|
+
"@import most likely failed to resolve"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Written into a real module's pages/ so the host build treats it exactly like
|
|
77
|
+
# any other module page. Removed again in the fixture's teardown.
|
|
78
|
+
PROBE_MODULE_PAGES = REPO_ROOT / "modules/audit_log/audit_log/pages"
|
|
79
|
+
PROBE_PAGE = PROBE_MODULE_PAGES / "CrossModuleImportProbe.tsx"
|
|
80
|
+
PROBE_SOURCE = """\
|
|
81
|
+
// Generated by test_module_css_build.py — safe to delete.
|
|
82
|
+
import { DemoPlaceholders } from '@simple-module-py/dashboard/pages/components/DemoPlaceholders';
|
|
83
|
+
|
|
84
|
+
export default function CrossModuleImportProbe() {
|
|
85
|
+
return <DemoPlaceholders totalUsers={0} />;
|
|
86
|
+
}
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.fixture
|
|
91
|
+
def cross_module_probe_page():
|
|
92
|
+
"""Add a page importing a sibling module by npm package name, then remove it."""
|
|
93
|
+
PROBE_PAGE.write_text(PROBE_SOURCE, encoding="utf-8")
|
|
94
|
+
try:
|
|
95
|
+
yield PROBE_PAGE
|
|
96
|
+
finally:
|
|
97
|
+
PROBE_PAGE.unlink(missing_ok=True)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestCrossModuleImportResolves:
|
|
101
|
+
def test_module_can_import_a_sibling_by_npm_package_name(self, cross_module_probe_page):
|
|
102
|
+
"""`@simple-module-py/<slug>/…` resolves to the sibling's package dir.
|
|
103
|
+
|
|
104
|
+
Nothing in Node's own resolution makes this work. A wheel-installed
|
|
105
|
+
module is not an npm workspace member at all, so it never lands in
|
|
106
|
+
node_modules; a workspace member *is* symlinked, but onto the
|
|
107
|
+
source-tree module root — one level above the Python package — so the
|
|
108
|
+
subpath lands somewhere that does not exist. Both cases are covered by
|
|
109
|
+
the `npm_name` aliases the host builds from modules.assets.json.
|
|
110
|
+
|
|
111
|
+
This can only be caught by a real build: every unit test around
|
|
112
|
+
`npm_name` stays green while the alias itself is missing or misaimed.
|
|
113
|
+
"""
|
|
114
|
+
subprocess.run(
|
|
115
|
+
[
|
|
116
|
+
"uv",
|
|
117
|
+
"run",
|
|
118
|
+
"--project",
|
|
119
|
+
"host",
|
|
120
|
+
"smpy",
|
|
121
|
+
"host",
|
|
122
|
+
"gen-pages",
|
|
123
|
+
"--host-dir=host/client_app",
|
|
124
|
+
],
|
|
125
|
+
cwd=REPO_ROOT,
|
|
126
|
+
check=True,
|
|
127
|
+
capture_output=True,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
result = subprocess.run(
|
|
131
|
+
["npm", "run", "build"], cwd=REPO_ROOT, capture_output=True, text=True
|
|
132
|
+
)
|
|
133
|
+
assert result.returncode == 0, (
|
|
134
|
+
"build failed — a module could not import a sibling by npm package "
|
|
135
|
+
f"name:\n{result.stderr[-2000:]}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
chunks = list((REPO_ROOT / "host/static/dist/assets").glob("CrossModuleImportProbe*.js"))
|
|
139
|
+
assert chunks, "probe page produced no chunk"
|
|
140
|
+
# Resolution really happened if the sibling's component was pulled in
|
|
141
|
+
# rather than the import being dropped or stubbed out.
|
|
142
|
+
assert any("DemoPlaceholders" in c.read_text(encoding="utf-8") for c in chunks), (
|
|
143
|
+
"probe chunk does not reference the sibling module's component"
|
|
144
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Tests for a module's npm identity — the `npm_name` in `modules.assets.json`.
|
|
2
|
+
|
|
3
|
+
That field is what lets one module import another's TS/TSX by package name.
|
|
4
|
+
The host aliases it onto the module's *Python package directory*, and the
|
|
5
|
+
tests here pin both halves: that the name is discovered in either install
|
|
6
|
+
layout, and that it is aimed at the one directory both layouts share.
|
|
7
|
+
|
|
8
|
+
The real end-to-end proof lives in `test_module_css_build.py` — a bundler that
|
|
9
|
+
never resolves the alias would leave every assertion here green.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestNpmNameDiscovery:
|
|
19
|
+
async def test_read_from_wheel_layout(self, make_importable_module):
|
|
20
|
+
"""A wheel embeds package.json *inside* the Python package.
|
|
21
|
+
|
|
22
|
+
Hatch force-includes the module-root `package.json` as
|
|
23
|
+
`<pkg>/package.json`, so that is where an installed module carries its
|
|
24
|
+
npm identity.
|
|
25
|
+
"""
|
|
26
|
+
from simple_module_hosting.assets import compute_module_assets
|
|
27
|
+
|
|
28
|
+
mod, pkg = make_importable_module("wheelish_mod", "Wheelish")
|
|
29
|
+
(pkg / "pages").mkdir()
|
|
30
|
+
(pkg / "package.json").write_text(
|
|
31
|
+
json.dumps({"name": "@simple-module-py/wheelish"}), encoding="utf-8"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
assert compute_module_assets([mod])[0].npm_name == "@simple-module-py/wheelish"
|
|
35
|
+
|
|
36
|
+
async def test_read_from_workspace_layout(self, make_importable_module):
|
|
37
|
+
"""An editable install leaves package.json at the source-tree module root."""
|
|
38
|
+
from simple_module_hosting.assets import compute_module_assets
|
|
39
|
+
|
|
40
|
+
mod, pkg = make_importable_module("srcish_mod", "Srcish")
|
|
41
|
+
(pkg / "pages").mkdir()
|
|
42
|
+
(pkg.parent / "pyproject.toml").write_text("[project]\n", encoding="utf-8")
|
|
43
|
+
(pkg.parent / "package.json").write_text(
|
|
44
|
+
json.dumps({"name": "@simple-module-py/srcish"}), encoding="utf-8"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
assert compute_module_assets([mod])[0].npm_name == "@simple-module-py/srcish"
|
|
48
|
+
|
|
49
|
+
async def test_parent_package_json_ignored_without_pyproject(self, make_importable_module):
|
|
50
|
+
"""No `pyproject.toml` beside it means that directory is not a module root.
|
|
51
|
+
|
|
52
|
+
Without this guard a wheel-installed module would read whatever
|
|
53
|
+
`package.json` happened to sit in `site-packages/` and alias itself
|
|
54
|
+
onto a stranger's name.
|
|
55
|
+
"""
|
|
56
|
+
from simple_module_hosting.assets import compute_module_assets
|
|
57
|
+
|
|
58
|
+
mod, pkg = make_importable_module("stray_mod", "Stray")
|
|
59
|
+
(pkg / "pages").mkdir()
|
|
60
|
+
(pkg.parent / "package.json").write_text(
|
|
61
|
+
json.dumps({"name": "totally-unrelated"}), encoding="utf-8"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
assert compute_module_assets([mod])[0].npm_name is None
|
|
65
|
+
|
|
66
|
+
async def test_module_without_package_json(self, make_importable_module):
|
|
67
|
+
"""A Python-only module contributes no npm identity — and must not crash."""
|
|
68
|
+
from simple_module_hosting.assets import compute_module_assets
|
|
69
|
+
|
|
70
|
+
mod, pkg = make_importable_module("nojs_mod", "NoJs")
|
|
71
|
+
(pkg / "styles.css").write_text(".z { color: red; }\n", encoding="utf-8")
|
|
72
|
+
|
|
73
|
+
assert compute_module_assets([mod])[0].npm_name is None
|
|
74
|
+
|
|
75
|
+
async def test_malformed_package_json_is_ignored(self, make_importable_module):
|
|
76
|
+
"""A broken package.json degrades to "no npm name", never to a boot failure."""
|
|
77
|
+
from simple_module_hosting.assets import compute_module_assets
|
|
78
|
+
|
|
79
|
+
mod, pkg = make_importable_module("broken_mod", "Broken")
|
|
80
|
+
(pkg / "pages").mkdir()
|
|
81
|
+
(pkg / "package.json").write_text("{not json", encoding="utf-8")
|
|
82
|
+
|
|
83
|
+
assert compute_module_assets([mod])[0].npm_name is None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TestNpmAliasContract:
|
|
87
|
+
async def test_alias_target_is_the_python_package_dir(self, tmp_path):
|
|
88
|
+
"""`npm_name` must alias onto `package`, not the source-tree module root.
|
|
89
|
+
|
|
90
|
+
This is the invariant that makes a cross-module import mean the same
|
|
91
|
+
thing in both install layouts. A wheel ships `site-packages/<pkg>/**`
|
|
92
|
+
and nothing above it — the module root does not survive installation —
|
|
93
|
+
so the Python package directory is the only anchor both layouts share.
|
|
94
|
+
|
|
95
|
+
Concretely: `@simple-module-py/dashboard/pages/Home` has to land on
|
|
96
|
+
`<package>/pages/Home`, which exists in a wheel and in a checkout.
|
|
97
|
+
"""
|
|
98
|
+
from simple_module_core import discover_modules
|
|
99
|
+
from simple_module_hosting.manifest import write_module_pages_manifest
|
|
100
|
+
|
|
101
|
+
write_module_pages_manifest(discover_modules(), tmp_path)
|
|
102
|
+
data = json.loads((tmp_path / "modules.assets.json").read_text(encoding="utf-8"))
|
|
103
|
+
|
|
104
|
+
aliased = {k: v for k, v in data.items() if v["npm_name"]}
|
|
105
|
+
assert aliased, "expected at least one module shipping a package.json"
|
|
106
|
+
for name, entry in aliased.items():
|
|
107
|
+
package = Path(entry["package"])
|
|
108
|
+
assert package.is_dir(), f"{name}: alias target is not a directory"
|
|
109
|
+
assert package.name == entry["package_name"], (
|
|
110
|
+
f"{name}: npm name must alias onto the Python package dir "
|
|
111
|
+
f"({entry['package_name']}), not {package.name}"
|
|
112
|
+
)
|
|
113
|
+
if entry["pages"]:
|
|
114
|
+
assert Path(entry["pages"]).parent == package, (
|
|
115
|
+
f"{name}: pages/ must sit directly under the alias target, "
|
|
116
|
+
"or subpath imports resolve differently per install layout"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
async def test_npm_names_are_unique(self, tmp_path):
|
|
120
|
+
"""Two modules claiming one npm name would make the alias order-dependent."""
|
|
121
|
+
from simple_module_core import discover_modules
|
|
122
|
+
from simple_module_hosting.manifest import write_module_pages_manifest
|
|
123
|
+
|
|
124
|
+
write_module_pages_manifest(discover_modules(), tmp_path)
|
|
125
|
+
data = json.loads((tmp_path / "modules.assets.json").read_text(encoding="utf-8"))
|
|
126
|
+
|
|
127
|
+
names = [v["npm_name"] for v in data.values() if v["npm_name"]]
|
|
128
|
+
assert len(names) == len(set(names)), f"duplicate npm package names: {names}"
|
|
@@ -1,72 +0,0 @@
|
|
|
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
|
-
)
|
|
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
|
|
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.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/.env.example
RENAMED
|
File without changes
|
{simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/.gitignore
RENAMED
|
File without changes
|
{simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/simple_module_cli/templates/host/Makefile
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/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
|
|
File without changes
|
{simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/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.28 → simple_module_cli-0.0.29}/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.28 → simple_module_cli-0.0.29}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_cli-0.0.28 → simple_module_cli-0.0.29}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|