fix: add a types condition to the ./styles subpath across all framework packages (#357) - #363
Merged
Conversation
… 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.
Review or Edit in CodeSandboxOpen the branch in Web Editor • VS Code • Insiders |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #357.
Problem
All seven UI packages exported their stylesheet as a bare string:
TypeScript 6 started type-checking side-effect imports, so the documented
import '@upupjs/<framework>/styles'no longer resolves — forcing consumers to hand-write an ambientdeclare moduleshim (which is what DevinoSolutions/GetItDone had to do on TS 6.0.3).Mechanism, and why this one
The subpath gets a
typescondition backed by a generated empty-module declaration:dist/styles.d.tsis justexport {}. I tested all four candidate mechanisms against real packed tarballs before choosing:dist/styles.d.ts+export {}dist/styles.d.ts, empty (global script, not a module)dist/tailwind-prefixed.d.css.ts(theallowArbitraryExtensionsform)dist/tailwind-prefixed.css.d.tsAll four work — notably the
.d.css.tsform resolves without the consumer settingallowArbitraryExtensions, because thetypescondition points at the file directly instead of going through extension inference. Since correctness didn't decide it, robustness did: a plain.d.tsdoesn'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 underskipLibCheck: false.The issue proposed
.d.css.tsas option 1; I went with option 2 for the reasons above.typesVersionstoomoduleResolution: "node10"ignoresexportsentirely, and TS names the gap explicitly:So each package also gets
"typesVersions": { "*": { "styles": ["dist/styles.d.ts"] } }, following the existing@upupjs/corepattern. Verified inert for modern resolution — bundler/node16 stay green with it present.No runtime change
The
defaultcondition still points at the same unmoveddist/tailwind-prefixed.css. Verified for all seven packages thatrequire.resolveandimport.meta.resolvestill land on the CSS, so bundlers andpackages/{preact,next}/scripts/copy-styles.mjs(which resolves the CSS through this subpath) are unaffected.Generated, not hand-placed
distis gitignored and rebuilt, so the declaration is emitted byscripts/emit-styles-dts.mjs, wired into each package'sbuild:css— which already runs last in every package'sbuild, after tsup / vue-tsc / svelte-package / ng-packagr have finished writing and cleaningdist. 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 intonode_modules,import '@upupjs/<fw>/styles'for all seven, type-checked withtsc --noEmit.Before — reproduces the report exactly, on every package, both resolution modes:
After — repacked and re-extracted, exit 0 everywhere:
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
assertExportsResolvablein 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:typescondition present and first,defaultstill the unmoved CSS,typesVersionsfallback present, declaration shipped and a module.scripts/lib/styles-subpath.test.mjs— 7 cases, one per failure mode, wired intopnpm run test:scripts(143 tests, was 136).scripts/package-smoke-consumer.mjscalls it against all 7 real tarballs.packages/next/src/__tests__/exports.spec.tspinsdist/styles.d.ts. Negative-tested: deleting the file givesAssertionError: missing dist/styles.d.ts: expected false to be true.Gates
All via
rtk proxywith raw exit codes, run sequentially:typechecktestbuildlintlint:oxprettier-checkknipvocab:checktest:qualitytest:scriptssizesmoke:packagesdocs:snippets:checkNotes for review
scripts/lib/tarball.test.mjsshows 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.mjswrites adeclare 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+../styles. The./server(next) and./element(vanilla) subpaths already havetypesconditions and resolve fine under node16/bundler; they'd only needtypesVersionsfor node10, which is deprecated in TS 6 and removed in TS 7.