Skip to content

fix: add a types condition to the ./styles subpath across all framework packages (#357) - #363

Merged
AminDhouib merged 1 commit into
devfrom
fix/styles-types-357
Aug 26, 2026
Merged

fix: add a types condition to the ./styles subpath across all framework packages (#357)#363
AminDhouib merged 1 commit into
devfrom
fix/styles-types-357

Conversation

@AminDhouib

Copy link
Copy Markdown
Member

Fixes #357.

Problem

All seven UI packages exported their stylesheet as a bare string:

"./styles": "./dist/tailwind-prefixed.css"

TypeScript 6 started type-checking side-effect imports, so the documented import '@upupjs/<framework>/styles' no longer resolves — forcing consumers to hand-write an ambient declare module shim (which is what DevinoSolutions/GetItDone had to do on TS 6.0.3).

Mechanism, and why this one

The subpath gets a types condition backed by a generated empty-module declaration:

"./styles": {
    "types": "./dist/styles.d.ts",
    "default": "./dist/tailwind-prefixed.css"
}

dist/styles.d.ts is just export {}. I tested all four candidate mechanisms against real packed tarballs before choosing:

candidate resolves on TS 6/7?
dist/styles.d.ts + export {} yes
dist/styles.d.ts, empty (global script, not a module) yes
dist/tailwind-prefixed.d.css.ts (the allowArbitraryExtensions form) yes
dist/tailwind-prefixed.css.d.ts yes

All four work — notably the .d.css.ts form resolves without the consumer setting allowArbitraryExtensions, because the types condition points at the file directly instead of going through extension inference. Since correctness didn't decide it, robustness did: a plain .d.ts doesn't depend on the arbitrary-extension feature at all, export {} makes it an explicit module (an empty file would be a global script, a subtly different thing), and the name says what it is. It also holds under skipLibCheck: false.

The issue proposed .d.css.ts as option 1; I went with option 2 for the reasons above.

typesVersions too

moduleResolution: "node10" ignores exports entirely, and TS names the gap explicitly:

error TS2882: Cannot find module or type declarations for side-effect import of '@upupjs/react/styles'.
  There are types at '.../node_modules/@upupjs/react/dist/styles.d.ts', but this result could not be
  resolved under your current 'moduleResolution' setting. Consider updating to 'node16', 'nodenext', or 'bundler'.

So each package also gets "typesVersions": { "*": { "styles": ["dist/styles.d.ts"] } }, following the existing @upupjs/core pattern. Verified inert for modern resolution — bundler/node16 stay green with it present.

No runtime change

The default condition still points at the same unmoved dist/tailwind-prefixed.css. Verified for all seven packages that require.resolve and import.meta.resolve still land on the CSS, so bundlers and packages/{preact,next}/scripts/copy-styles.mjs (which resolves the CSS through this subpath) are unaffected.

Generated, not hand-placed

dist is gitignored and rebuilt, so the declaration is emitted by scripts/emit-styles-dts.mjs, wired into each package's build:css — which already runs last in every package's build, after tsup / vue-tsc / svelte-package / ng-packagr have finished writing and cleaning dist. ng-packagr accepted the new conditions object without complaint (its two "." conflicting-condition warnings are pre-existing and unrelated).

RED → GREEN proof

A real consumer: all 8 tarballs (pnpm pack) extracted into node_modules, import '@upupjs/<fw>/styles' for all seven, type-checked with tsc --noEmit.

Before — reproduces the report exactly, on every package, both resolution modes:

src/index.ts(1,8): error TS2882: Cannot find module or type declarations for side-effect import of '@upupjs/react/styles'.
src/index.ts(2,8): error TS2882: Cannot find module or type declarations for side-effect import of '@upupjs/vue/styles'.
src/index.ts(3,8): error TS2882: Cannot find module or type declarations for side-effect import of '@upupjs/svelte/styles'.
src/index.ts(4,8): error TS2882: Cannot find module or type declarations for side-effect import of '@upupjs/vanilla/styles'.
src/index.ts(5,8): error TS2882: Cannot find module or type declarations for side-effect import of '@upupjs/angular/styles'.
src/index.ts(6,8): error TS2882: Cannot find module or type declarations for side-effect import of '@upupjs/preact/styles'.
src/index.ts(7,8): error TS2882: Cannot find module or type declarations for side-effect import of '@upupjs/next/styles'.

After — repacked and re-extracted, exit 0 everywhere:

bundler node16 node10
typescript@5.9.3 0 0
typescript@6.0.3 0 0 0
typescript@7.0.2 0 0 n/a (node10 removed)

TS 5.9.3 was green before the change too — it doesn't type-check side-effect imports, which is why this never showed up until consumers upgraded.

Regression guards

assertExportsResolvable in the smoke consumer only proves export targets exist, so it would stay green if someone collapsed the subpath back to a bare string (the CSS would still be there). Added:

  • scripts/lib/styles-subpath.mjs — the shape contract as a pure check: types condition present and first, default still the unmoved CSS, typesVersions fallback present, declaration shipped and a module.
  • scripts/lib/styles-subpath.test.mjs — 7 cases, one per failure mode, wired into pnpm run test:scripts (143 tests, was 136).
  • scripts/package-smoke-consumer.mjs calls it against all 7 real tarballs.
  • packages/next/src/__tests__/exports.spec.ts pins dist/styles.d.ts. Negative-tested: deleting the file gives AssertionError: missing dist/styles.d.ts: expected false to be true.

Gates

All via rtk proxy with raw exit codes, run sequentially:

gate exit
typecheck 0
test 0 (28/28 turbo tasks)
build 0
lint 0
lint:ox 0
prettier-check 0
knip 0
vocab:check 0
test:quality 0 (391 files, 0 exceptions)
test:scripts 0 (143/143)
size 0
smoke:packages 0
docs:snippets:check 0

Notes for review

  • scripts/lib/tarball.test.mjs shows a large diff because it was pre-existing-unformatted (2-space, repo prettier is 4) and lint-staged prettier-checks every staged file. Read it with ?w=1; the real change is +42/−9, the rest is the reformat that makes the file committable.
  • scripts/docs/check-snippets.mjs writes a declare module '@upupjs/<pkg>/styles' shim for the docs-snippet harness — the same workaround this fixes. I confirmed the snippets still compile without it, but the harness runs on TS 5.x where side-effect imports aren't checked at all, so that measurement only shows the shim is currently inert, not that the fix retired it. Removing it also means updating the harness's own unit test, so I left it out of this PR. Worth a follow-up when the harness moves to TS 6+.
  • Scope was kept to ./styles. The ./server (next) and ./element (vanilla) subpaths already have types conditions and resolve fine under node16/bundler; they'd only need typesVersions for node10, which is deprecated in TS 6 and removed in TS 7.

… package (#357)

TypeScript 6 type-checks side-effect imports, so the documented
`import '@upupjs/<framework>/styles'` failed to resolve declarations for a
subpath exported as a bare string:

  src/index.ts(1,8): error TS2882: Cannot find module or type declarations for
  side-effect import of '@upupjs/react/styles'.

Reproduced from real packed tarballs on typescript@6.0.3 and @7.0.2 under both
moduleResolution bundler and node16, for all seven UI packages; typescript@5.9.3
is unaffected (it does not check side-effect imports). A production consumer on
TS 6.0.3 had to hand-write an ambient `declare module` shim.

Each framework package's ./styles subpath now resolves types through a generated
empty-module declaration:

  "./styles": {
      "types": "./dist/styles.d.ts",
      "default": "./dist/tailwind-prefixed.css"
  }

plus a typesVersions fallback, because moduleResolution "node10" ignores
`exports` entirely and TS names that gap explicitly ("There are types at
.../dist/styles.d.ts, but this result could not be resolved under your current
'moduleResolution' setting").

This is types-only. The `default` condition still points at the same unmoved
dist/tailwind-prefixed.css, and Node's require.resolve / import.meta.resolve
still land on the CSS for all seven packages, so bundlers and the preact/next
copy-styles.mjs step are unaffected.

The declaration is generated by scripts/emit-styles-dts.mjs, wired into each
package's build:css so it is rebuilt with dist rather than hand-placed in it.

Guarded by scripts/lib/styles-subpath.mjs (shape rules + negative cases in
test:scripts) called from the package smoke consumer, so dropping the types
condition, moving the CSS, or shipping a tarball without the declaration turns
smoke:packages red even though the CSS itself would still be present.
@codesandbox

codesandbox Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@AminDhouib
AminDhouib merged commit 742e879 into dev Aug 26, 2026
18 checks passed
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.

1 participant