Skip to content

fix(hosting): self-contained module CSS imports + cross-module npm-name imports (#253) - #254

Open
antosubash wants to merge 3 commits into
mainfrom
worktree-fix-253-module-css-imports
Open

fix(hosting): self-contained module CSS imports + cross-module npm-name imports (#253)#254
antosubash wants to merge 3 commits into
mainfrom
worktree-fix-253-module-css-imports

Conversation

@antosubash

@antosubash antosubash commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Fixes #253 — both problems in it.

1. Module CSS imports broke every pre-0.0.27 host

gen-pages emitted @import "#module/<pkg>/styles.css" into modules.generated.css. That specifier resolves only if the host's vite.config.ts defines a matching resolve.alias — but that file is scaffold output: written into an app once, then owned and edited there. It's versioned independently of the Python packages, so upgrading simple_module_* 0.0.26 → 0.0.27 broke vite build in every app scaffolded earlier, on a specifier appearing nowhere in the app's own sources.

Fix: emit absolute paths, exactly as the @source lines in the same file already did. The "ugly ../../../.venv/... path" objection only ever applied to relative ones. modules.generated.css is now self-contained and needs no host cooperation.

This is suggestion (1) from the issue, and it subsumes (2) and (3): with nothing to resolve, there's no host config to verify and no doctor check to add. No host action required.

2. Cross-module TS imports by npm package name

Now supported: import { X } from '@simple-module-py/pagebuilder/components/blockRegistry'.

gen-pages records each module's npm_name in modules.assets.json; the host aliases it onto the module's Python package directory.

That anchor is forced, not chosen — I confirmed it by inspecting a real built wheel, which contains dashboard/** and nothing above it. Hatch force-includes the module-root package.json into the package, so the source-tree module root doesn't survive installation, and the package dir is the only anchor both layouts share:

import x from '@simple-module-py/foo/components/Widget';      // ✅ both layouts
import x from '@simple-module-py/foo/foo/components/Widget';  // ❌ workspace-only

The second shape is what npm's symlink gives you for a workspace member — which is exactly why a hand-rolled alias against the module root appears to work in a checkout and breaks once the module is wheel-installed.

⚠️ This half does require a vite.config.ts change in existing hosts — unlike the CSS fix, the import lives in module source rather than a generated file, so it can't be made self-contained. The CHANGELOG carries the diff. New scaffolds get it automatically, and the new template degrades gracefully against 0.0.27 packages (no npm_name → no alias → no crash), which I verified.

Correcting the issue's diagnosis

The issue attributes this to the template removing package-name aliases and to the #156 fix being unreleased. Both are wrong, and suggestion (4) rests on them:

So rather than re-landing something that was never removed, this adds the capability properly, with an anchor that works in both install layouts.

Verification

Measured, not assumed:

Check Result
Absolute @import, vite 6.4.3 + @tailwindcss/vite 4.3.3, no alias configured resolves
Alias @import, same stack, no alias (negative control) fails with the reporter's exact error
Repo build with resolve.alias forced to [] module CSS reaches the bundle
Dev server module CSS served
Module dir entirely outside the project root, styles.css + unlayered @theme both resolve
Real smpy new app (vite 6): workspace module imports a wheel-installed module by npm name builds; sibling component lands in the chunk
Same app, new template + 0.0.27 packages builds (graceful degradation)
Cross-module import with the alias removed fails — so the new test isn't vacuous
make lint / make test clean — 1760 Python, 48 JS

New guards: test_generated_css_is_self_contained (every @import absolute, non-aliased, exists on disk), a real-build test that a cross-module npm-name import resolves, and unit coverage for both install layouts, the pyproject.toml guard, malformed package.json, and npm-name uniqueness.

Also in here

`gen-pages` emitted `@import "#module/<pkg>/styles.css"` into
`modules.generated.css`. That specifier only resolves if the host's
`vite.config.ts` defines a matching `resolve.alias` — but that file is
scaffold output: written into an app once, then owned and edited there.
It is versioned independently of these Python packages, so a Python-only
bump 0.0.26 -> 0.0.27 broke `vite build` in every app scaffolded earlier,
with `Can't resolve '#module/<pkg>/styles.css'` naming a specifier that
appears nowhere in the app's own sources.

Emit absolute paths instead, exactly as the `@source` lines in the same
file already did. The stated objection to filesystem paths (an ugly
`../../../.venv/lib/python3.12/site-packages/...`) only ever applied to
*relative* ones. `modules.generated.css` is now self-contained and needs
no host cooperation at all.

Verified on the scaffold's own stack (vite 6.4.3 + @tailwindcss/vite
4.3.3) that an absolute `@import` resolves with no alias configured, and
that the alias form fails there with the reporter's exact error; and on
the repo's stack (vite 8) for both `vite build` and the dev server,
including a module directory entirely outside the project root.

The `#module/<pkg>` alias stays in the scaffold template — it costs
nothing and removing it would be a second breaking change to a file apps
own — but nothing generated depends on it any more.

Also:
- biome.json: exclude `modules.generated.css` / `modules.assets.json`,
  matching the existing exclusions for the other two generated files.
  Absolute paths are long, so whether a line exceeds biome's 100-char
  lineWidth depended on where the repo happened to live — `make lint`
  passed or failed by checkout path.
- CHANGELOG: move the #152/#155/#156/#173 entries out of [Unreleased]
  into the releases that actually shipped them (verified with
  `git tag --contains`). Leaving them there is what led #253 to conclude
  the #156 fix was unreleased when it has shipped since v0.0.15.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 7, 2026

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1a96865
Status: ✅  Deploy successful!
Preview URL: https://5343f365.simple-module-python.pages.dev
Branch Preview URL: https://worktree-fix-253-module-css.simple-module-python.pages.dev

View logs

…#253)

Nothing in Node's own resolution made this work. A wheel-installed module
is not an npm workspace member, so it never lands in node_modules at all;
a workspace member *is* symlinked, but onto the source-tree module root —
one level above the Python package — so subpaths landed somewhere that
does not exist. Either way `@simple-module-py/foo/components/x` failed to
resolve.

`gen-pages` now records each module's `npm_name` in modules.assets.json
and the host aliases it onto the module's **Python package directory**.

That anchor is forced, not chosen. A wheel ships `site-packages/<pkg>/**`
and nothing above it — Hatch force-includes the module-root package.json
*into* the package — so the source-tree module root does not survive
installation, and the package dir is the only anchor both layouts share.
Verified by inspecting a real built wheel. The practical consequence is
that the subpath is relative to the package:

    @simple-module-py/foo/components/Widget      ok in both layouts
    @simple-module-py/foo/foo/components/Widget  workspace-only

The second shape is what npm's own symlink gives you for a workspace
member, which is why hand-rolled aliases against the module root appear
to work in a checkout and break once the module is wheel-installed.

npm_name discovery skips a parent package.json unless that directory also
holds a pyproject.toml, so a wheel-installed module cannot pick up a
stray site-packages/package.json and alias itself onto a stranger.
Sibling module names are also excluded from optimizeDeps: they resolve to
source directories, not to pre-bundlable packages.

Verified end-to-end in a real `smpy new` app on the template's vite 6
stack: a workspace module importing a wheel-installed module by package
name — the exact shape from the issue — builds and pulls the sibling's
component into the chunk. Also verified the new template degrades
gracefully against 0.0.27 packages (no npm_name -> no alias, no crash).

Structural changes to stay under the 300-line cap, per CLAUDE.md's
"split by responsibility":
- host/client_app/module-assets.ts — module discovery lifted out of
  vite.config.ts (which was already at 291 lines), mirroring the existing
  compress-assets.ts split.
- framework/cli/tests/test_module_npm_aliases.py — npm identity tests,
  with the importable-module factory moved to a tests/conftest.py fixture
  so both files share it without a cross-file import.

Tests: a real-build guard proving a cross-module import resolves (checked
that it fails when the alias is removed, so it is not vacuous), plus unit
coverage for both install layouts, the pyproject guard, malformed
package.json, and npm-name uniqueness.
@antosubash antosubash changed the title fix(hosting): emit absolute paths for module CSS imports (#253) fix(hosting): self-contained module CSS imports + cross-module npm-name imports (#253) Aug 7, 2026
…them

test_module_css_build.py self-skips without node_modules, and the
`Python tests` job runs `make install-py` only — so both real-build
guards silently skipped in CI (`test_module_css_build.py ss`). They are
the only tests that prove module CSS and cross-module npm-name imports
resolve through a real Tailwind/Vite build; every unit test around them
stays green while the wiring is broken.

`JS build (Vite)` already installs Python deps, JS deps, and runs
gen-pages + build, so it is the one job with everything they need.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[hosting/scaffold] 0.0.27 gen-pages emits #module/<pkg> CSS imports that pre-0.0.27 hosts cannot resolve — build breaks on upgrade

1 participant