Repository navigation
feat(config): an app's config builds on its design-system package's, and so does the root's - #202
Merged
Merged
Conversation
This was referenced Oct 21, 2026
Open
dgesteves
force-pushed
the
fix/os2-76-layer-configs
branch
2 times, most recently
from
October 23, 2026 21:22
f6af5ce to
4eb7a70
Compare
…and so does the root's
An app with an onsystem.config.json of its own read none of the config
its design-system package declares. A config holding one rule severity,
{"rules":{"no-hardcoded-color":"warn"}}, in an app that installs such a
package lost the package's components and color tokens: check said "No
design system found" and exited 0. A monorepo root's rules and overrides
did not reach any app whose package declares a config, without a word.
- loadConfig reads the package's config under the project's own: what
the project's config leaves out comes from the package's (components
with their importPath, docs and elements; tokens; libraries), and its
rules apply over the package's, rule by rule. "extends": false reads
a config alone, as before.
- A workspace root's config, from the root, builds on the package's the
same way (src/workspace.ts passes the root config with the lookup).
- inspect and inspect --explain name both configs ("onsystem.config.json,
over node_modules/@acme/ui/onsystem.config.json (declared by @acme/ui)").
- init --force in an app on such a package writes a config with only
$schema, which keeps the package's design system. init at a monorepo
root says its rules apply over the package's config.
- init in an app whose design system zero config reads from an installed
package leaves components and tokens to zero config. It wrote paths
into node_modules/.pnpm/<name>@<version>_<peers>/..., which broke with
"Token file not found" at the next upgrade.
- init in a package that publishes dist/ without its sources says apps
read the components from the type declarations it publishes, when it
names some; it said they get no component checks.
- The docs say what apps get from a package published with type
declarations, and how app and root configs build on the package's.
…s-only projects, watch the package's config - init in an app whose components zero config finds itself (components.json) and whose tokens include an installed package's stylesheet left "tokens" out but wrote "components". A config that names components turns detection off, so a stylesheet outside the usual places (app/styles/tailwind.css, and the package's theme it imports) was lost: the dry run said "No color tokens found". init now leaves "components" to zero config as well when a config that names them would read other token files, and says so. With app/globals.css it still writes the components. - A workspace project whose design-system package's config gives it no component file (tokens only) is listed with a design system again when the root has a config: it was moved to "Without a design system". - The MCP server watches the config a design-system package declares when the project reads it under a config of its own or the root's, so an edit to it is picked up live again. - Docs and the changeset: the package's exclude patterns are added to the app's, and elements merge element by element. init's note in a design-system package says apps read its config under their own.
…okens and libraries
An app config that sets "components" but leaves "tokens" out took the
design-system package's tokens instead of zero config's stylesheets, so
the app's own app/globals.css, and what it imports, was no longer read.
With {"components":["components/ui/**/*.tsx"]}, an app/globals.css that
imports @acme/ui/theme.css and adds --color-brand and --radius-card, and
@acme/ui declaring tokens ["theme.css"], inspect went from 11 tokens to 2
and check . from 2 errors and 1 warning (exit 1) to 2 warnings (exit 0).
init writes such a config ({components}, no tokens) when the stylesheet
imports an installed package. "libraries" fell back to the package's the
same way, so a package's "libraries": false turned MUI off in such an app.
- The package's tokens and libraries now come only with its components,
as importPath, docs and elements already did: a config that sets
components of its own reads them as on main.
- "extends": "@acme/ui" says that extends is true or false and that the
package is found from the dependencies, instead of "expected boolean,
received string".
- inspect --explain with "extends": false names the dependency whose
config was left out, and why.
- Docs, the changeset, the extends description (config.ts, schema.json)
and init's note in a design-system package say so.
… the package's config in --explain init --force in an app whose config names components of its own wrote a config with $schema only whenever a dependency declared a config, so the app took the package's components, checked its own components as app code, and lost its own tokens. It now writes $schema only when the app's config takes the package's components: it sets no components, or ones that match no file outside the package. A config with components of the app's own is written out again, as on main. inspect --explain named the app's config as where the components come from when they came from the package's. ResolvedConfig.inherits says which one it is. The docs say that an app taking the package's components reads only the package's tokens, with the workaround, that the two configs layer field by field, and which apps init --force writes $schema alone for.
dgesteves
force-pushed
the
fix/os2-76-layer-configs
branch
from
October 23, 2026 22:15
4eb7a70 to
d0c8a0c
Compare
…package note with --components init --force over a config that takes the package's components and lists its own stylesheet beside the package's theme (the documented way to read both) wrote $schema only and dropped that list: 12 tokens became 3 and the colors went from errors to warnings. It now keeps the list when one of its files is outside the package, and says so. A list of the package's files only is still left to the package's config. With --components, init no longer says the components come from the package's config.
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-76 (layer configs: an app config extends the package's; root rules apply over a package config), with the review follow-ups for OS2-75 (#176) and OS2-49 (#194) that sit in the same files.
What was wrong
@acme/ui(sources published, config declared),{"rules":{"no-hardcoded-color":"warn"}}in the app'sonsystem.config.jsondropped the package's components and color tokens: on main,check app/page.tsxprintedNo design system found (no components or color tokens): only the accessibility rule ranand exited 0, where the same app with no config reported 5 errors and 1 warning.{"rules":{"no-hardcoded-spacing":"off"},"overrides":[{"files":["apps/admin/legacy/**"],"rules":{"prefer-design-system-component":"off"}}]},check .at the root on main still reported the spacing warning inapps/admin/app/page.tsxandNative <table> where the design system has <Table>inapps/admin/legacy/old.tsx.inspectshowed onlypackages/ui/onsystem.config.json (declared by @acme/ui)for the app.init --forcein such an app wrote a copy of the package's paths, or, when the package's config leftcomponentsto zero config, a config that lost the package's components, as its own note said (It does not know @acme/kit's components).init --yesin a pnpm app that zero config reads an installed design-system package for wrotecomponents,docsandtokensthroughnode_modules/.pnpm/@acme+ui@1.0.0_react@19.0.0/node_modules/@acme/ui/.... pnpm moves that folder at the next version, socheckthen fails withToken file not found, and the component globs match nothing..d.ts, several texts were wrong:inittold a package that publishesdist/with type declarations that installed apps "get no component, prop, variant or native-element checks" and "no component checks until files publishes src/";docs/design-system-packages.md(intro, the third row of "What apps get", the dist-only bullets, "importPathdoes not turn the component rules on"),docs/configuration.md(thelibrariesrow, "Published libraries (MUI)", theimportPathrow), theimportPathdescription insrc/config.tsandschema.json, anddocs/plugin.md("the hook checks the tokens") all said such apps get no components. I checked on main first: with real declarations and the config declared, every component rule runs and--require-design-systemexits 1; without declarations, only the token rules andicon-button-accessible-nameon native elements run, and it exits 2.What changed
src/config.ts: a project's own config (or--config, or a workspace root's config it takes on) is read over the config its design-system package declares.ResolvedConfig.inheritsis set when that config sets nocomponents, soinitandinspect --explaincan tell where the components come from. When it sets nocomponentsof its own, what it leaves out comes from the package's:componentstogether with the package'simportPath,docs,elements,tokensandlibraries. A config that setscomponentstakes none of those, so its components are not mislabelled with the package's import path, and its tokens and libraries are read as on main: withouttokens, from zero config's stylesheets (the app'sapp/globals.cssand what it imports).rulesmerge rule by rule, the app's on top, and, with the package's components,elementsmerge element by element. The package'sexcludepatterns are added to the app's, as they already were in an app without a config.overrides,tsconfigandincludeDesignSystemstay with whichever config sets them, as they already did for the package. New config key"extends"(boolean, default true):falsereads the config alone, as before."extends": "@acme/ui"is an error that saysextendsis true or false and that the package is found from the dependencies. A workspace package whose"onsystem"field cannot be read is still an error in an app without a config, and a warning in one with a config, which is then read alone.src/workspace.ts(two conditions): a project in a monorepo with a root config now gets the root config together with the package lookup, so the root's rules and overrides apply over the package's config incheckandinspectfrom the root. Runs from the project's folder still read no root config, as documented. A project that reads a package's config counts as one with a design system even when that config gives it no component file (tokens only), as it did when the package's config was itsconfigFile.src/design-system.ts(watchConfigFiles): the MCP server also watches the config the package declares, so an edit to it is picked up live when the project reads it under a config of its own or the root's. On main that config was the project'sconfigFile, and was watched as such.src/cli.tsandsrc/explain.ts: theconfigline saysonsystem.config.json, over node_modules/@acme/ui/onsystem.config.json (declared by @acme/ui), andinspect --explainsayswhich this project reads under onsystem.config.json. Without this they would printonsystem.config.json (declared by @acme/ui), which is wrong. When the app's config sets nocomponents,--explainnames the package's config as their source:components come from node_modules/@acme/ui/onsystem.config.json, so zero config did not look(it named the app's config before). With"extends": false,--explainnames the dependency it left out:no a design-system package's config: @acme/ui (installed) declares node_modules/@acme/ui/onsystem.config.json, not read: "extends": false in onsystem.config.json reads it alone.src/init.ts:--forcein an app on a package that declares a config writes{ "$schema": … }only, and says what it builds on, when the app has no config, or one whosecomponentsmatch no file outside the package: none set, a glob that matches nothing, or the package's own paths copied in (node_modules/@acme/ui/src/*.tsx, as main'sinit --forcewrote them). A config that names components of the app's own (components/ui/**/*.tsx) is written out again, as on main:{"components":["components/ui/**/*.tsx"],"tokens":["app/globals.css"]}in the fifth review's app.componentsandtokensare left to zero config, with a note saying why. I chose this over rewriting paths tonode_modules/<name>/: a file list also misses the components a new version adds, and zero config already finds the package again after an upgrade.components.jsonnamesapp/styles/tailwind.css, which imports@acme/ui/theme.css),componentsis left to zero config as well whenever a config that names them would read other token files: such a config turns detection off, and the stylesheetcomponents.jsonnames is not one of the usual ones. Withapp/globals.css, the components are still written.package.jsonnames type declarations (types,typings, atypesexport condition, orindex.d.ts) thatfilespublishes.design-system-packages.mdgets a new "An app's own config" subsection, the precedence list becomes layering, and "What apps get" has 6 rows (with and without.d.ts).configuration.mdgets the zero-config paragraph, "Published libraries" and thelibraries,importPathand newextendsrows.plugin.mdline 32 is corrected. After the fifth review: the precedence sentence says the two configs layer field by field, as "An app's own config" says; a new sentence says that an app taking the package's components (no config, or one withoutcomponents) reads only the tokens the package's config names, with the workaround"tokens": ["app/globals.css", "node_modules/@acme/ui/theme.css"](atokenslist does not follow@imports); and theinit --forcesentence, here and in the changeset, says which apps get$schemaalone.schema.jsonis regenerated bypnpm build.checkreports for apps that have a config of their own and a dependency that declares one: they now get the package's rules where their config leaves them out, and its components and tokens when their config sets nocomponents.A different fix from the one proposed
The finding proposes
"extends": "@northwind/ui", or a merge. This PR merges by default and adds"extends": falseas the way out, so a one-line severity tweak keeps the design system without the app team having to know aboutextends. An app with no config already takes the package's config on, and an app with a config now gets the same design system. I left out a warning for a project whose own config shadows the root config. That case is documented ("applies to every project that has no config of its own"), and it is deliberate in setups that keep per-app configs, where the warning would print on every root run with no way to silence it. The case the finding reports, a package config shadowing the root's, no longer happens.Files outside the item's list
src/workspace.ts: the two conditions above, and a doc comment. No wave 4 or 5 item lists this file.src/design-system.ts:watchConfigFilesand the fallback reload inreloadConfigonly (without aloadConfigoption, a config passed by path that the project reads over the package's is now read again, where the reload looked the config up by name instead). OS2-54 (fix: per-user temp cache, quoted repository text, and linear color rule #199) changes this file in other functions; OS2-47 (wave 5, config reload) lists it.src/explain.ts: wording of theconfigand trace lines, and one trace step for"extends": false. OS2-77 (wave 5) lists this file and comes after this PR.src/cli.ts:declaredBy()and the 2 lines that call it. OS2-54 (wave 4) also listssrc/cli.ts, in other functions, so a rebase there should be mechanical.test/design-system-package.test.tsandtest/init-registry-apps.test.ts. Its first test now uses a package with notypes, since the package with them is covered in the new file.test/design-system-packages-docs.test.tsandtest/dist-only-package.test.tsget real.d.tsfixtures, as the item allows.How it was tested
New
test/layered-config.test.ts, 25 tests. The first 12 fail on main (source stashed, test kept):componentstake none of the package'simportPath,docs,elements,tokensorlibraries"extends": falsereads the config alone"extends": falsein a root configinit --forcewrites$schemaonly, and the findings stay the sameinit --yesin a pnpm.pnpmlayout writes nonode_modulespath, andcheckstill works after the package moves to@acme+ui@1.1.0_…init's dist-only text, with and without type declarations, against what an installed app then getsAdded after review, each run against this branch before the fix (source stashed, tests kept):
init --yesin a shadcn app withcomponents.json, a stylesheet outside the usual places and an installed package's theme, in npm and pnpm layouts: writes$schemaonly, keeps the 3 findings zero config reports and both token files, also after the package moves to@acme+ui@1.1.0_…(2 tests, both fail before the fix, which wrotecomponentsand lost the tokens)app/globals.css: the components are still written (2 tests, a guard: they pass before and after)inspectlists1 project with a design systemwith itsconfigline (fails before the fix)Added after the third review, each run against the previous head (source stashed, tests kept); all fail there:
{"components":["components/ui/**/*.tsx"]}with anapp/globals.cssthat imports the package's theme and adds--color-brandand--radius-card: the app's tokens are read (bg-[#1a7f37]is an exact match forbg-brand,rounded-[14px]suggestsrounded-card), and so is the package's spacing scale it imports. Before, the tokens were onlynode_modules/@acme/ui/theme.css."libraries": falseleaves MUI on in an app whose config setscomponents, and applies when the app's config leavescomponentsoutinspect --explainwith"extends": falsenames the package left out, and"extends": "@acme/ui"gets the hintcomponents, and a new one intest/design-system-packages-docs.test.tsfor the docs sentenceAdded after the fifth review, run against the previous head (source stashed, tests kept):
init --forcein an app whose config is{"components":["components/ui/**/*.tsx"]}, with its own--color-brand: writes{"$schema", "components":["components/ui/**/*.tsx"], "tokens":["app/globals.css"]}without the "reads the config @acme/ui declares" note, andcheckreports the same 2 findings before and after (fails on the previous head, which wrote$schemaonly), and the same case intest/design-system-packages-docs.test.tswith its docs sentence (fails there too)inspect --explainnamesnode_modules/@acme/ui/onsystem.config.jsonas where the components come from under a rules-only app config, andonsystem.config.json, over …under one withcomponents(fails on the previous head)init --forceover a rules-only config, over the package's paths copied in (node_modules/@acme/ui/src/*.tsx, withimportPathand the theme) and over a glob that matches nothing:$schemaonly, and the findings of an app without a config (a guard: it passes on the previous head too, and failed on my first version of this fix, which spelled out every config withcomponents)node_modules/@acme/ui/theme.css, so its own--color-brandgets no exact match; with"tokens": ["app/globals.css", "node_modules/@acme/ui/theme.css"]all 8 rules still run and both are read; with["app/globals.css"]alone the imported theme is not read (no spacing finding); the "field by field" sentencetest/design-system-package.test.ts: 2 new watch tests next to the existing one. An edit to the package's config is picked up byDesignSystemHostunder the app's own config, and by the server'sWorkspaceProjectsunder a root config. Both time out before the fix.test/design-system-packages-docs.test.ts: a new test for theexcludeandelementssentences.test/design-system-packages-docs.test.tschecks each changed docs sentence and table row againstcheck,inspectand the MCP tools, including:.d.tsimportPath"extends": falseinit --forceGate, in this order, after rebasing on main:
pnpm install --frozen-lockfile --prefer-offline,pnpm format:check,pnpm lint,pnpm typecheck,pnpm test(93 files passed, 1 skipped; 1219 tests passed, 3 skipped),pnpm build.pnpm smokepassed in the previous round; this round touches no server or tool code. I did not run the real-world corpus locally. No corpus repository has a config of its own next to a declaring dependency, or a root config, anddub-web-package(init in@dub/ui, app without a config) goes through the unchanged path. CI runs it on this pull request.The third review's fixtures, with this branch and the previous head: in its app shapes (Next.js
app/globals.css, Vitesrc/index.css, Remixapp/tailwind.css, nocomponents.json, each with npm and pnpm layouts, and the shadcn monorepo's 3 variants) the output ofcheck,inspect,init --yesand the upgrade is identical between the two (11 shapes; findings unchanged afterinitin all 11 and after the upgrade in all 8 single-app shapes). In its other fixtures, only the own-components app changes:inspectshows 11 tokens again andcheck .reports 2 errors and 1 warning and exits 1, as on main. The previous head showed 2 tokens and 2 warnings, and exited 0.The fifth review's round: every fixture directory of the first, third and fifth reviews (104), copied, with the previous head (4eb7a70) and this branch, running
inspect,inspect --explain,check .,init --yesandinit --yes --force(each followed byinspectandcheck) in its root and in each project folder. 74 give identical output. 21 differ only in the--explainlinecomponents come from …, which now names the package's config for apps that take its components. The other 9 are apps whose config names components of their own (owncomp-*in the third and fifth reviews,force-own-main,shape-next-npm-*,later-npm,later-pnpm):init --forcewrites their components again, andcheckthen reads the app's own components and its stylesheet's tokens. Inowncomp-*that is 1 component and 11 tokens, with 2 errors and 1 warning, the same as main's build gives there; the previous head gave 3 components, 2 tokens, 2 warnings and anicon-button-accessible-nameerror in the app's owncomponents/ui/button.tsx. The fifth review'srun-*.shscripts (the 11 app shapes the third review also ran, the registry app, the monorepo, rootinit,extends, a package that declares its config afterinit) give identical output on both, except the force-own run. My first version of this fix spelled out every config that setscomponents; these runs showed it then wrotenode_modules/@northwind/ui/src/components/*.tsxback into the first review'sreg-mainapp and lost its tokens (2 findings instead of 7), and failed with "zero config found no design system" over a glob that matches nothing (ex). Hence the "no file outside the package" condition: both now give the previous head's output.Follow-ups
components) reads only the tokens the package's config names, not its own stylesheet's, as an app without a config already did on main. Reading both by default is a follow-up; the docs now say it and give thetokensworkaround.README.md:77-78still says installed apps on a compiled-only package get no component checks. That line belongs to X2-23-onsystem.