diff --git a/packages/docs/package.json b/packages/docs/package.json index 5c29c1a20..57c44beef 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -58,6 +58,7 @@ "react": "catalog:react19", "react-dom": "catalog:react19", "recharts": "3.3.0", + "registry": "workspace:*", "remark-smartypants": "^3.0.2", "res": "workspace:*", "server-only": "^0.0.1", diff --git a/packages/docs/src/registry/assemble.ts b/packages/docs/src/registry/assemble.ts index 0044395e0..47276f7d0 100755 --- a/packages/docs/src/registry/assemble.ts +++ b/packages/docs/src/registry/assemble.ts @@ -16,12 +16,17 @@ import { // Paths const __dirname = fileURLToPath(new URL('.', import.meta.url)) const packageRoot = resolve(__dirname, '../../') -const itemsDir = resolve(__dirname, './items') +const docsItemsDir = resolve(__dirname, './items') +const registryItemsDir = resolve(packageRoot, 'node_modules/registry/items') const remoteDir = resolve(__dirname, './remote') const registryJson = resolve(packageRoot, 'registry.json') async function loadItems() { - const itemFiles = await Array.fromAsync(glob(`${itemsDir}/*.json`)) + const [localFiles, registryFiles] = await Promise.all([ + Array.fromAsync(glob(`${docsItemsDir}/*.json`)), + Array.fromAsync(glob(`${registryItemsDir}/*.json`)) + ]) + const itemFiles = [...localFiles, ...registryFiles] return await Promise.all( itemFiles.map(async filePath => { const item = await loadItem(filePath) diff --git a/packages/docs/src/registry/read.ts b/packages/docs/src/registry/read.ts index 104db7569..7acf2d6bb 100644 --- a/packages/docs/src/registry/read.ts +++ b/packages/docs/src/registry/read.ts @@ -31,12 +31,20 @@ export async function readRegistryItem(name: string) { } export async function readUsage(name: string) { - try { - const fileName = resolve(process.cwd(), `src/registry/items/${name}.md`) - return await readFile(fileName, 'utf-8') - } catch { - return null + const paths = [ + resolve(process.cwd(), `src/registry/items/${name}.md`), + resolve(process.cwd(), `node_modules/registry/items/${name}.md`) + ] + + for (const filePath of paths) { + try { + return await readFile(filePath, 'utf-8') + } catch { + continue + } } + + return null } export const registryItemCategories = [ diff --git a/packages/nuqs/package.json b/packages/nuqs/package.json index af07a6e6f..138edec33 100644 --- a/packages/nuqs/package.json +++ b/packages/nuqs/package.json @@ -48,7 +48,8 @@ "adapters/react-router/v7.d.ts", "adapters/tanstack-router.d.ts", "adapters/custom.d.ts", - "adapters/testing.d.ts" + "adapters/testing.d.ts", + "lib/index.d.ts" ], "type": "module", "sideEffects": false, @@ -125,6 +126,11 @@ "types": "./dist/adapters/testing.d.ts", "import": "./dist/adapters/testing.js", "default": "./dist/adapters/testing.js" + }, + "./lib": { + "types": "./dist/lib.d.ts", + "import": "./dist/lib.js", + "default": "./dist/lib.js" } }, "scripts": { diff --git a/packages/nuqs/src/api.test.ts b/packages/nuqs/src/api.test.ts index c17f6e61f..443691a01 100644 --- a/packages/nuqs/src/api.test.ts +++ b/packages/nuqs/src/api.test.ts @@ -73,6 +73,9 @@ const exports = ` "NuqsTestingAdapter": "function", "withNuqsTestingAdapter": "function", }, + "./lib": { + "safeParse": "function", + }, "./server": { "createLoader": "function", "createMultiParser": "function", diff --git a/packages/nuqs/src/lib/index.ts b/packages/nuqs/src/lib/index.ts new file mode 100644 index 000000000..721dad157 --- /dev/null +++ b/packages/nuqs/src/lib/index.ts @@ -0,0 +1 @@ +export * from './safe-parse' diff --git a/packages/nuqs/tsdown.config.ts b/packages/nuqs/tsdown.config.ts index 3490ba574..5c85c95bc 100644 --- a/packages/nuqs/tsdown.config.ts +++ b/packages/nuqs/tsdown.config.ts @@ -32,7 +32,8 @@ const entrypoints = { 'adapters/react-router/v7': 'src/adapters/react-router/v7.ts', 'adapters/tanstack-router': 'src/adapters/tanstack-router.ts', 'adapters/custom': 'src/adapters/custom.ts', - 'adapters/testing': 'src/adapters/testing.ts' + 'adapters/testing': 'src/adapters/testing.ts', + lib: 'src/lib/index.ts' }, server: { server: 'src/index.server.ts', diff --git a/packages/registry/.gitignore b/packages/registry/.gitignore new file mode 100644 index 000000000..a0cac096e --- /dev/null +++ b/packages/registry/.gitignore @@ -0,0 +1 @@ +public/r/ \ No newline at end of file diff --git a/packages/registry/README.md b/packages/registry/README.md new file mode 100644 index 000000000..32e47d2eb --- /dev/null +++ b/packages/registry/README.md @@ -0,0 +1,30 @@ +# registry + +A shadcn/ui compatible registry for community parsers and utilities for the [nuqs](https://nuqs.dev) library. + +## Development + +```bash +# Install dependencies +pnpm install + +# Build the registry +cd ../docs && pnpm build:registry +# Run the docs server +cd ../docs && pnpm dev + +``` + +Usage example in any npm package: + +```bash +pnpm dlx shadcn@latest add http://localhost:3000/r/parse-as-tuple.json +``` + +## Learn More + +To learn more about nuqs and shadcn/ui registries, take a look at the following resources: + +- [nuqs Documentation](https://nuqs.dev) - learn about nuqs features and API +- [shadcn/ui Registry Documentation](https://ui.shadcn.com/docs/registry) - learn about shadcn/ui registries +- [nuqs GitHub Repository](https://github.com/47ng/nuqs) - source code and issues diff --git a/packages/registry/items/parse-as-tuple.json b/packages/registry/items/parse-as-tuple.json new file mode 100644 index 000000000..a8836f689 --- /dev/null +++ b/packages/registry/items/parse-as-tuple.json @@ -0,0 +1,14 @@ +{ + "name": "parse-as-tuple", + "type": "registry:item", + "title": "Tuples", + "description": "Parse query string as a tuple of values.", + "dependencies": ["nuqs"], + "files": [ + { + "path": "node_modules/registry/items/parse-as-tuple.ts", + "type": "registry:file", + "target": "~/src/lib/parsers/parse-as-tuple.ts" + } + ] +} diff --git a/packages/registry/items/parse-as-tuple.md b/packages/registry/items/parse-as-tuple.md new file mode 100644 index 000000000..26a7f1598 --- /dev/null +++ b/packages/registry/items/parse-as-tuple.md @@ -0,0 +1,12 @@ +The `parseAsTuple` parser allows you to parse fixed-length tuples with **any type** for each position. + +```ts +import { parseAsTuple } from '@/lib/parsers/parse-as-tuple' +import { parseAsString, parseAsStringLiteral } from 'nuqs' + +// Sorting tuple [key: string, direction: 'asc' | 'desc'] +parseAsTuple([parseAsString, parseAsStringLiteral(['asc', 'desc'])]) + +// Optionally, customise the separator +parseAsTuple([parseAsString, parseAsString], ';') +``` diff --git a/packages/registry/items/parse-as-tuple.test.ts b/packages/registry/items/parse-as-tuple.test.ts new file mode 100644 index 000000000..4c5f4a9f9 --- /dev/null +++ b/packages/registry/items/parse-as-tuple.test.ts @@ -0,0 +1,41 @@ +import { parseAsBoolean, parseAsInteger, parseAsString } from 'nuqs' +import { + isParserBijective, + testParseThenSerialize, + testSerializeThenParse +} from 'nuqs/testing' +import { describe, expect, it } from 'vitest' +import { parseAsTuple } from './parse-as-tuple' + +describe('parseAsTuple', () => { + it('parses and serializes tuples correctly', () => { + const parser = parseAsTuple([parseAsInteger, parseAsString, parseAsBoolean]) + expect(parser.parse('1,a,false,will-ignore')).toStrictEqual([1, 'a', false]) + expect(parser.parse('not-a-number,a,true')).toBeNull() + expect(parser.parse('1,a')).toBeNull() + // @ts-expect-error - Tuple length is less than 2 + expect(() => parseAsTuple([parseAsInteger])).toThrow() + expect(parser.serialize([1, 'a', true])).toBe('1,a,true') + // @ts-expect-error - Tuple length mismatch + expect(() => parser.serialize([1, 'a'])).toThrow() + expect(testParseThenSerialize(parser, '1,a,true')).toBe(true) + expect(testSerializeThenParse(parser, [1, 'a', true] as const)).toBe(true) + expect(isParserBijective(parser, '1,a,true', [1, 'a', true] as const)).toBe( + true + ) + expect(() => + isParserBijective(parser, 'not-a-tuple', [1, 'a', true] as const) + ).toThrow() + }) + + it('equality comparison works correctly', () => { + const eq = parseAsTuple([parseAsInteger, parseAsBoolean]).eq! + expect(eq([1, true], [1, true])).toBe(true) + expect(eq([1, true], [1, false])).toBe(false) + expect(eq([1, true], [2, true])).toBe(false) + // @ts-expect-error - Tuple length mismatch + expect(eq([1, true], [1])).toBe(false) + // @ts-expect-error - Tuple length mismatch + expect(eq([1], [1])).toBe(false) + }) +}) diff --git a/packages/registry/items/parse-as-tuple.ts b/packages/registry/items/parse-as-tuple.ts new file mode 100644 index 000000000..78299fcff --- /dev/null +++ b/packages/registry/items/parse-as-tuple.ts @@ -0,0 +1,74 @@ +import { createParser, type SingleParserBuilder } from 'nuqs' +import { safeParse } from 'nuqs/lib' + +type ParserTuple = { + [K in keyof T]: SingleParserBuilder +} & { length: 2 | 3 | 4 | 5 | 6 | 7 | 8 } + +/** + * Parse a comma-separated tuple with type-safe positions. + * Items are URI-encoded for safety, so they may not look nice in the URL. + * allowed tuple length is 2-8. + * + * @param itemParsers Tuple of parsers for each position in the tuple + * @param separator The character to use to separate items (default ',') + */ +export function parseAsTuple( + itemParsers: ParserTuple, + separator = ',' +): SingleParserBuilder { + const encodedSeparator = encodeURIComponent(separator) + if (itemParsers.length < 2 || itemParsers.length > 8) { + throw new Error( + `Tuple length must be between 2 and 8, got ${itemParsers.length}` + ) + } + return createParser({ + parse: query => { + if (query === '') { + return null + } + const parts = query.split(separator) + if (parts.length < itemParsers.length) { + return null + } + // iterating by parsers instead of parts, any additional parts are ignored. + const result = itemParsers.map( + (parser, index) => + safeParse( + parser.parse, + parts[index]!.replaceAll(encodedSeparator, separator), + `[${index}]` + ) as T[number] | null + ) + return result.some(x => x === null) ? null : (result as T) + }, + serialize: (values: T) => { + if (values.length !== itemParsers.length) { + throw new Error( + `Tuple length mismatch: expected ${itemParsers.length}, got ${values.length}` + ) + } + return values + .map((value, index) => { + const parser = itemParsers[index]! + const str = parser.serialize ? parser.serialize(value) : String(value) + return str.replaceAll(separator, encodedSeparator) + }) + .join(separator) + }, + eq(a: T, b: T) { + if (a === b) { + return true + } + if (a.length !== b.length || a.length !== itemParsers.length) { + return false + } + return a.every((value, index) => { + const parser = itemParsers[index]! + const itemEq = parser.eq ?? ((x, y) => x === y) + return itemEq(value, b[index]) + }) + } + }) +} diff --git a/packages/registry/package.json b/packages/registry/package.json new file mode 100644 index 000000000..1de0aad4c --- /dev/null +++ b/packages/registry/package.json @@ -0,0 +1,24 @@ +{ + "name": "registry", + "description": "Shadcn CLI registry for community parsers & utilities", + "type": "module", + "exports": { + "./items/*": "./items/*" + }, + "files": [ + "items" + ], + "scripts": { + "test": "pnpm run --stream '/^test:/'", + "test:unit": "vitest run --typecheck" + }, + "dependencies": { + "next": "16.0.7", + "nuqs": "workspace:*", + "shadcn": "^3.4.2" + }, + "devDependencies": { + "typescript": "^5.9.2", + "vitest": "^4.0.1" + } +} diff --git a/packages/registry/tsconfig.json b/packages/registry/tsconfig.json new file mode 100644 index 000000000..f5b521155 --- /dev/null +++ b/packages/registry/tsconfig.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "jsx": "react", + // Type checking + "strict": true, + "noUncheckedIndexedAccess": true, + "alwaysStrict": false, // Don't emit "use strict" to avoid conflicts with "use client" + // Modules + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + // Language & Environment + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + // Emit + "noEmit": true, + "declaration": true, + "declarationMap": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + + "downlevelIteration": true, + // Interop + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + // Misc + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "incremental": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/registry/vitest.config.ts b/packages/registry/vitest.config.ts new file mode 100644 index 000000000..6e0d39b6a --- /dev/null +++ b/packages/registry/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig, type ViteUserConfig } from 'vitest/config' + +const config: ViteUserConfig = defineConfig({ + test: { + typecheck: { + tsconfig: './tsconfig.json' + } + } +}) + +export default config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b01f471af..c7969af26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,9 @@ importers: recharts: specifier: 3.3.0 version: 3.3.0(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react-is@18.3.1)(react@19.2.1)(redux@5.0.1) + registry: + specifier: workspace:* + version: link:../registry remark-smartypants: specifier: ^3.0.2 version: 3.0.2 @@ -820,6 +823,25 @@ importers: specifier: ^4.1.5 version: 4.1.5 + packages/registry: + dependencies: + next: + specifier: 16.0.7 + version: 16.0.7(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + nuqs: + specifier: workspace:* + version: link:../nuqs + shadcn: + specifier: ^3.4.2 + version: 3.4.2(@types/node@24.3.0)(typescript@5.9.2) + devDependencies: + typescript: + specifier: ^5.9.2 + version: 5.9.2 + vitest: + specifier: ^4.0.1 + version: 4.0.1(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.11.6(@types/node@24.3.0)(typescript@5.9.2))(terser@5.43.1)(tsx@4.20.4)(yaml@2.8.1) + packages/res: {} packages/scripts: @@ -1235,8 +1257,8 @@ packages: '@emnapi/core@1.5.0': resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} - '@emnapi/runtime@1.5.0': - resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@emnapi/runtime@1.6.0': + resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -4560,6 +4582,10 @@ packages: resolution: {integrity: sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==} hasBin: true + baseline-browser-mapping@2.8.9: + resolution: {integrity: sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==} + hasBin: true + basic-auth@2.0.1: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} @@ -4615,6 +4641,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.26.2: + resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + browserslist@4.27.0: resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -4665,6 +4696,9 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + caniuse-lite@1.0.30001745: + resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==} + caniuse-lite@1.0.30001751: resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} @@ -5284,6 +5318,9 @@ packages: electron-to-chromium@1.5.207: resolution: {integrity: sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==} + electron-to-chromium@1.5.227: + resolution: {integrity: sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==} + electron-to-chromium@1.5.239: resolution: {integrity: sha512-1y5w0Zsq39MSPmEjHjbizvhYoTaulVtivpxkp5q5kaPmQtsK6/2nvAzGRxNMS9DoYySp9PkW0MAQDwU1m764mg==} @@ -7304,6 +7341,9 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.21: + resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + node-releases@2.0.26: resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} @@ -9907,7 +9947,7 @@ snapshots: dependencies: '@babel/compat-data': 7.28.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.3 + browserslist: 4.26.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -10318,7 +10358,7 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.5.0': + '@emnapi/runtime@1.6.0': dependencies: tslib: 2.8.1 optional: true @@ -10757,7 +10797,7 @@ snapshots: '@img/sharp-wasm32@0.34.4': dependencies: - '@emnapi/runtime': 1.5.0 + '@emnapi/runtime': 1.6.0 optional: true '@img/sharp-win32-arm64@0.34.4': @@ -10947,7 +10987,7 @@ snapshots: '@napi-rs/wasm-runtime@1.0.7': dependencies: '@emnapi/core': 1.5.0 - '@emnapi/runtime': 1.5.0 + '@emnapi/runtime': 1.6.0 '@tybys/wasm-util': 0.10.1 optional: true @@ -13310,7 +13350,7 @@ snapshots: '@vanilla-extract/private': 1.0.9 css-what: 6.2.2 cssesc: 3.0.0 - csstype: 3.1.3 + csstype: 3.2.3 dedent: 1.6.0 deep-object-diff: 1.1.9 deepmerge: 4.3.1 @@ -13754,6 +13794,8 @@ snapshots: baseline-browser-mapping@2.8.19: {} + baseline-browser-mapping@2.8.9: {} + basic-auth@2.0.1: dependencies: safe-buffer: 5.1.2 @@ -13834,6 +13876,14 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.3) + browserslist@4.26.2: + dependencies: + baseline-browser-mapping: 2.8.9 + caniuse-lite: 1.0.30001745 + electron-to-chromium: 1.5.227 + node-releases: 2.0.21 + update-browserslist-db: 1.1.3(browserslist@4.26.2) + browserslist@4.27.0: dependencies: baseline-browser-mapping: 2.8.19 @@ -13893,6 +13943,8 @@ snapshots: callsites@3.1.0: {} + caniuse-lite@1.0.30001745: {} + caniuse-lite@1.0.30001751: {} caseless@0.12.0: {} @@ -14468,6 +14520,8 @@ snapshots: electron-to-chromium@1.5.207: {} + electron-to-chromium@1.5.227: {} + electron-to-chromium@1.5.239: {} emoji-regex@10.6.0: {} @@ -17134,6 +17188,8 @@ snapshots: node-releases@2.0.19: {} + node-releases@2.0.21: {} + node-releases@2.0.26: {} normalize-package-data@5.0.0: @@ -19324,6 +19380,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.1.3(browserslist@4.26.2): + dependencies: + browserslist: 4.26.2 + escalade: 3.2.0 + picocolors: 1.1.1 + update-browserslist-db@1.1.4(browserslist@4.27.0): dependencies: browserslist: 4.27.0 diff --git a/turbo.json b/turbo.json index 5bc3bad71..910e1286e 100644 --- a/turbo.json +++ b/turbo.json @@ -75,6 +75,9 @@ "outputs": ["dist/**", "coverage/**"], "dependsOn": ["build"] }, + "registry#test": { + "dependsOn": ["^build"] + }, "docs#test": { "dependsOn": ["^build"] },