From c6cc74d5c720f4244e5dd7cd853077c7a783ebff Mon Sep 17 00:00:00 2001 From: Diogo Esteves Date: Fri, 30 Oct 2026 17:53:36 +0000 Subject: [PATCH 1/3] fix: ship culori's types with the package; Cursor manifest and CONTRIBUTING gaps - @types/culori moves from devDependencies to dependencies: the published index.d.ts imports Oklch from culori, which ships no types, so a project with skipLibCheck off failed with TS7016 on `import { defineConfig } from 'onsystem'`. A test builds the declarations and checks that every package they import comes with its types from dependencies or peers. - The Cursor plugin manifest drops displayName, which Cursor's manifest reference does not have; the plugin test holds it to those fields. - CONTRIBUTING links the good first issue label, gives an npm fallback for Corepack, explains the site bin warning before the first build, and lists the examples typecheck and the Website job with how to preview the docs. Roadmap item OS2-59. --- .changeset/culori-types.md | 5 ++ CONTRIBUTING.md | 7 +- package.json | 2 +- .../.cursor-plugin/plugin.json | 1 - pnpm-lock.yaml | 6 +- test/plugin.test.ts | 20 +++++ test/published-types.test.ts | 78 +++++++++++++++++++ 7 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 .changeset/culori-types.md create mode 100644 test/published-types.test.ts diff --git a/.changeset/culori-types.md b/.changeset/culori-types.md new file mode 100644 index 00000000..b4c3a58a --- /dev/null +++ b/.changeset/culori-types.md @@ -0,0 +1,5 @@ +--- +'onsystem': patch +--- + +The published types no longer fail a TypeScript project that checks library types (`skipLibCheck: false`) with TS7016, "Could not find a declaration file for module 'culori'", as soon as it imports `onsystem`, as an `onsystem.config.ts` with `defineConfig` does. The color types the API uses come from `@types/culori`, which is now a dependency rather than a devDependency; with `skipLibCheck: true`, those types were read as `any`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc1a8272..126aefff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Bug reports with a minimal snippet and the `inspect` output are the most useful contribution: most fixes start from one. For a change in behaviour, open an issue first so we can agree on it before you write it. +Bug reports with a minimal snippet and the `inspect` output are the most useful contribution: most fixes start from one. For a change in behaviour, open an issue first so we can agree on it before you write it. Issues that make a good first change are labelled [good first issue](https://github.com/dgesteves/onsystem/labels/good%20first%20issue). ## New integrations @@ -17,6 +17,8 @@ corepack enable pnpm install ``` +Without Corepack, install pnpm 11 with npm (`npm install -g pnpm@11`), then run `pnpm install`. Until the first `pnpm build`, `pnpm install` warns that it failed to create a bin at `site/node_modules/.bin/onsystem`: the website depends on this package, whose `dist/cli.js` the build writes. The warning is harmless. + ## Checks CI runs these on every pull request, in this order: @@ -25,6 +27,7 @@ CI runs these on every pull request, in this order: pnpm lint # ESLint pnpm format:check # Prettier (pnpm format writes) pnpm typecheck # tsc --noEmit +pnpm --filter "./examples/*" typecheck # the examples, against this package's types pnpm build # tsdown, then schema.json from the config schema pnpm smoke # the built server over stdio, every tool, against examples/shadcn-demo pnpm test # Vitest, on Node 22 and 24 (Linux), and on Windows and macOS @@ -33,6 +36,8 @@ pnpm pack-test # npm pack, install into a new project, run it (Linux, Wind A pull request that touches `src/` also runs the [real-world corpus](#real-world-corpus), which is not a required check yet. +The Website job builds the site, which renders `docs/*.md` and data generated from the built library: after `pnpm build`, it runs `pnpm --filter site generate`, then the site's `test`, `typecheck`, `lint` and `build`. Run them when you change the docs or `site/`. To see a docs page as the website shows it, run `pnpm build`, then `pnpm --filter site dev`, which serves the site on http://localhost:3300. + `pnpm smoke` spawns `dist/cli.js` the way an MCP client does, so it catches what unit tests cannot: a broken build, a tool that fails over stdio, output that no longer parses. It connects twice, on the 2025 protocol and on 2026-07-28. Run it after `pnpm build`; add `--verbose` to see every response. CI also runs `check` on the demo's clean page (it must pass) and the Claude Code hook on its draft (it must block). `pnpm pack-test` (after `pnpm build`) tests the package as a user gets it: it packs the repository with `npm pack`, so the publish hooks run, installs the tarball into a new project in the system temp folder, and runs the installed `onsystem` bin (`--version`, `inspect`, `check`), an MCP stdio handshake with the installed server in both protocol eras, and the Claude Code hook, which picks up the project's own install. Then it installs the tarball with plain npm into a project already on ESLint 8.57.0, the oldest version the `eslint` peer range admits (npm stops the install when the range leaves the project's ESLint out), checks that the installed `onsystem/eslint` imports no `eslint`, and lints and fixes a page with that ESLint (`scripts/pack-test-eslint8.mjs`). It needs the npm registry for the package's dependencies. diff --git a/package.json b/package.json index d7e8db7b..fc6fb55b 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ }, "dependencies": { "@modelcontextprotocol/server": "^2.3.1", + "@types/culori": "^4.0.1", "culori": "^4.0.2", "postcss": "^8.5.30", "stemmer": "^2.0.1", @@ -110,7 +111,6 @@ "@eslint/js": "^10.0.1", "@modelcontextprotocol/client": "^2.3.1", "@modelcontextprotocol/sdk": "^1.32.1", - "@types/culori": "^4.0.1", "@types/node": "^24.19.2", "@vitest/coverage-istanbul": "^5.0.5", "ajv": "^8.20.0", diff --git a/plugins/onsystem-cursor/.cursor-plugin/plugin.json b/plugins/onsystem-cursor/.cursor-plugin/plugin.json index 11effe9c..4405eccd 100644 --- a/plugins/onsystem-cursor/.cursor-plugin/plugin.json +++ b/plugins/onsystem-cursor/.cursor-plugin/plugin.json @@ -1,6 +1,5 @@ { "name": "onsystem", - "displayName": "onsystem", "version": "0.11.2", "description": "Keeps coding agents on your design system: catches palette colors, native elements and APIs your system lacks, at every edit and pull request, with the fix. Set it up once in your design-system package; every app in your monorepo gets it.", "author": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49dc0980..68ab6f1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@modelcontextprotocol/server': specifier: ^2.3.1 version: 2.3.1 + '@types/culori': + specifier: ^4.0.1 + version: 4.0.1 culori: specifier: ^4.0.2 version: 4.0.2 @@ -48,9 +51,6 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.32.1 version: 1.32.2(zod@4.6.6) - '@types/culori': - specifier: ^4.0.1 - version: 4.0.1 '@types/node': specifier: ^24.19.2 version: 24.19.2 diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 8456a83a..b1892b8a 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -246,6 +246,26 @@ describe('packages for other agents', () => { ]); const plugin = readJson('plugins/onsystem-cursor/.cursor-plugin/plugin.json'); expect(plugin).toMatchObject({ name: 'onsystem', version, logo: 'icon.svg' }); + // The fields of Cursor's manifest reference (cursor.com/docs/reference/plugins), and no other: + // `displayName` is the Claude Code plugin's, not Cursor's. + const fields = [ + 'name', + 'description', + 'version', + 'author', + 'homepage', + 'repository', + 'license', + 'keywords', + 'logo', + 'rules', + 'agents', + 'skills', + 'commands', + 'hooks', + 'mcpServers', + ]; + expect(Object.keys(plugin).filter((key) => !fields.includes(key))).toEqual([]); expect(plugin.name).toMatch(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/); expect(fs.existsSync(path.join(CURSOR, 'icon.svg'))).toBe(true); const skill = frontMatter(path.join(CURSOR, SKILL)); diff --git a/test/published-types.test.ts b/test/published-types.test.ts new file mode 100644 index 00000000..7d7c2332 --- /dev/null +++ b/test/published-types.test.ts @@ -0,0 +1,78 @@ +import fs from 'node:fs'; +import { builtinModules } from 'node:module'; +import path from 'node:path'; + +import { build } from 'tsdown'; +import { beforeAll, describe, expect, it } from 'vitest'; + +// The type declarations the package publishes import types from other packages: an install has +// those types only when `dependencies` (or a peer) brings them. `Oklch` from culori, which ships +// no types of its own, came from @types/culori, a devDependency, so a project with skipLibCheck +// off failed to typecheck on `import { defineConfig } from 'onsystem'` (TS7016), and with it on +// read those types as `any`. Built as it ships, into a folder of its own. +const REPO = path.resolve(import.meta.dirname, '..'); +const OUT = path.join(REPO, 'node_modules/.cache/onsystem-published-types-test'); +const pkg = JSON.parse(fs.readFileSync(path.join(REPO, 'package.json'), 'utf8')) as { + dependencies: Record; + peerDependencies: Record; +}; + +let declarations: Map; + +beforeAll(async () => { + await build({ + cwd: REPO, + config: false, + entry: { index: 'src/index.ts', eslint: 'src/eslint/index.ts' }, + outDir: OUT, + format: 'esm', + platform: 'node', + target: 'node22.18', + fixedExtension: false, + dts: true, + publint: false, + clean: true, + logLevel: 'silent', + }); + declarations = new Map( + fs + .readdirSync(OUT) + .filter((file) => file.endsWith('.d.ts')) + .map((file) => [file, fs.readFileSync(path.join(OUT, file), 'utf8')]), + ); +}, 120_000); + +/** The package an import specifier names: `culori`, `@modelcontextprotocol/server`. */ +function packageOf(specifier: string): string { + const parts = specifier.split('/'); + return (specifier.startsWith('@') ? parts.slice(0, 2) : parts.slice(0, 1)).join('/'); +} + +/** Whether the installed package ships its own type declarations. */ +function shipsTypes(name: string): boolean { + const json = fs.readFileSync(path.join(REPO, 'node_modules', name, 'package.json'), 'utf8'); + const { types, typings } = JSON.parse(json) as { types?: string; typings?: string }; + return Boolean(types ?? typings) || /"types"\s*:/.test(json); +} + +describe('the published type declarations', () => { + it('import types only from packages an install brings with their types', () => { + const imported = new Set(); + for (const text of declarations.values()) { + for (const match of text.matchAll(/(?:\bfrom\s+|\bimport\(\s*)["']([^"'.][^"']*)["']/g)) { + const specifier = match[1] ?? ''; + if (specifier.startsWith('node:') || builtinModules.includes(specifier)) continue; + imported.add(packageOf(specifier)); + } + } + // The build read the public entry points: the API's types come from these. + expect(imported).toContain('culori'); + const installed = { ...pkg.dependencies, ...pkg.peerDependencies }; + for (const name of imported) { + expect(installed, `${name} is imported by the declarations`).toHaveProperty([name]); + if (shipsTypes(name)) continue; + const types = `@types/${name.replace(/^@/, '').replace('/', '__')}`; + expect(pkg.dependencies, `${name} ships no types: ${types}`).toHaveProperty([types]); + } + }); +}); From 1ffb4520cb4e00fc8e019621992730887c310598 Mon Sep 17 00:00:00 2001 From: Diogo Esteves Date: Fri, 30 Oct 2026 18:03:37 +0000 Subject: [PATCH 2/3] docs: CONTRIBUTING lists Node 26 in the test matrix, as CI runs it --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 126aefff..d818cd7e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ pnpm typecheck # tsc --noEmit pnpm --filter "./examples/*" typecheck # the examples, against this package's types pnpm build # tsdown, then schema.json from the config schema pnpm smoke # the built server over stdio, every tool, against examples/shadcn-demo -pnpm test # Vitest, on Node 22 and 24 (Linux), and on Windows and macOS +pnpm test # Vitest, on Node 22, 24 and 26 (Linux), and on Windows and macOS pnpm pack-test # npm pack, install into a new project, run it (Linux, Windows, macOS) ``` From 54d541a4423f462cfe1de7325792d6289f774ccd Mon Sep 17 00:00:00 2001 From: Diogo Esteves Date: Fri, 30 Oct 2026 18:16:17 +0000 Subject: [PATCH 3/3] fix: keep displayName in the Cursor manifest, which Cursor's schema lists Cursor's manifest schema (cursor/plugins, schemas/plugin.schema.json) has displayName, so the test now holds the manifest to that schema's properties. --- .../onsystem-cursor/.cursor-plugin/plugin.json | 1 + test/plugin.test.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/plugins/onsystem-cursor/.cursor-plugin/plugin.json b/plugins/onsystem-cursor/.cursor-plugin/plugin.json index 4405eccd..11effe9c 100644 --- a/plugins/onsystem-cursor/.cursor-plugin/plugin.json +++ b/plugins/onsystem-cursor/.cursor-plugin/plugin.json @@ -1,5 +1,6 @@ { "name": "onsystem", + "displayName": "onsystem", "version": "0.11.2", "description": "Keeps coding agents on your design system: catches palette colors, native elements and APIs your system lacks, at every edit and pull request, with the fix. Set it up once in your design-system package; every app in your monorepo gets it.", "author": { diff --git a/test/plugin.test.ts b/test/plugin.test.ts index b1892b8a..a05a8c65 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -246,23 +246,29 @@ describe('packages for other agents', () => { ]); const plugin = readJson('plugins/onsystem-cursor/.cursor-plugin/plugin.json'); expect(plugin).toMatchObject({ name: 'onsystem', version, logo: 'icon.svg' }); - // The fields of Cursor's manifest reference (cursor.com/docs/reference/plugins), and no other: - // `displayName` is the Claude Code plugin's, not Cursor's. + // The properties of Cursor's manifest schema (github.com/cursor/plugins, + // schemas/plugin.schema.json), and no other: the schema sets additionalProperties to false. const fields = [ 'name', + 'displayName', 'description', 'version', + 'minClientVersions', 'author', + 'publisher', 'homepage', 'repository', 'license', - 'keywords', 'logo', - 'rules', + 'keywords', + 'category', + 'tags', + 'commands', 'agents', 'skills', - 'commands', + 'rules', 'hooks', + 'variables', 'mcpServers', ]; expect(Object.keys(plugin).filter((key) => !fields.includes(key))).toEqual([]);