diff --git a/src/generator/dts.ts b/src/generator/dts.ts index 04ef406..4f3123e 100644 --- a/src/generator/dts.ts +++ b/src/generator/dts.ts @@ -38,6 +38,7 @@ const SCHEMA_KEYS = [ 'title', 'description', '$schema', + '$constraints', 'type', 'tsType', 'markdownType', @@ -187,6 +188,9 @@ function generateJSDoc (schema: Schema, opts: GenerateTypesOptions): string[] { if (!SCHEMA_KEYS.includes(key)) { buff.push('', `@${key} ${schema[key]}`) } + if (key === '$constraints') { + buff.push(...Object.entries(schema[key]).map(([pkg, version]) => `@requires ${pkg}@${version}`)) + } } if (Array.isArray(schema.tags)) { diff --git a/src/loader/babel.ts b/src/loader/babel.ts index 56236b7..e8e2422 100644 --- a/src/loader/babel.ts +++ b/src/loader/babel.ts @@ -196,6 +196,14 @@ function parseJSDocs (input: string | string[]): Schema { return typedefs }, {} as Record) for (const tag of tags) { + if (tag.startsWith('@requires')) { + const { pkg, version } = tag.match(/^@requires\s+(?([^\s/][^@\s/]+\/)*[^\s/][^@\s]+)(@(?.+))?$/)?.groups || {} + if (pkg) { + schema.$constraints = schema.$constraints || {} + schema.$constraints[pkg] = version || '*' + } + continue + } if (tag.startsWith('@type')) { const type = tag.match(/@type\s+\{([\S\s]+)\}/)?.[1] // Skip typedefs diff --git a/src/schema.ts b/src/schema.ts index 03abf96..0861bf5 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -3,12 +3,14 @@ import type { InputObject, InputValue, JSValue, Schema } from './types' interface _ResolveCtx { root: InputObject - defaults?: InputObject, + defaults?: InputObject + limiter?: (constraints: Record) => boolean resolveCache: Record } -export function resolveSchema (obj: InputObject, defaults?: InputObject) { +export function resolveSchema (obj: InputObject, defaults?: InputObject, resolveOptions?: Pick<_ResolveCtx, 'limiter'>) { const schema = _resolveSchema(obj, '', { + ...resolveOptions, root: obj, defaults, resolveCache: {} @@ -42,6 +44,10 @@ function _resolveSchema (input: InputValue, id: string, ctx: _ResolveCtx): Schem // Clone to avoid mutation const node = { ...input as any } as InputObject + if (ctx.limiter && '$constraints' in node && !ctx.limiter(node.$constraints)) { + return undefined + } + const schema: Schema = ctx.resolveCache[id] = { ...node.$schema, id: '#' + id.replace(/\./g, '/') @@ -50,12 +56,15 @@ function _resolveSchema (input: InputValue, id: string, ctx: _ResolveCtx): Schem // Resolve children for (const key in node) { // Ignore special keys - if (key === '$resolve' || key === '$schema' || key === '$default') { + if (key === '$resolve' || key === '$schema' || key === '$default' || key === '$constraints') { continue } schema.properties = schema.properties || {} if (!schema.properties[key]) { - schema.properties[key] = _resolveSchema(node[key], joinPath(id, key), ctx) + const value = _resolveSchema(node[key], joinPath(id, key), ctx) + if (value !== undefined) { + schema.properties[key] = _resolveSchema(node[key], joinPath(id, key), ctx) + } } } diff --git a/src/types.ts b/src/types.ts index 85dcdc5..1fa824f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -48,6 +48,7 @@ export interface Schema extends TypeDescriptor { title?: string description?: string $schema?: string + $constraints?: Record tags?: string[] args?: FunctionArg[] returns?: TypeDescriptor, diff --git a/test/schema.test.ts b/test/schema.test.ts index 4b82982..0a08c8f 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -121,6 +121,46 @@ describe('resolveSchema', () => { }) }) + it('with constraints', () => { + const schema = resolveSchema({ + foo: { + $default: '123', + $resolve: val => parseInt(val), + $constraints: { test: 'bar' } + }, + bar: { + $default: '123', + $resolve: val => parseInt(val), + $constraints: { test: 'baz' } + }, + qux: { + $default: '123', + $resolve: val => parseInt(val) + } + }, undefined, { limiter: constraints => constraints.test === 'bar' }) + + expect(schema).toMatchObject({ + default: { + foo: 123, + qux: 123 + }, + id: '#', + properties: { + foo: { + default: 123, + id: '#foo', + type: 'number' + }, + qux: { + default: 123, + id: '#qux', + type: 'number' + } + }, + type: 'object' + }) + }) + it('array', () => { const schema = resolveSchema({ empty: [], diff --git a/test/transform.test.ts b/test/transform.test.ts index d6a37fc..613f278 100644 --- a/test/transform.test.ts +++ b/test/transform.test.ts @@ -159,6 +159,8 @@ describe('transform (jsdoc)', () => { * \`\`\` * * @see https://nuxtjs.org + * @requires nuxt@1.2.3 + * @requires pkg2 */ srcDir: 'src' } @@ -173,7 +175,11 @@ describe('transform (jsdoc)', () => { '@note This is a note.\nthat is on two lines', '@example\n```js\nexport default secretNumber = 42\n```', '@see https://nuxtjs.org' - ] + ], + $constraints: { + nuxt: '1.2.3', + pkg2: '*' + } } } }) diff --git a/test/types.test.ts b/test/types.test.ts index 2dc8860..0ba0a83 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -9,7 +9,10 @@ describe('resolveSchema', () => { $default: 'test value', $schema: { title: 'Test', - description: 'this is test' + description: 'this is test', + $constraints: { + nuxt: '^1' + } } } } @@ -21,6 +24,7 @@ export interface Untyped { * Test * this is test * @default "test value" + * @requires nuxt@^1 */ foo: string, },