@znight/graphql 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1063 -0
- package/dist/compiler.d.mts +93 -0
- package/dist/compiler.mjs +2832 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/index.d.mts +542 -0
- package/dist/index.mjs +128 -0
- package/dist/index.mjs.map +1 -0
- package/dist/names-BlzDM5cF.mjs +111 -0
- package/dist/names-BlzDM5cF.mjs.map +1 -0
- package/dist/plugin-BdBxr5NQ.mjs +3222 -0
- package/dist/plugin-BdBxr5NQ.mjs.map +1 -0
- package/dist/testing.d.mts +101 -0
- package/dist/testing.mjs +306 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-CgbV9LjD.d.mts +234 -0
- package/package.json +97 -1
- package/src/compiler/analyze.ts +608 -0
- package/src/compiler/builder.ts +1640 -0
- package/src/compiler/constants.ts +74 -0
- package/src/compiler/data.ts +73 -0
- package/src/compiler/defaults.ts +92 -0
- package/src/compiler/discriminators.ts +53 -0
- package/src/compiler/docs.ts +55 -0
- package/src/compiler/generate.ts +405 -0
- package/src/compiler/ids.ts +128 -0
- package/src/compiler/index.ts +93 -0
- package/src/compiler/locate.ts +44 -0
- package/src/compiler/model.ts +122 -0
- package/src/compiler/options.ts +54 -0
- package/src/compiler/scalars.ts +138 -0
- package/src/compiler/sdl-view.ts +59 -0
- package/src/context.ts +181 -0
- package/src/decisions.ts +169 -0
- package/src/decorators.ts +196 -0
- package/src/errors.ts +89 -0
- package/src/execution.ts +1064 -0
- package/src/format.ts +285 -0
- package/src/index.ts +84 -0
- package/src/limits.ts +177 -0
- package/src/loaders.ts +149 -0
- package/src/memory-pubsub.ts +133 -0
- package/src/metadata.ts +195 -0
- package/src/names.ts +47 -0
- package/src/options.ts +309 -0
- package/src/pipeline.ts +277 -0
- package/src/plugin.ts +557 -0
- package/src/pubsub.ts +36 -0
- package/src/scalars.ts +131 -0
- package/src/schema-hash.ts +18 -0
- package/src/schema.ts +557 -0
- package/src/sdl.ts +515 -0
- package/src/testing.ts +454 -0
- package/src/tokens.ts +22 -0
- package/src/transport/abort.ts +80 -0
- package/src/transport/accept.ts +155 -0
- package/src/transport/graphiql.ts +106 -0
- package/src/transport/http.ts +373 -0
- package/src/transport/sse.ts +45 -0
- package/src/type-ref.ts +89 -0
- package/src/types.ts +190 -0
- package/src/validate-args.ts +48 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants of the compiler extension modules: diagnostic codes, shape options, ordering. This
|
|
3
|
+
* module must stay free of runtime imports (the compiler loads the extension entry, which must
|
|
4
|
+
* never load the GraphQL runtime, graphql-js nor dataloader).
|
|
5
|
+
*/
|
|
6
|
+
import type { ErrorCode } from '@znight/core'
|
|
7
|
+
|
|
8
|
+
import { GraphQLIds } from './ids.ts'
|
|
9
|
+
|
|
10
|
+
/** Diagnostic codes the extension reports (RFC 0005 §6.6 and the reused framework codes). */
|
|
11
|
+
export const Codes: {
|
|
12
|
+
/** ZNIGHT005: decorator target, arguments, method shape, look-alikes, `@Resolver` + `@Controller`. */
|
|
13
|
+
readonly InvalidDecorator: ErrorCode
|
|
14
|
+
/** ZNIGHT023: a root field without a decision of a required route decorator group (RFC 0001). */
|
|
15
|
+
readonly MissingRouteDecision: ErrorCode
|
|
16
|
+
/** ZNIGHT060: a TypeScript type with no GraphQL mapping, a missing annotation. */
|
|
17
|
+
readonly InvalidType: ErrorCode
|
|
18
|
+
/** ZNIGHT061: name conflicts, invalid or reserved names, field resolver type mismatches. */
|
|
19
|
+
readonly NameConflict: ErrorCode
|
|
20
|
+
/** ZNIGHT062: resolver structure (parent, subscriptions, no query, inheritance, libraries). */
|
|
21
|
+
readonly InvalidResolver: ErrorCode
|
|
22
|
+
/** ZNIGHT063: input position problems (dual use, getters, unions, cycles, defaults). */
|
|
23
|
+
readonly InvalidInput: ErrorCode
|
|
24
|
+
/** ZNIGHT064: union members that cannot be told apart at runtime. */
|
|
25
|
+
readonly UnresolvableAbstractType: ErrorCode
|
|
26
|
+
/** ZNIGHT065: `@Loader()` parameters and loader providers. */
|
|
27
|
+
readonly InvalidLoader: ErrorCode
|
|
28
|
+
} = Object.freeze({
|
|
29
|
+
InvalidDecorator: 'ZNIGHT005',
|
|
30
|
+
MissingRouteDecision: 'ZNIGHT023',
|
|
31
|
+
InvalidType: 'ZNIGHT060',
|
|
32
|
+
NameConflict: 'ZNIGHT061',
|
|
33
|
+
InvalidResolver: 'ZNIGHT062',
|
|
34
|
+
InvalidInput: 'ZNIGHT063',
|
|
35
|
+
UnresolvableAbstractType: 'ZNIGHT064',
|
|
36
|
+
InvalidLoader: 'ZNIGHT065',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/** Root reason given to `AnalyzeContext.addRoot()`. */
|
|
40
|
+
export const ROOT_REASON = 'GraphQL resolver'
|
|
41
|
+
|
|
42
|
+
/** Async iterables: the return type of `@Subscription` methods (§6.2). */
|
|
43
|
+
export const ASYNC_ITERABLES: ReadonlySet<string> = new Set([
|
|
44
|
+
'globalThis#AsyncIterable',
|
|
45
|
+
'globalThis#AsyncIterableIterator',
|
|
46
|
+
'globalThis#AsyncGenerator',
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
/** Iterables mapped to lists in output position (§6.2). */
|
|
50
|
+
export const OUTPUT_ITERABLES: ReadonlySet<string> = new Set([
|
|
51
|
+
'globalThis#Set',
|
|
52
|
+
'globalThis#ReadonlySet',
|
|
53
|
+
'globalThis#Iterable',
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Identities the shapes keep as references (`ShapeOptions.keep`), before the identities of the
|
|
58
|
+
* `scalar<T>()` declarations of the application.
|
|
59
|
+
*/
|
|
60
|
+
export const BASE_KEEP: readonly string[] = Object.freeze([
|
|
61
|
+
...ASYNC_ITERABLES,
|
|
62
|
+
...OUTPUT_ITERABLES,
|
|
63
|
+
GraphQLIds.ID,
|
|
64
|
+
GraphQLIds.Int,
|
|
65
|
+
GraphQLIds.Float,
|
|
66
|
+
GraphQLIds.JSONValue,
|
|
67
|
+
GraphQLIds.Loader,
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
/** Deterministic string comparison (code-unit order, never `localeCompare`). */
|
|
71
|
+
export function compareStrings(a: string, b: string): number {
|
|
72
|
+
if (a === b) return 0
|
|
73
|
+
return a < b ? -1 : 1
|
|
74
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data stored by the extension during analysis (`ctx.store()`), read back by `validate` and
|
|
3
|
+
* `generate`. Only what compiled libraries can encode: primitives, plain objects and arrays,
|
|
4
|
+
* `ClassInfo` (kept for its identity), `ValueRef`, `BindingHandle` and `Location`. Methods and
|
|
5
|
+
* parameters are looked up again by name and index (`ctx.classInfo(host.identity)`).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { BindingHandle, ClassInfo, ClassRef, Location, ValueRef } from '@znight/compiler'
|
|
9
|
+
import type { Scope } from '@znight/core'
|
|
10
|
+
import type { MetaEntry } from '@znight/http/compiler'
|
|
11
|
+
|
|
12
|
+
/** Kind of field a resolver method provides. */
|
|
13
|
+
export type FieldOperation = 'query' | 'mutation' | 'subscription' | 'field'
|
|
14
|
+
|
|
15
|
+
/** A `@Resolver` class. */
|
|
16
|
+
export interface ResolverData {
|
|
17
|
+
readonly kind: 'resolver'
|
|
18
|
+
/** The resolver class; its identity is the provider token. */
|
|
19
|
+
readonly host: ClassInfo
|
|
20
|
+
readonly scope: Scope
|
|
21
|
+
/** Location of `@Resolver`. */
|
|
22
|
+
readonly location: Location
|
|
23
|
+
/** Class `@UseGuard` guards. */
|
|
24
|
+
readonly guards: readonly ClassRef[]
|
|
25
|
+
/** One entry per field decorator of the class's methods, in source order. */
|
|
26
|
+
readonly fields: readonly FieldData[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One `@Query`, `@Mutation`, `@Subscription` or `@ResolveField` decorator. */
|
|
30
|
+
export interface FieldData {
|
|
31
|
+
/** Method name. */
|
|
32
|
+
readonly method: string
|
|
33
|
+
readonly operation: FieldOperation
|
|
34
|
+
/** GraphQL field name (decorator argument, or the method name). */
|
|
35
|
+
readonly name: string
|
|
36
|
+
/** Location of the field decorator. */
|
|
37
|
+
readonly location: Location
|
|
38
|
+
/** Location of the method name. */
|
|
39
|
+
readonly methodLocation: Location
|
|
40
|
+
/** Method `@UseGuard` guards (class guards are on {@link ResolverData.guards}). */
|
|
41
|
+
readonly guards: readonly ClassRef[]
|
|
42
|
+
/** Effective route metadata: the class entries a method decision does not replace, then the method's. */
|
|
43
|
+
readonly meta: readonly MetaEntry[]
|
|
44
|
+
/** One entry per method parameter, by index (`undefined`: the parameter could not be analysed). */
|
|
45
|
+
readonly params: readonly (ParamData | undefined)[]
|
|
46
|
+
/** `false` when a parameter or the method could not be analysed (an error was reported). */
|
|
47
|
+
readonly complete: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** What a method parameter receives (§7.3). */
|
|
51
|
+
export type ParamData =
|
|
52
|
+
| {
|
|
53
|
+
readonly kind: 'args'
|
|
54
|
+
/** `@Args('name')`: one argument; `undefined`: the properties of the type are the arguments. */
|
|
55
|
+
readonly name: string | undefined
|
|
56
|
+
/** `@Args(..., Schema)`: the Standard Schema validating the value. */
|
|
57
|
+
readonly schema: ValueRef | undefined
|
|
58
|
+
readonly location: Location
|
|
59
|
+
}
|
|
60
|
+
| { readonly kind: 'parent'; readonly location: Location }
|
|
61
|
+
| { readonly kind: 'ctx' }
|
|
62
|
+
| { readonly kind: 'info' }
|
|
63
|
+
| {
|
|
64
|
+
readonly kind: 'loader'
|
|
65
|
+
/** Binding of the loader provider (lazy edge from the resolver). */
|
|
66
|
+
readonly binding: BindingHandle
|
|
67
|
+
/** Identity of `L` in `Loader<L>`. */
|
|
68
|
+
readonly identity: string
|
|
69
|
+
readonly location: Location
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Everything the extension stores. */
|
|
73
|
+
export type GraphQLData = ResolverData
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default values of arguments and input fields (RFC 0005 §6.3): an internal value (a `@default`
|
|
3
|
+
* JSON value, a statically evaluated parameter default) checked against its GraphQL type and turned
|
|
4
|
+
* into the external value graphql-js expects (`default: { value }`, enum values by name).
|
|
5
|
+
*/
|
|
6
|
+
import type { GraphQLTypeRef } from '../type-ref.ts'
|
|
7
|
+
import type { TypeModel } from './model.ts'
|
|
8
|
+
|
|
9
|
+
const GRAPHQL_INT_MAX = 2_147_483_647
|
|
10
|
+
const GRAPHQL_INT_MIN = -2_147_483_648
|
|
11
|
+
|
|
12
|
+
/** The external value, or why the value does not fit the type. */
|
|
13
|
+
export type ExternalValue = { readonly value: unknown } | { readonly error: string }
|
|
14
|
+
|
|
15
|
+
/** `value` (an internal value) as a GraphQL external value of `ref`; `types` resolves enums and inputs. */
|
|
16
|
+
export function externalValue(
|
|
17
|
+
value: unknown,
|
|
18
|
+
ref: GraphQLTypeRef,
|
|
19
|
+
types: ReadonlyMap<string, TypeModel>,
|
|
20
|
+
): ExternalValue {
|
|
21
|
+
if (value === null) return ref.nonNull ? { error: 'null for a non-null type' } : { value: null }
|
|
22
|
+
if (value === undefined) return { error: 'undefined' }
|
|
23
|
+
if (ref.kind === 'list') {
|
|
24
|
+
if (!Array.isArray(value)) return externalValue(value, ref.of, types)
|
|
25
|
+
const items: unknown[] = []
|
|
26
|
+
for (const item of value) {
|
|
27
|
+
const converted = externalValue(item, ref.of, types)
|
|
28
|
+
if ('error' in converted) return converted
|
|
29
|
+
items.push(converted.value)
|
|
30
|
+
}
|
|
31
|
+
return { value: items }
|
|
32
|
+
}
|
|
33
|
+
switch (ref.name) {
|
|
34
|
+
case 'String':
|
|
35
|
+
return typeof value === 'string' ? { value } : { error: 'expected a string' }
|
|
36
|
+
case 'ID':
|
|
37
|
+
return typeof value === 'string' || (typeof value === 'number' && Number.isInteger(value))
|
|
38
|
+
? { value }
|
|
39
|
+
: { error: 'expected a string or an integer' }
|
|
40
|
+
case 'Int':
|
|
41
|
+
return typeof value === 'number' &&
|
|
42
|
+
Number.isInteger(value) &&
|
|
43
|
+
value <= GRAPHQL_INT_MAX &&
|
|
44
|
+
value >= GRAPHQL_INT_MIN
|
|
45
|
+
? { value }
|
|
46
|
+
: { error: 'expected a 32-bit integer' }
|
|
47
|
+
case 'Float':
|
|
48
|
+
return typeof value === 'number' && Number.isFinite(value) ? { value } : { error: 'expected a finite number' }
|
|
49
|
+
case 'Boolean':
|
|
50
|
+
return typeof value === 'boolean' ? { value } : { error: 'expected a boolean' }
|
|
51
|
+
case 'JSON':
|
|
52
|
+
return isJson(value) ? { value } : { error: 'expected a JSON value' }
|
|
53
|
+
}
|
|
54
|
+
const type = types.get(ref.name)
|
|
55
|
+
if (type?.kind === 'enum') {
|
|
56
|
+
const found =
|
|
57
|
+
type.values.find(entry => entry.value === value) ??
|
|
58
|
+
(typeof value === 'string' ? type.values.find(entry => entry.name === value) : undefined)
|
|
59
|
+
return found === undefined
|
|
60
|
+
? { error: `expected one of ${type.values.map(entry => entry.name).join(', ')}` }
|
|
61
|
+
: { value: found.name }
|
|
62
|
+
}
|
|
63
|
+
if (type?.kind === 'input') {
|
|
64
|
+
if (typeof value !== 'object' || Array.isArray(value)) return { error: `expected an object of type ${type.name}` }
|
|
65
|
+
const record = value as Readonly<Record<string, unknown>>
|
|
66
|
+
for (const key of Object.keys(record)) {
|
|
67
|
+
if (!type.fields.some(field => field.name === key)) return { error: `${type.name} has no field '${key}'` }
|
|
68
|
+
}
|
|
69
|
+
const result: Record<string, unknown> = {}
|
|
70
|
+
for (const field of type.fields) {
|
|
71
|
+
if (!Object.hasOwn(record, field.name) || record[field.name] === undefined) {
|
|
72
|
+
if (field.ref.nonNull && field.defaultValue === undefined)
|
|
73
|
+
return { error: `${type.name}.${field.name} is required` }
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
const converted = externalValue(record[field.name], field.ref, types)
|
|
77
|
+
if ('error' in converted) return { error: `${type.name}.${field.name}: ${converted.error}` }
|
|
78
|
+
result[field.name] = converted.value
|
|
79
|
+
}
|
|
80
|
+
return { value: result }
|
|
81
|
+
}
|
|
82
|
+
return { error: `defaults of the scalar ${ref.name} are not supported` }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A JSON-compatible value (the `JSON` scalar). */
|
|
86
|
+
function isJson(value: unknown): boolean {
|
|
87
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return true
|
|
88
|
+
if (typeof value === 'number') return Number.isFinite(value)
|
|
89
|
+
if (Array.isArray(value)) return value.every(isJson)
|
|
90
|
+
if (typeof value !== 'object') return false
|
|
91
|
+
return Object.values(value).every(isJson)
|
|
92
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Union discriminators (RFC 0005 §5.7): how the runtime tells the members of a union apart, decided
|
|
3
|
+
* at build time so that `resolveType` is a lookup.
|
|
4
|
+
*/
|
|
5
|
+
import type { ClassInfo } from '@znight/compiler'
|
|
6
|
+
import type { DiscriminatorModel, ObjectModel } from './model.ts'
|
|
7
|
+
|
|
8
|
+
import { compareStrings } from './constants.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `instanceof` when every member is an exported class (subclasses first), `__typename` when every
|
|
12
|
+
* member declares it, else the first property of the first member whose literal value differs in
|
|
13
|
+
* every member; `undefined` when none applies (ZNIGHT064).
|
|
14
|
+
*/
|
|
15
|
+
export function discriminatorOf(
|
|
16
|
+
members: readonly ObjectModel[],
|
|
17
|
+
classInfo: (identity: string) => ClassInfo | undefined,
|
|
18
|
+
): DiscriminatorModel | undefined {
|
|
19
|
+
if (members.every(member => member.classIdentity !== undefined && classInfo(member.classIdentity)?.exported)) {
|
|
20
|
+
const depth = (identity: string): number => classInfo(identity)?.heritage.length ?? 0
|
|
21
|
+
return {
|
|
22
|
+
by: 'class',
|
|
23
|
+
classes: members
|
|
24
|
+
.map(member => ({ identity: member.classIdentity as string, name: member.name }))
|
|
25
|
+
// subclasses first: `instanceof` of a base class also matches its subclasses
|
|
26
|
+
.sort((a, b) => depth(b.identity) - depth(a.identity) || compareStrings(a.name, b.name)),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (members.every(member => member.properties.some(property => property.name === '__typename'))) {
|
|
30
|
+
return { by: 'typename' }
|
|
31
|
+
}
|
|
32
|
+
for (const candidate of members[0]?.properties ?? []) {
|
|
33
|
+
if (candidate.optional || candidate.literal === undefined) continue
|
|
34
|
+
const values = literalValues(members, candidate.name)
|
|
35
|
+
if (values !== undefined) return { by: 'property', property: candidate.name, values }
|
|
36
|
+
}
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Literal value → member name when `property` is a required literal with a distinct value in every member. */
|
|
41
|
+
function literalValues(members: readonly ObjectModel[], property: string): Record<string, string> | undefined {
|
|
42
|
+
const values = new Map<string, string>()
|
|
43
|
+
for (const member of members) {
|
|
44
|
+
const found = member.properties.find(entry => entry.name === property)
|
|
45
|
+
if (found === undefined || found.optional || found.literal === undefined) return undefined
|
|
46
|
+
const key = String(found.literal)
|
|
47
|
+
if (values.has(key)) return undefined
|
|
48
|
+
values.set(key, member.name)
|
|
49
|
+
}
|
|
50
|
+
const sorted: Record<string, string> = {}
|
|
51
|
+
for (const key of [...values.keys()].sort(compareStrings)) sorted[key] = values.get(key) as string
|
|
52
|
+
return sorted
|
|
53
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSDoc vocabulary of the extension (RFC 0005 §5.14): descriptions, `@param`, `@deprecated`,
|
|
3
|
+
* `@default`, `@gqlHidden` and `@gqlName`.
|
|
4
|
+
*/
|
|
5
|
+
import type { DocComment, DocTag } from '@znight/compiler'
|
|
6
|
+
|
|
7
|
+
import { DEFAULT_DEPRECATION_REASON } from '../names.ts'
|
|
8
|
+
|
|
9
|
+
/** The comment text (before the first tag), `undefined` when empty. */
|
|
10
|
+
export function descriptionOf(doc: DocComment | undefined): string | undefined {
|
|
11
|
+
return doc === undefined || doc.text === '' ? undefined : doc.text
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The first tag named `name`. */
|
|
15
|
+
export function tagOf(doc: DocComment | undefined, name: string): DocTag | undefined {
|
|
16
|
+
return doc?.tags.find(tag => tag.name === name)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `@deprecated reason`: the reason (graphql-js' default one without text), `undefined` without the tag. */
|
|
20
|
+
export function deprecationOf(doc: DocComment | undefined): string | undefined {
|
|
21
|
+
const tag = tagOf(doc, 'deprecated')
|
|
22
|
+
if (tag === undefined) return undefined
|
|
23
|
+
const reason = tag.text.trim()
|
|
24
|
+
return reason === '' ? DEFAULT_DEPRECATION_REASON : reason
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `true` with a `@gqlHidden` tag. */
|
|
28
|
+
export function isHidden(doc: DocComment | undefined): boolean {
|
|
29
|
+
return tagOf(doc, 'gqlHidden') !== undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The text of `@param name text` (also `@param {Type} name - text`), `undefined` when there is no
|
|
34
|
+
* such tag or it has no text.
|
|
35
|
+
*/
|
|
36
|
+
export function paramDescription(doc: DocComment | undefined, name: string): string | undefined {
|
|
37
|
+
for (const tag of doc?.tags ?? []) {
|
|
38
|
+
if (tag.name !== 'param') continue
|
|
39
|
+
const match = /^(?:\{[^}]*\}\s*)?\[?([\w$]+)(?:=[^\]]*)?\]?(?:\s+-)?(?:\s+([\s\S]*))?$/.exec(tag.text.trim())
|
|
40
|
+
if (match?.[1] !== name) continue
|
|
41
|
+
const text = match[2]?.trim()
|
|
42
|
+
return text === undefined || text === '' ? undefined : text
|
|
43
|
+
}
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A `@default` value: JSON when it parses, the text otherwise (`@default title`). */
|
|
48
|
+
export function defaultTagValue(tag: DocTag): unknown {
|
|
49
|
+
const text = tag.text.trim()
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(text) as unknown
|
|
52
|
+
} catch {
|
|
53
|
+
return text
|
|
54
|
+
}
|
|
55
|
+
}
|