Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/package-mode-correctness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'onsystem': patch
---

A design-system package whose tokens are a Tailwind v4 theme partial (`@theme`, with no `@import "tailwindcss"`, since the app imports Tailwind) no longer turns off `no-hardcoded-spacing` in the apps that read its config. In a project on Tailwind v4 (`@tailwindcss/postcss`, `@tailwindcss/vite` or `@tailwindcss/cli` in its `package.json`, or `tailwindcss` in a range other than v3's, such as `^4`), such a theme gets Tailwind's default spacing unit and radius scale, as a stylesheet that imports Tailwind does, unless it clears them (`--spacing-*: initial`). `p-[13px]` is reported again, with `p-3` as the nearest step.

A component imported through a subpath export that leads to a source file that does not exist, such as `<Tooltip>` from `@acme/ui/components/tooltip` where `"./components/*": "./src/components/*.tsx"` has no `src/components/tooltip.tsx`, is reported by `no-unknown-component` in workspace apps that read the package's config, as it already was in apps that install the package. It was said not to have been checked, with advice to add the missing files to "components". An import whose export leads to a build folder (`dist/`) with no file there, as before the package is built, is still said not to have been checked, whatever its sources are named.

In an app whose design-system package publishes neither its component sources nor their type declarations, `list_components`, `get_component` and `inspect --explain` now say so, and what its team can do, instead of telling you to check the "components" globs in the config, which would not help.

`init` in a design-system package adds `ONSYSTEM.md` to `files` in its `package.json`, next to the config, so the teams of apps that install the package get the guide in `node_modules`, unless `files` leaves it out (`"!ONSYSTEM.md"`).
8 changes: 5 additions & 3 deletions docs/design-system-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ npx -y onsystem init
`init` treats the folder as a design-system package when zero config reads the components through the package's own `exports`, or when the package is named like one (`@acme/ui`, `@acme/design-system`, `acme-ui`), as apps look for it. There it writes:

- **`onsystem.config.json`**, from what zero config finds in the package, as in any project ([configuration](configuration.md#config-file)).
- **`"onsystem": "./onsystem.config.json"` in `package.json`**, which declares the config to the apps. When `package.json` lists `files`, the config is added to them, so it is published with the package.
- **`"onsystem": "./onsystem.config.json"` in `package.json`**, which declares the config to the apps. When `package.json` lists `files`, the config and `ONSYSTEM.md` are added to them, so they are published with the package, unless `files` leaves `ONSYSTEM.md` out (`"!ONSYSTEM.md"`).
- **`ONSYSTEM.md`**, for the apps' teams: how to see what their app finds, how to give their agents the plugin, the skill or the MCP server, and the CI check. An `ONSYSTEM.md` that is already there is kept, unless `--force` is passed.

Then it checks the package once and offers a baseline, as in any project. When `files` leaves out the component or token files (a package that publishes only `dist/`), `init` says so: apps that install the package from a registry will not find them, and read the components from the type declarations it publishes instead, when it names some (`types` in `package.json`). Commit what it wrote.
Expand Down Expand Up @@ -43,9 +43,10 @@ detected workspace package @acme/ui

- **Paths come from the package.** `components`, `exclude`, `tokens` and `docs` are relative to the package's folder, wherever the app finds it: `../../packages/ui`, or `node_modules/@acme/ui`. They stay in the package when it is installed, and in the workspace when it is a workspace package, so `../tokens/theme.css` in a sibling package works. A path that leaves them is ignored, with a warning.
- **`exclude` adds to the app's defaults.** The app still leaves out `node_modules`, `.d.ts` files, tests and stories, and the package's patterns are added. The package's components are found with its own list, as in the package.
- **Components.** When the config leaves `components` to zero config, the app reads the package as zero config reads a design-system package (step 3 of [zero config](configuration.md#zero-config)), whatever the package's name, and also when it is installed. When the config sets `components`, they are imported by `importPath` when it is set, else by the package's `exports`.
- **Components.** When the config leaves `components` to zero config, the app reads the package as zero config reads a design-system package (step 3 of [zero config](configuration.md#zero-config)), whatever the package's name, and also when it is installed. When the config sets `components`, they are imported by `importPath` when it is set, else by the package's `exports`. An import through an export that leads to a source file that is not there names no component: `<Tooltip>` from `@acme/ui/components/tooltip`, where `"./components/*": "./src/components/*.tsx"` leads to no `src/components/tooltip.tsx`, is reported by `no-unknown-component`. The export is the one Node.js picks, and a `.jsx` target is there when the `.tsx` file TypeScript reads for it is. One whose export leads to a build folder with no file there (`"./*": "./dist/components/*.js"`, not built yet, whatever the extension) is said not to have been checked, since its source may be any file the build reads (`src/components/tooltip.ts`).
- **What applies to the app:** the rules' severities and options, `elements`, `importPath` and `libraries`.
- **What stays with the package:** `tsconfig` (the app keeps its own), `overrides` (their globs are about the package's files) and `includeDesignSystem`.
- **A Tailwind theme partial keeps Tailwind's defaults.** A theme the package ships for apps that import Tailwind themselves (`@theme`, with no `@import "tailwindcss"` in it) sets no spacing unit, and only the radii it names. In an app on Tailwind v4 (`@tailwindcss/postcss`, `@tailwindcss/vite` or `@tailwindcss/cli` in its `package.json`, or `tailwindcss` in a range other than v3's, such as `^4`), Tailwind's default spacing unit and radius scale apply too, as from a stylesheet that imports Tailwind, unless the theme clears them (`--spacing-*: initial`), so `no-hardcoded-spacing` runs.
- **A missing file is not an error in the app.** A token file the config names that is not there matches nothing, since a published package ships only some of its files. In the package itself it is an error, so a typo is caught there.

A project reads up to two configs, the first over the second, field by field as [an app's own config](#an-apps-own-config) says, and zero config fills in the rest:
Expand All @@ -65,7 +66,7 @@ config onsystem.config.json, over node_modules/@acme/ui/onsystem.config.json (

`init --force` in an app on such a package, without a config or with one whose `components` match no file outside the package, writes a config with only `$schema` in it, for the app's own rules and overrides, and says what it builds on. In an app whose config names components of its own, it writes them again, as it does without such a package.

`"extends": false` in the app's config reads it alone. Zero config then fills in only the fields it leaves out, as in any project: it finds a package named like a design system, as a workspace package or installed in `node_modules` with `exports` that point at its component sources ([without the config](#without-the-config)), but not one named otherwise (`@acme/kit`) or one whose `exports` point at compiled code: of those, it reads only the type declarations a package publishes, when it declares its config ([what apps get](#what-apps-get)). So such a config that only changes a rule's severity can leave the app without the package's components and tokens, and `check` then says which rules did not run. Copy `components`, `importPath` and `tokens` from the package's config into the app's, with paths into the package: `node_modules/@acme/ui/src/**/*.tsx`, or `../../packages/ui/src/**/*.tsx` from a workspace app. `init` writes no `importPath` for a package that apps import by subpath (`@acme/ui/components/button`), so when the package's config has none, set it to the package's name (`@acme/ui`). Without it, `no-unknown-component`, `no-unknown-prop`, `no-unknown-variant`, and `icon-button-accessible-name` on the package's `<Button>`, check none of the app's imports from the package, and nothing says so. With it, the same rules run as with the package's config, but the imports that findings and `get_component` suggest name `@acme/ui`, not the subpath.
`"extends": false` in the app's config reads it alone. Zero config then fills in only the fields it leaves out, as in any project: it finds a package named like a design system, as a workspace package or installed in `node_modules` with `exports` that point at its component sources ([without the config](#without-the-config)), but not one named otherwise (`@acme/kit`) or one whose `exports` point at compiled code: of those, it reads only the type declarations a package publishes, when it declares its config ([what apps get](#what-apps-get)). So such a config that only changes a rule's severity can leave the app without the package's components and tokens, and `check` then says which rules did not run. Copy `components`, `importPath` and `tokens` from the package's config into the app's, with paths into the package: `node_modules/@acme/ui/src/**/*.tsx`, or `../../packages/ui/src/**/*.tsx` from a workspace app. `init` writes no `importPath` for a package that apps import by subpath (`@acme/ui/components/button`), so when the package's config has none, set it to the package's name (`@acme/ui`). Without it, `no-unknown-component`, `no-unknown-prop`, `no-unknown-variant`, and `icon-button-accessible-name` on the package's `<Button>`, check none of the app's imports from the package, and nothing says so. With it, the same rules run as with the package's config, but the imports that findings and `get_component` suggest name `@acme/ui`, not the subpath. Such a config does not read the package's `exports`, so in a workspace app a component imported by a subpath with no file behind it (`@acme/ui/components/tooltip`) is said not to have been checked, rather than reported.

The declared config is read as JSON and never run, so a dependency cannot make an app run its code. A field that names a `.js` or `.ts` config, a file outside the package, or a file that does not exist is an error that names the package when it is a workspace package, which its team can fix; in an app with a config of its own, it is a warning, and that config is read alone. In a package installed in `node_modules`, the declaration is skipped with a warning, and the app is read as if the package declared none.

Expand All @@ -89,6 +90,7 @@ In an app whose package publishes only `dist/`, the components are read from the
When the package publishes no type declarations either:

- **No component is known.** `no-unknown-component`, `no-unknown-prop` and `no-unknown-variant` check none of the components the app imports from the package, `prefer-design-system-component` reports nothing, `icon-button-accessible-name` does not know that the package's `<Button>` renders a button, and agents find no component with `list_components`, `search_components` or `get_component`. `check` says `No design-system components found: the component rules did not run.`
- **`list_components`, `get_component` and `inspect --explain` say why:** `@acme/ui publishes neither its component sources nor their type declarations, so its components are unknown here and the component rules did not run: ask its team to add its sources to "files" in its package.json, or to publish type declarations.` When the package publishes type declarations and the app's config turns them off with `"libraries": false`, they say that instead: `"libraries": []` reads them, and no library.
- **`importPath` does not turn the component rules on**, with no declarations to read. `init` sets it when apps import the package by one name, such as `@acme/ui`. With no component to compare against, imports from the package are not checked: `<Badge>` from `@acme/ui` is not reported, although the package has no `Badge`. It still tells `icon-button-accessible-name` that an icon the package re-exports is an icon, so `<button><ChevronLeft /></button>`, with `ChevronLeft` from `@acme/ui`, is reported for having no accessible name. Without `importPath`, that button is not reported.
- **The token rules check against the published tokens:** `no-hardcoded-color` when they hold colors, `no-hardcoded-spacing` and `no-hardcoded-radius` when they set a spacing scale and radii.
- **`--require-design-system` fails** (exit code 2), and it is on by default in the [GitHub Action](required-check.md).
Expand Down
Loading
Loading