Repository navigation
feat: read installed design-system packages that publish their sources - #176
Merged
Merged
Conversation
Zero config skipped every dependency installed in node_modules as compiled, so an app on a source-only package such as @trycompai/design-system (exports at ./src/index.ts, 72 .tsx files) got no components and no tokens. An installed dependency named like a design system is now read through its exports when they lead to the .tsx or .jsx sources it publishes, from its real folder so pnpm's sibling dependencies resolve, or through the app's node_modules when pnpm's store is elsewhere. Its build output is not walked. Its exported stylesheets are its tokens, with the partials of the same package they import, and an app stylesheet's @import of such a package is followed into node_modules. Other installed packages (tailwindcss) are not read. inspect --explain now says a package publishes build output and no component source, instead of calling every installed package compiled.
… sources An installed package that declares no config was read from dist/ back to src/ when src/ was published. Component kits named like a design system (@copilotkit/react-ui, @assistant-ui/react-ui) publish src/ next to dist/ for their source maps, so a shadcn app picked them up as a second design system, with their compiled dist/index.css as tokens and false no-unknown-component findings on valid imports. An installed package's exports are now read as written, and nothing compiled in it is read: no JavaScript, JSX or CSS in dist/, build/, lib/, out/, esm/ or cjs/, from its exports, its barrels or an @import. @trycompai/design-system, whose exports point at ./src, reads as before. The explain line now says "its exports point at build output (...), not at component sources". The docs say that, and that props from a package's own dependencies are not known when pnpm's store is outside the project.
… app resolves A custom condition before default, such as @expo/ui's "expo-source" or react-native-builder-bob's "source", points at src/ for the package's own repository; apps that install it get the build after it. Reading the first condition detected @expo/ui@58.0.18 as an Expo app's design system (147 components from its src/). Installed packages now take each export through import, require, module-sync, node, module, browser, style and default only, for components and for the stylesheets an app's @import reaches.
dgesteves
force-pushed
the
fix/os2-75-installed-source-packages
branch
from
October 16, 2026 06:45
0624742 to
e80b44b
Compare
An installed package named like a design system whose exports point at JavaScript outside dist/, build/, lib/, out/, esm/ and cjs/ (`./index.js` at its root, `es/index.mjs`, `build-module/index.mjs`) had its whole chain of compiled re-exports parsed on every load, to find no component. Such an export is now skipped like build output, and inspect --explain names it as build output instead of "its exports lead to 0 component files". A package that publishes a `.js` barrel in front of `.jsx` files is no longer read; the docs say so. An app stylesheet's @import into an installed package now resolves through the first stylesheet target the matching export gives under the conditions an app resolves, as the package's own stylesheets are found, and does not fall through to a later condition when that one is compiled. The docs and the changeset list all eight conditions, `module-sync` and `node` included.
Merged
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.
Roadmap item: OS2-75 (read installed design-system packages that ship source; follow
@importinto node_modules).What was wrong
Zero config treated every dependency installed in
node_modulesas compiled and skipped it.@trycompai/design-system@1.1.19publishes only sources: itsexportspoint at./src/index.tsand./src/components/ui/*.tsx, it ships 72.tsxfiles, and apps import@trycompai/design-system/globals.css. An app on it got nothing. With the package copied into an app'snode_modules, main'sinspectprintedComponents (0 + 0 parts)andTokens (9)(Tailwind's default spacing and radii only).inspect --explainsaidnot a workspace package: an installed package is compiled, and only sources are read, which is false for this package.checkon a page with seven seeded mistakes gave0 errors, 1 warningandNo design system found. The app's@import "@trycompai/design-system/globals.css"was not followed either, because the stylesheet resolver followed@importonly into workspace packages.What changed
src/detect.ts:PackageFinder.installed(name)finds a dependency in the nearestnode_modules, not linked to a workspace package. It returns the package's real folder, the same place the declared-config path reads (place()), so that pnpm's sibling dependencies resolve. If the real folder is outside the root and its workspace (pnpm's store elsewhere on the machine), it returns the folder it was found by innode_modules. fix: read design-system files only inside the project, links resolved #172's loader containment reads that path.detectProject, a dependency named like a design system that is neither the declared package nor a workspace package is now looked up there. It is read throughfromPackagealone, with itsexportsas written and as an app resolves them (next bullets): directly, or through barrels. It is accepted only when they lead to.tsx/.jsxfiles on disk. Installed packages do not go throughfromImportsorfromPackageComponentsJson.dist/are not followed back tosrc/in an installed package (they still are in a workspace package and in a package whose config the app takes on). Component kits named like a design system, such as@copilotkit/react-uiand@assistant-ui/react-ui, publishsrc/next todist/for their source maps. Followed back, they became a second design system in apps that already have one.import,require,module-sync,node,module,browser,styleanddefault(APP_CONDITIONS), for its components and for the stylesheets an@importreaches. For both, the first of them decides. An app's@import "@acme/ui/x.css", exported as{ "style": "./dist/x.css", "default": "./src/x.css" }, reads nothing, since Tailwind CSS takes the compileddist/x.css. An export that maps the subpath decides even when it leads to nothing. A condition for the package's own repository that comes first is passed over:@expo/ui@58.0.18has"expo-source": "./src/universal/index.ts"before"default": "./build/universal/index.js", and react-native-builder-bob writes"source": "./src/index.tsx"before"default": "./lib/module/index.js". Read through its first condition,@expo/uiwas detected as an Expo app's design system.developmentis passed over too. Workspace packages and packages whose config the app takes on still take the first condition buttypes, which leads to their sources.dist/,build/,lib/,out/,esm/orcjs/, whether an export, a barrel or an@importleads to it. TypeScript there (lib/utils.ts) is source. Nor is an export that points at JavaScript in any other folder (./index.jsat the root, as in@cloudscape-design/components,./es/index.mjsin@lobehub/ui,./build-module/index.mjsin@wordpress/components). Its re-exports are not followed, andinspect --explainnames it as build output. So a compiled package named like a design system costs no parse of its compiled files on load, wherever they are (timings below). The trade-off: a package that publishes a.jsbarrel in front of.jsxfiles is not read. The docs say so: point theexportsat the.jsxfiles, or declare the config.its exports point at build output (node_modules/@acme/ui/dist/index.mjs), not at component sources, and only sources are read. A dependency that is neither installed nor a workspace package getsnot a workspace package, and not installed.stylesheetResolverresolves@import "<pkg>/..."into an installed package only when zero config reads that package as a design system (named like one, exports point at its sources), and never to a stylesheet in its build folder. A relative@importfrom a stylesheet inside an installed package resolves only inside that same package, outside its build folders. So the package's@import './theme.css'is read, while@import 'tailwindcss',@import 'tw-animate-css', a../../tailwindcss/index.csspath out of the package and a../../dist/partial.csspath into its build folder are not.src/tokens/imports.ts: the resolver type's comment says what it now resolves to. No other change was needed there, andsrc/explain.tsneeded none: the candidate lines come fromdetectProject.Docs:
docs/design-system-packages.mdgets a "Without the config" subsection and two table rows: all eight rules for a package named like a design system whoseexportspoint at its sources, and nothing from one whoseexportspoint atdist/or that is named otherwise. The subsection says that exports are read through the conditions an app resolves, so asourceorexpo-sourcecondition before adefaultinto the build is passed over, that exports intodist/are not followed back tosrc/, that nothing compiled is read (stylesheets included), and that when pnpm's store is outside the project, the props a package's components take from their own dependencies are not known. The "In the apps" sentence that said zero config never finds an installed package is updated.docs/troubleshooting.mdquoted the old explain message, so it now quotes the new one. No other wave-1 PR touches it.Changeset:
minor, because roadmap/build/WAVES.md plans the release after wave 4 as a minor for this item and three others ("new behaviour"). Change it topatchif you prefer.Choices:
node_modules/@x/ds(a pnpm link) does not resolve an import of a package pnpm installed next to it. The same file opened atnode_modules/.pnpm/@x+ds@1.0.0/node_modules/@x/dsdoes. A test pins this: with the link path, the Button's props from its own dependency stay unresolved.dist/tosrc/. The name check alone also catches popular widget kits. A design-system package that wants apps to read it while its exports point atdist/can declare its config, which keeps the mapping.{ "import": "./src/index.ts", "require": "./dist/index.cjs" }is read, since bundlers takeimport.browserandstyleare in the set because bundlers and Tailwind CSS resolve them for an app;react-native,react-server,developmentandproductionare not, so a source that only one of those leads to is not read.Behavior changes for upgraders
componentsunset, now picks up an installed package named like a design system whose exports point at its sources. It gets that package's components and, as tokens, its exported stylesheets outside its build folders. A package whose exports point atdist/is not read, with or withoutsrc/published, as on main. The docs test "replaces the package's: ..." uses such a package. A test next to it shows that an app with its own rules-only config keeps the components of a package whose exports point atsrc/.Follow-ups (files outside this item's boundary)
docs/configuration.md(OS2-41's file in this wave, merged in fix(cli): match exclude the same way in check, at the root, in ESLint, the hook and check_ui #175): step 3 of zero config and the tokens paragraph should say that an installed package named like a design system whose exports point at its sources is read, and that@importis followed into it.src/init.ts(fix(init): add the shadcn registry folders to the components it writes #173's file; tracked in the roadmap's follow-up list, to land before the post-wave-4 release):init --yesin an app that reads such a package writes the detected paths it can spell. With pnpm, those paths contain the version. In a real pnpm install of@trycompai/design-system@1.1.19, it wrote"tokens": ["node_modules/.pnpm/@trycompai+design-system@1.1.19_@date-fns+tz@1.5.0_..._c47a9a74.../node_modules/@trycompai/design-system/src/styles/globals.css"], on this branch and on this branch merged with main. That path stops existing at the next upgrade, andcheckthen fails withToken file not found. This PR's base stops withzero config found no design systemand writes nothing.initshould leave an installed package's components and tokens to zero config, or write them throughnode_modules/<name>/.How it was tested
New
test/installed-source-packages.test.ts, 23 tests. Of the first 18, 15 fail on main. The 3 that pass there pin what must stay unread (a package not named like a design system, an@importinto a package that publishes no source, and an app with its own design system that installs a package laid out like@expo/ui). 6 of them fail on 462f468, before the conditions fix. The 5 added in this round fail on e80b44b, the head before this round:@trycompai/design-system:detected installed package @acme/design-system, the explain line,Button,CardandInputwithimportPath@acme/design-system,get_componentover MCP, andcheckreportingno-unknown-variant,prefer-design-system-componentandno-hardcoded-color.tailwindcssortw-animate-css, whether the stylesheet imports them by name or by a../path out of the package.@import "@acme/design-system/globals.css".node_modules/.pnpm/..., and the Button'srenderprop from its own dependency resolves.node_modules, with noNot readwarning, and the Button'srenderprop from its own dependency is not known (the docs say so).../../node_modules/....dist/-only package: not read (even with.jsxfiles indist/), with the new explain message.@acme/react-ui: exports todist/,src/*.tsxpublished, an exporteddist/index.csswith:rootvars, aThreadexported as anObject.assigndefault) in a shadcn app, imported by a page and by the app's stylesheet: the same detection, components, tokens (name and value) andcheckfindings as the same app without it installed. On main, this test fails only on its explain line. As the only candidate, the kit is not read either../styles.cssindist/, a./theme.cssexport with"source"intosrc/before"default"intodist/, a source stylesheet that imports../../dist/partial.css, and an app stylesheet that imports all three: no token from any, with zero config and with a config that names only the components.{ "expo-source": "./src/index.ts", "default": "./build/index.js" }(withtypesas@expo/uihas them), bob's{ "source": "./src/index.tsx", "default": "./lib/module/index.js" }, older bob'ssourcebeforeimportandrequire, anddevelopmentbeforedefault. None is read, andinspect --explainnames the build target../index.js(Cloudscape's layout),./es/index.mjs(@lobehub/ui's) and./build-module/index.js(@wordpress/components's). Each one re-exports a barrel that leads to three.jsxfiles, which only following the compiled re-exports would reach. None is read, andinspect --explainnames the export as build output..export points at./index.js, with./atoms/*and./molecules/*intosrc/: read through the source patterns alone (Card's import path is@acme/design-system/molecules/card, not the.jsbarrel's@acme/design-system).@importof./x.css, exported as{ "style": "./dist/x.css", "default": "./src/x.css" }: no token from either file, with zero config and with a config that names only the components.@expo/ui-like package and uses itsButtonwith an unknown prop: the same detection, components, tokens andcheckfindings as without it..export hassourcebefore a nestedimportintosrc/(andrequireintodist/),./components/*withexpo-sourcebeforebrowserintosrc/, and./globals.csswithdevelopmentintodist/beforestyleintosrc/: read, with its 3 components and its tokens.Updated:
test/design-system-packages-docs.test.ts(the two rows; a package whose exports point atsrc/, run end to end with all eight rules, including when its declared config is not published; a package whose exports point atdist/, withsrc/published or not, or with asourcecondition intosrc/before them, gets nothing, with the explain line and the conditions sentence quoted in the docs; the "In the apps" sentence) and the one explain line intest/init.test.ts. This round adds two docs tests. The conditions the docs list are the eight a package is read through, and it is not read throughdevelopmentorreact-native. A.jsbarrel in front of.jsxfiles gets onlyicon-button-accessible-name, with the explain line and the docs sentence.Gate, all passing:
pnpm install --frozen-lockfile --prefer-offlinepnpm format:checkpnpm lintpnpm typecheckpnpm test: 73 files passed, 1 skipped; 981 tests passed, 3 skippedpnpm build:schema.jsonunchangedpnpm smokeRebased on current main (#173, #174, #175 and #178 in); the gate above ran on the rebased branch.
Corpus, on this head:
node scripts/corpus.mjs --dir <scratch folder>with the built CLI printed "Counts match corpus/snapshot.json". The blind sample fp-sample-20261016 still has 80 of its 80 labels reported. No snapshot, label or README change.Timing of
inspect --no-cache, median of 7 runs at a 1-minute load average of about 6. The real tarballs were copied into an app'snode_modules, and none of them is read on any of the three builds (Components (0 + 0 parts),Tokens (0)):.export)@cloudscape-design/components@3.0.1401(./index.js)@wordpress/components@42.0.0(./build-module/index.mjs)@lobehub/ui@6.0.1(./es/index.mjs)On e80b44b,
inspect --explainsaidits exports lead to 0 component files; a design system exports at least 3for all three. On this head it says, for example,no installed package @lobehub/ui: its exports point at build output (node_modules/@lobehub/ui/es/index.mjs, node_modules/@lobehub/ui/es/i18n/index.mjs), not at component sources, and only sources are read.By hand, with the real
@trycompai/design-system@1.1.19tarball copied into an app'snode_modules(no dependencies installed) and a page with the finding's seeded mistakes:inspectComponents (0 + 0 parts),Tokens (9)detected installed package @trycompai/design-system,Components (87 + 268 parts),Tokens (46)inspect --explainnot a workspace package: an installed package is compiled, ...found installed package @trycompai/design-system: node_modules/@trycompai/design-system, 71 component filescheck app/page.tsx0 errors, 1 warning5 errors, 1 warning: variant,SuperCard, native<input>, palette, hex, spacingThe "this branch" column holds after the conditions fix: with the fixed build,
inspectprinted the samedetected,Components (87 + 268 parts)andTokens (46)lines,inspect --explainthe same71 component filesline, andcheckon a page with the same kinds of mistakes5 errors, 1 warning. On this head,inspect --explainon the same app still printsdetected installed package @trycompai/design-system,Components (87 + 268 parts),Tokens (46)and71 component files. The 71 component files and 46 tokens are the same as the finding's hand config. Theloadingprop finding needs@types/react, which this copy does not install, as in the verifier's run. From the first round, before the conditions fix: a real pnpm install of the same package gives the same component and token counts and7 errors, 1 warning, theisLoadingprop included. Linked into another install's.pnpmfolder (pnpm's store outside the project), it gives6 errors, 1 warning: theisLoadingprop is not flagged there, as the docs now say.check --no-cacheon the page took 0.75 s on this branch and 0.31 s on main (median of 3 runs, at a 1-minute load average of 16). The difference is the time to extract 71 components.With the real
@expo/ui@58.0.18tarball copied into an app'snode_modulesand a page that uses itsswift-uiHost,TextandButton:inspectComponents (0 + 0 parts),Tokens (0)detected installed package @expo/ui,Components (147 + 93 parts)Components (0 + 0 parts),Tokens (0)check app/index.tsxNo problems,No design system foundNo problems,No color tokens foundNo problems,No design system foundinspect --explainon this branch saysno installed package @expo/ui: its exports point at build output (node_modules/@expo/ui/build/universal/index.js, node_modules/@expo/ui/build/swift-ui/index.js), not at component sources, and only sources are read.From the first round, before the conditions fix: with the real
@copilotkit/react-ui@1.79.1and@assistant-ui/react-ui@0.2.1installed in a shadcn app (components.json,components/ui),inspectandcheck .print the same as main, apart from the timing line:Components (5 + 15 parts),Tokens (31), and12 errors, 3 warningsand8 errors, 3 warnings. No CopilotKit component or token, and nono-unknown-componenton assistant-ui'sThread.inspect --explainnow saysno installed package @copilotkit/react-ui: its exports point at build output (node_modules/@copilotkit/react-ui/dist/index.mjs, node_modules/@copilotkit/react-ui/dist/index.css), not at component sources, and only sources are read.