@likec4/core 1.2.0 → 1.2.1
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/dist/colors/element.d.ts +1 -0
- package/dist/colors/index.d.ts +1 -0
- package/dist/colors/relationships.d.ts +1 -0
- package/dist/errors/errors.spec.d.ts +2 -0
- package/dist/errors/errors.spec.js +69 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +7 -1
- package/dist/index.d.ts +1 -0
- package/dist/types/_common.d.ts +1 -0
- package/dist/types/element.d.ts +1 -0
- package/dist/types/expression.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +2 -1
- package/dist/types/model.d.ts +1 -0
- package/dist/types/opaque.d.ts +1 -0
- package/dist/types/relation.d.ts +1 -0
- package/dist/types/theme.d.ts +1 -0
- package/dist/types/view.d.ts +1 -0
- package/dist/utils/fqn.d.ts +1 -0
- package/dist/utils/fqn.spec.d.ts +2 -0
- package/dist/utils/fqn.spec.js +82 -0
- package/dist/utils/guards.d.ts +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/promises.d.ts +1 -0
- package/dist/utils/relations.d.ts +1 -0
- package/dist/utils/relations.spec.d.ts +2 -0
- package/dist/utils/relations.spec.js +210 -0
- package/package.json +6 -6
- package/src/colors/element.ts +80 -0
- package/src/colors/index.ts +12 -0
- package/src/colors/relationships.ts +54 -0
- package/src/errors/errors.spec.ts +88 -0
- package/src/errors/index.ts +120 -0
- package/src/index.ts +9 -0
- package/src/reset.d.ts +2 -0
- package/src/types/_common.ts +5 -0
- package/src/types/element.ts +56 -0
- package/src/types/expression.ts +140 -0
- package/src/types/index.ts +10 -0
- package/src/types/model.ts +15 -0
- package/src/types/opaque.ts +108 -0
- package/src/types/relation.ts +38 -0
- package/src/types/theme.ts +46 -0
- package/src/types/view.ts +263 -0
- package/src/utils/fqn.spec.ts +105 -0
- package/src/utils/fqn.ts +112 -0
- package/src/utils/guards.ts +10 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/promises.ts +9 -0
- package/src/utils/relations.spec.ts +268 -0
- package/src/utils/relations.ts +104 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RelationshipThemeColors, RelationshipThemeColorValues } from '../types/theme'
|
|
2
|
+
|
|
3
|
+
const gray = {
|
|
4
|
+
lineColor: '#6E6E6E',
|
|
5
|
+
labelBgColor: '#18191b',
|
|
6
|
+
labelColor: '#C6C6C6'
|
|
7
|
+
} satisfies RelationshipThemeColorValues
|
|
8
|
+
const slate = {
|
|
9
|
+
lineColor: '#64748b', // 500
|
|
10
|
+
labelBgColor: '#0f172a', // 900
|
|
11
|
+
labelColor: '#cbd5e1' // 300
|
|
12
|
+
} satisfies RelationshipThemeColorValues
|
|
13
|
+
|
|
14
|
+
const blue = {
|
|
15
|
+
lineColor: '#3b82f6', // 500
|
|
16
|
+
labelBgColor: '#172554', // 950
|
|
17
|
+
labelColor: '#60a5fa' // 400
|
|
18
|
+
} satisfies RelationshipThemeColorValues
|
|
19
|
+
|
|
20
|
+
const sky = {
|
|
21
|
+
lineColor: '#0ea5e9', // 500
|
|
22
|
+
labelBgColor: '#082f49', // 950
|
|
23
|
+
labelColor: '#38bdf8' // 400
|
|
24
|
+
} satisfies RelationshipThemeColorValues
|
|
25
|
+
|
|
26
|
+
export const RelationshipColors = {
|
|
27
|
+
amber: {
|
|
28
|
+
lineColor: '#b45309',
|
|
29
|
+
labelBgColor: '#78350f',
|
|
30
|
+
labelColor: '#FFE0C2'
|
|
31
|
+
},
|
|
32
|
+
blue,
|
|
33
|
+
gray,
|
|
34
|
+
green: {
|
|
35
|
+
lineColor: '#15803d', // 700
|
|
36
|
+
labelBgColor: '#052e16', // 950
|
|
37
|
+
labelColor: '#22c55e' // 500
|
|
38
|
+
},
|
|
39
|
+
indigo: {
|
|
40
|
+
lineColor: '#6366f1', // 500
|
|
41
|
+
labelBgColor: '#1e1b4b', // 950
|
|
42
|
+
labelColor: '#818cf8' // 400
|
|
43
|
+
},
|
|
44
|
+
muted: slate,
|
|
45
|
+
primary: blue,
|
|
46
|
+
red: {
|
|
47
|
+
lineColor: '#AC4D39',
|
|
48
|
+
labelBgColor: '#b91c1c',
|
|
49
|
+
labelColor: '#FF977D'
|
|
50
|
+
},
|
|
51
|
+
secondary: sky,
|
|
52
|
+
sky,
|
|
53
|
+
slate
|
|
54
|
+
} satisfies RelationshipThemeColors
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
BaseError,
|
|
4
|
+
InvalidArgError,
|
|
5
|
+
InvalidModelError,
|
|
6
|
+
invariant,
|
|
7
|
+
InvariantError,
|
|
8
|
+
NonExhaustiveError,
|
|
9
|
+
normalizeError,
|
|
10
|
+
NullableError,
|
|
11
|
+
RelationRefError,
|
|
12
|
+
UnknownError
|
|
13
|
+
} from './index'
|
|
14
|
+
|
|
15
|
+
describe('errors', () => {
|
|
16
|
+
describe('normalizeError', () => {
|
|
17
|
+
it.each([
|
|
18
|
+
{ error: new BaseError('BaseError') },
|
|
19
|
+
{ error: new RelationRefError('RelationRefError') },
|
|
20
|
+
{ error: new UnknownError('UnknownError') },
|
|
21
|
+
{ error: new InvariantError('InvariantError') },
|
|
22
|
+
{ error: new NonExhaustiveError('NonExhaustiveError') },
|
|
23
|
+
{ error: new InvalidModelError('InvalidModelError') },
|
|
24
|
+
{ error: new NullableError('NullableError') },
|
|
25
|
+
{ error: new InvalidArgError('InvalidArgError') }
|
|
26
|
+
])('should return $error.name as-is', ({ error }) => {
|
|
27
|
+
expect(normalizeError(error)).toBe(error)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it.skip('should wrap an error', () => {
|
|
31
|
+
const cause = new Error('original test')
|
|
32
|
+
const error = normalizeError(cause)
|
|
33
|
+
expect(error).toBeInstanceOf(UnknownError)
|
|
34
|
+
expect(error).to.include({
|
|
35
|
+
name: 'UnknownError',
|
|
36
|
+
message: 'original test'
|
|
37
|
+
})
|
|
38
|
+
expect(error).to.have.property('cause').that.eq(cause)
|
|
39
|
+
expect(error)
|
|
40
|
+
.to.have.property('stack')
|
|
41
|
+
.that.is.a('string')
|
|
42
|
+
.and.includes('UnknownError: original test')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should wrap a string', () => {
|
|
46
|
+
const cause = 'error string'
|
|
47
|
+
const error = normalizeError(cause)
|
|
48
|
+
expect(error).toBeInstanceOf(UnknownError)
|
|
49
|
+
expect(error).to.include({
|
|
50
|
+
name: 'UnknownError',
|
|
51
|
+
message: cause
|
|
52
|
+
})
|
|
53
|
+
expect(error).not.to.have.property('cause')
|
|
54
|
+
expect(error).to.have.property('stack').that.is.a('string')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Fails on serializiation of AST
|
|
58
|
+
it.skip('should wrap an object', () => {
|
|
59
|
+
const cause = { foo: 'bar' }
|
|
60
|
+
const error = normalizeError(cause)
|
|
61
|
+
expect(error).toBeInstanceOf(UnknownError)
|
|
62
|
+
expect(error).to.include({
|
|
63
|
+
name: 'UnknownError',
|
|
64
|
+
message: '{"foo":"bar"}'
|
|
65
|
+
})
|
|
66
|
+
expect(error).not.to.have.property('cause')
|
|
67
|
+
expect(error).to.have.property('stack').that.is.a('string')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('invariant', () => {
|
|
73
|
+
it('should throw an error if the condition fails', () => {
|
|
74
|
+
expect(() => invariant(false)).toThrow('Invariant failed')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should throw an error if the condition is null', () => {
|
|
78
|
+
expect(() => invariant(null)).toThrow(InvariantError)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should not throw an error if the condition passes', () => {
|
|
82
|
+
expect(() => invariant(true)).not.toThrow()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should throw an error with the provided message if the condition fails', () => {
|
|
86
|
+
expect(() => invariant(false, 'test')).toThrow('test')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { CustomError } from 'ts-custom-error'
|
|
2
|
+
import { isString } from '../utils/guards'
|
|
3
|
+
|
|
4
|
+
export interface BaseErrorOptions {
|
|
5
|
+
cause?: unknown
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base class for all errors in the LikeC4 library.
|
|
10
|
+
*/
|
|
11
|
+
export class BaseError extends CustomError {
|
|
12
|
+
constructor(message: string, options?: BaseErrorOptions) {
|
|
13
|
+
super(message, options)
|
|
14
|
+
// Set name explicitly as minification can mangle class names
|
|
15
|
+
Object.defineProperty(this, 'name', { value: 'BaseError' })
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class InvalidArgError extends BaseError {
|
|
20
|
+
constructor(message: string, options?: BaseErrorOptions) {
|
|
21
|
+
super(message, options)
|
|
22
|
+
// Set name explicitly as minification can mangle class names
|
|
23
|
+
Object.defineProperty(this, 'name', { value: 'InvalidArgError' })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Unknown error, mosly probably a bug, unhandled case or coming from a third-party library.
|
|
29
|
+
*/
|
|
30
|
+
export class UnknownError extends BaseError {
|
|
31
|
+
constructor(message: string, options?: BaseErrorOptions) {
|
|
32
|
+
super(message, options)
|
|
33
|
+
// Set name explicitly as minification can mangle class names
|
|
34
|
+
Object.defineProperty(this, 'name', { value: 'UnknownError' })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class NullableError extends BaseError {
|
|
39
|
+
public constructor(message: string, options?: BaseErrorOptions) {
|
|
40
|
+
super(message, options)
|
|
41
|
+
// Set name explicitly as minification can mangle class names
|
|
42
|
+
Object.defineProperty(this, 'name', { value: 'NullableError' })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class InvariantError extends BaseError {
|
|
47
|
+
public constructor(message: string, options?: BaseErrorOptions) {
|
|
48
|
+
super(message, options)
|
|
49
|
+
// Set name explicitly as minification can mangle class names
|
|
50
|
+
Object.defineProperty(this, 'name', { value: 'InvariantError' })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Ensure that the value is NonNullable
|
|
55
|
+
// Mostly as safer `value!`
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
57
|
+
export function nonNullable<T>(value: T, message?: string): NonNullable<T> {
|
|
58
|
+
if (typeof value === 'undefined' || value == null) {
|
|
59
|
+
throw new NullableError(message ?? `Expected defined value, but received ${value}`)
|
|
60
|
+
}
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class RelationRefError extends BaseError {
|
|
65
|
+
public constructor(message: string, options?: BaseErrorOptions) {
|
|
66
|
+
super(message, options)
|
|
67
|
+
// Set name explicitly as minification can mangle class names
|
|
68
|
+
Object.defineProperty(this, 'name', { value: 'RelationRefError' })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function normalizeError(e: unknown): Error {
|
|
73
|
+
if (e instanceof BaseError || e instanceof Error) {
|
|
74
|
+
return e
|
|
75
|
+
}
|
|
76
|
+
const message = isString(e) ? e : String(e)
|
|
77
|
+
const error = new UnknownError(message)
|
|
78
|
+
try {
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
Error.captureStackTrace(error, normalizeError)
|
|
81
|
+
} catch {
|
|
82
|
+
// Ignore
|
|
83
|
+
}
|
|
84
|
+
return error
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Throw an error if the condition fails
|
|
88
|
+
// > Not providing an inline default argument for message as the result is smaller
|
|
89
|
+
export function invariant(
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
+
condition: any,
|
|
92
|
+
// Can provide a string, or a function that returns a string for cases where
|
|
93
|
+
// the message takes a fair amount of effort to compute
|
|
94
|
+
message?: string
|
|
95
|
+
): asserts condition {
|
|
96
|
+
if (condition) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
throw new InvariantError(message ?? 'Invariant failed')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class NonExhaustiveError extends BaseError {
|
|
103
|
+
public constructor(message: string, options?: BaseErrorOptions) {
|
|
104
|
+
super(message, options)
|
|
105
|
+
// Set name explicitly as minification can mangle class names
|
|
106
|
+
Object.defineProperty(this, 'name', { value: 'NonExhaustiveError' })
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function nonexhaustive(value: never): never {
|
|
111
|
+
throw new NonExhaustiveError(`NonExhaustive value: ${value}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class InvalidModelError extends BaseError {
|
|
115
|
+
public constructor(message: string, options?: BaseErrorOptions) {
|
|
116
|
+
super(message, options)
|
|
117
|
+
// Set name explicitly as minification can mangle class names
|
|
118
|
+
Object.defineProperty(this, 'name', { value: 'InvalidModelError' })
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/index.ts
ADDED
package/src/reset.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { IconUrl, NonEmptyArray } from './_common'
|
|
2
|
+
import type { Opaque } from './opaque'
|
|
3
|
+
import type { ThemeColor } from './theme'
|
|
4
|
+
|
|
5
|
+
// Full-qualified-name
|
|
6
|
+
export type Fqn = Opaque<string, 'Fqn'>
|
|
7
|
+
|
|
8
|
+
export function AsFqn(name: string, parent?: Fqn | null) {
|
|
9
|
+
return (parent ? parent + '.' + name : name) as Fqn
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const BorderStyles = ['solid', 'dashed', 'dotted', 'none'] as const
|
|
13
|
+
|
|
14
|
+
export type BorderStyle = typeof BorderStyles[number]
|
|
15
|
+
|
|
16
|
+
export type ElementKind = Opaque<string, 'ElementKind'>
|
|
17
|
+
export const ElementShapes = [
|
|
18
|
+
'rectangle',
|
|
19
|
+
'person',
|
|
20
|
+
'browser',
|
|
21
|
+
'mobile',
|
|
22
|
+
'cylinder',
|
|
23
|
+
'storage',
|
|
24
|
+
'queue'
|
|
25
|
+
] as const
|
|
26
|
+
|
|
27
|
+
export type ElementShape = typeof ElementShapes[number]
|
|
28
|
+
export const DefaultThemeColor = 'primary' satisfies ThemeColor
|
|
29
|
+
export const DefaultElementShape = 'rectangle' satisfies ElementShape
|
|
30
|
+
|
|
31
|
+
export interface ElementStyle {
|
|
32
|
+
border?: BorderStyle
|
|
33
|
+
// 0-100
|
|
34
|
+
opacity?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type Tag = Opaque<string, 'Tag'>
|
|
38
|
+
|
|
39
|
+
export interface TagSpec {
|
|
40
|
+
readonly id: Tag
|
|
41
|
+
readonly style: ElementStyle
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Element {
|
|
45
|
+
readonly id: Fqn
|
|
46
|
+
readonly kind: ElementKind
|
|
47
|
+
readonly title: string
|
|
48
|
+
readonly description: string | null
|
|
49
|
+
readonly technology: string | null
|
|
50
|
+
readonly tags: NonEmptyArray<Tag> | null
|
|
51
|
+
readonly links: NonEmptyArray<string> | null
|
|
52
|
+
readonly icon?: IconUrl
|
|
53
|
+
readonly shape?: ElementShape
|
|
54
|
+
readonly color?: ThemeColor
|
|
55
|
+
readonly style?: ElementStyle
|
|
56
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { IconUrl } from './_common'
|
|
2
|
+
import type { BorderStyle, ElementKind, ElementShape, Fqn, Tag } from './element'
|
|
3
|
+
import type { ThemeColor } from './theme'
|
|
4
|
+
import type { ViewID } from './view'
|
|
5
|
+
|
|
6
|
+
interface BaseExpr {
|
|
7
|
+
element?: never
|
|
8
|
+
custom?: never
|
|
9
|
+
expanded?: never
|
|
10
|
+
elementKind?: never
|
|
11
|
+
elementTag?: never
|
|
12
|
+
isEqual?: never
|
|
13
|
+
isDescedants?: never
|
|
14
|
+
wildcard?: never
|
|
15
|
+
source?: never
|
|
16
|
+
target?: never
|
|
17
|
+
inout?: never
|
|
18
|
+
incoming?: never
|
|
19
|
+
outgoing?: never
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ElementRefExpr extends Omit<BaseExpr, 'element' | 'isDescedants'> {
|
|
23
|
+
element: Fqn
|
|
24
|
+
isDescedants?: boolean
|
|
25
|
+
}
|
|
26
|
+
export function isElementRef(expr: Expression): expr is ElementRefExpr {
|
|
27
|
+
return 'element' in expr
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ExpandedElementExpr extends Omit<BaseExpr, 'expanded'> {
|
|
31
|
+
expanded: Fqn
|
|
32
|
+
}
|
|
33
|
+
export function isExpandedElementExpr(expr: Expression): expr is ExpandedElementExpr {
|
|
34
|
+
return 'expanded' in expr
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CustomElementExpr extends Omit<BaseExpr, 'custom'> {
|
|
38
|
+
custom: {
|
|
39
|
+
element: Fqn
|
|
40
|
+
title?: string
|
|
41
|
+
description?: string
|
|
42
|
+
technology?: string
|
|
43
|
+
shape?: ElementShape
|
|
44
|
+
color?: ThemeColor
|
|
45
|
+
icon?: IconUrl
|
|
46
|
+
border?: BorderStyle
|
|
47
|
+
opacity?: number
|
|
48
|
+
navigateTo?: ViewID
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isCustomElement(expr: Expression): expr is CustomElementExpr {
|
|
53
|
+
return 'custom' in expr
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface WildcardExpr extends Omit<BaseExpr, 'wildcard'> {
|
|
57
|
+
wildcard: true
|
|
58
|
+
}
|
|
59
|
+
export function isWildcard(expr: Expression): expr is WildcardExpr {
|
|
60
|
+
return 'wildcard' in expr
|
|
61
|
+
}
|
|
62
|
+
export interface ElementKindExpr extends Omit<BaseExpr, 'elementKind' | 'isEqual'> {
|
|
63
|
+
elementKind: ElementKind
|
|
64
|
+
isEqual: boolean
|
|
65
|
+
}
|
|
66
|
+
export function isElementKindExpr(expr: Expression): expr is ElementKindExpr {
|
|
67
|
+
return 'elementKind' in expr && 'isEqual' in expr
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ElementTagExpr extends Omit<BaseExpr, 'elementTag' | 'isEqual'> {
|
|
71
|
+
elementTag: Tag
|
|
72
|
+
isEqual: boolean
|
|
73
|
+
}
|
|
74
|
+
export function isElementTagExpr(expr: Expression): expr is ElementTagExpr {
|
|
75
|
+
return 'elementTag' in expr && 'isEqual' in expr
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type ElementExpression =
|
|
79
|
+
| ElementRefExpr
|
|
80
|
+
| WildcardExpr
|
|
81
|
+
| ElementKindExpr
|
|
82
|
+
| ElementTagExpr
|
|
83
|
+
| ExpandedElementExpr
|
|
84
|
+
export function isElement(expr: Expression): expr is ElementExpression {
|
|
85
|
+
return isElementRef(expr)
|
|
86
|
+
|| isWildcard(expr)
|
|
87
|
+
|| isElementKindExpr(expr)
|
|
88
|
+
|| isElementTagExpr(expr)
|
|
89
|
+
|| isExpandedElementExpr(expr)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface RelationExpr extends Omit<BaseExpr, 'source' | 'target'> {
|
|
93
|
+
source: ElementExpression
|
|
94
|
+
target: ElementExpression
|
|
95
|
+
isBidirectional?: boolean
|
|
96
|
+
}
|
|
97
|
+
export function isRelation(expr: Expression): expr is RelationExpr {
|
|
98
|
+
return 'source' in expr && 'target' in expr
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface InOutExpr extends Omit<BaseExpr, 'inout'> {
|
|
102
|
+
inout: ElementExpression
|
|
103
|
+
}
|
|
104
|
+
export function isInOut(expr: Expression): expr is InOutExpr {
|
|
105
|
+
return 'inout' in expr
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface IncomingExpr extends Omit<BaseExpr, 'incoming'> {
|
|
109
|
+
incoming: ElementExpression
|
|
110
|
+
}
|
|
111
|
+
export function isIncoming(expr: Expression): expr is IncomingExpr {
|
|
112
|
+
return 'incoming' in expr
|
|
113
|
+
}
|
|
114
|
+
export interface OutgoingExpr extends Omit<BaseExpr, 'outgoing'> {
|
|
115
|
+
outgoing: ElementExpression
|
|
116
|
+
}
|
|
117
|
+
export function isOutgoing(expr: Expression): expr is OutgoingExpr {
|
|
118
|
+
return 'outgoing' in expr
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type AnyRelationExpression = RelationExpr | InOutExpr | IncomingExpr | OutgoingExpr
|
|
122
|
+
|
|
123
|
+
export function isAnyRelation(expr: Expression): expr is AnyRelationExpression {
|
|
124
|
+
return isRelation(expr) || isInOut(expr) || isIncoming(expr) || isOutgoing(expr)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type Expression = ElementExpression | CustomElementExpr | AnyRelationExpression
|
|
128
|
+
|
|
129
|
+
// export const Expr = {
|
|
130
|
+
// isElementRef,
|
|
131
|
+
// isWildcard,
|
|
132
|
+
// isElementKindExpr,
|
|
133
|
+
// isElementTagExpr,
|
|
134
|
+
// isElement,
|
|
135
|
+
// isRelation,
|
|
136
|
+
// isInOut,
|
|
137
|
+
// isIncoming,
|
|
138
|
+
// isOutgoing,
|
|
139
|
+
// isAnyRelation,
|
|
140
|
+
// }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Element, Fqn } from './element'
|
|
2
|
+
import type { Relation, RelationID } from './relation'
|
|
3
|
+
import type { ComputedView, View, ViewID } from './view'
|
|
4
|
+
|
|
5
|
+
export interface LikeC4Model {
|
|
6
|
+
elements: Record<Fqn, Element>
|
|
7
|
+
relations: Record<RelationID, Relation>
|
|
8
|
+
views: Record<ViewID, View>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LikeC4ComputedModel {
|
|
12
|
+
elements: Record<Fqn, Element>
|
|
13
|
+
relations: Record<RelationID, Relation>
|
|
14
|
+
views: Record<ViewID, ComputedView>
|
|
15
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
declare const tag: unique symbol
|
|
2
|
+
|
|
3
|
+
type Tagged<Token> = {
|
|
4
|
+
readonly [tag]: Token
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
Create an opaque type, which hides its internal details from the public, and can only be created by being used explicitly.
|
|
9
|
+
|
|
10
|
+
The generic type parameter can be anything. It doesn't have to be an object.
|
|
11
|
+
|
|
12
|
+
[Read more about opaque types.](https://codemix.com/opaque-types-in-javascript/)
|
|
13
|
+
|
|
14
|
+
There have been several discussions about adding this feature to TypeScript via the `opaque type` operator, similar to how Flow does it. Unfortunately, nothing has (yet) moved forward:
|
|
15
|
+
- [Microsoft/TypeScript#202](https://github.com/microsoft/TypeScript/issues/202)
|
|
16
|
+
- [Microsoft/TypeScript#15408](https://github.com/Microsoft/TypeScript/issues/15408)
|
|
17
|
+
- [Microsoft/TypeScript#15807](https://github.com/Microsoft/TypeScript/issues/15807)
|
|
18
|
+
|
|
19
|
+
@example
|
|
20
|
+
```
|
|
21
|
+
import type {Opaque} from 'type-fest';
|
|
22
|
+
|
|
23
|
+
type AccountNumber = Opaque<number, 'AccountNumber'>;
|
|
24
|
+
type AccountBalance = Opaque<number, 'AccountBalance'>;
|
|
25
|
+
|
|
26
|
+
// The `Token` parameter allows the compiler to differentiate between types, whereas "unknown" will not. For example, consider the following structures:
|
|
27
|
+
type ThingOne = Opaque<string>;
|
|
28
|
+
type ThingTwo = Opaque<string>;
|
|
29
|
+
|
|
30
|
+
// To the compiler, these types are allowed to be cast to each other as they have the same underlying type. They are both `string & { __opaque__: unknown }`.
|
|
31
|
+
// To avoid this behaviour, you would instead pass the "Token" parameter, like so.
|
|
32
|
+
type NewThingOne = Opaque<string, 'ThingOne'>;
|
|
33
|
+
type NewThingTwo = Opaque<string, 'ThingTwo'>;
|
|
34
|
+
|
|
35
|
+
// Now they're completely separate types, so the following will fail to compile.
|
|
36
|
+
function createNewThingOne (): NewThingOne {
|
|
37
|
+
// As you can see, casting from a string is still allowed. However, you may not cast NewThingOne to NewThingTwo, and vice versa.
|
|
38
|
+
return 'new thing one' as NewThingOne;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// This will fail to compile, as they are fundamentally different types.
|
|
42
|
+
const thingTwo = createNewThingOne() as NewThingTwo;
|
|
43
|
+
|
|
44
|
+
// Here's another example of opaque typing.
|
|
45
|
+
function createAccountNumber(): AccountNumber {
|
|
46
|
+
return 2 as AccountNumber;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getMoneyForAccount(accountNumber: AccountNumber): AccountBalance {
|
|
50
|
+
return 4 as AccountBalance;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// This will compile successfully.
|
|
54
|
+
getMoneyForAccount(createAccountNumber());
|
|
55
|
+
|
|
56
|
+
// But this won't, because it has to be explicitly passed as an `AccountNumber` type.
|
|
57
|
+
getMoneyForAccount(2);
|
|
58
|
+
|
|
59
|
+
// You can use opaque values like they aren't opaque too.
|
|
60
|
+
const accountNumber = createAccountNumber();
|
|
61
|
+
|
|
62
|
+
// This will not compile successfully.
|
|
63
|
+
const newAccountNumber = accountNumber + 2;
|
|
64
|
+
|
|
65
|
+
// As a side note, you can (and should) use recursive types for your opaque types to make them stronger and hopefully easier to type.
|
|
66
|
+
type Person = {
|
|
67
|
+
id: Opaque<number, Person>;
|
|
68
|
+
name: string;
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
@category Type
|
|
73
|
+
*/
|
|
74
|
+
export type Opaque<Type, Token = unknown> = Type & Tagged<Token>
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
Revert an opaque type back to its original type by removing the readonly `[tag]`.
|
|
78
|
+
|
|
79
|
+
Why is this necessary?
|
|
80
|
+
|
|
81
|
+
1. Use an `Opaque` type as object keys
|
|
82
|
+
2. Prevent TS4058 error: "Return type of exported function has or is using name X from external module Y but cannot be named"
|
|
83
|
+
|
|
84
|
+
@example
|
|
85
|
+
```
|
|
86
|
+
import type {Opaque, UnwrapOpaque} from 'type-fest';
|
|
87
|
+
|
|
88
|
+
type AccountType = Opaque<'SAVINGS' | 'CHECKING', 'AccountType'>;
|
|
89
|
+
|
|
90
|
+
const moneyByAccountType: Record<UnwrapOpaque<AccountType>, number> = {
|
|
91
|
+
SAVINGS: 99,
|
|
92
|
+
CHECKING: 0.1
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Without UnwrapOpaque, the following expression would throw a type error.
|
|
96
|
+
const money = moneyByAccountType.SAVINGS; // TS error: Property 'SAVINGS' does not exist
|
|
97
|
+
|
|
98
|
+
// Attempting to pass an non-Opaque type to UnwrapOpaque will raise a type error.
|
|
99
|
+
type WontWork = UnwrapOpaque<string>;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
@category Type
|
|
103
|
+
*/
|
|
104
|
+
export type UnwrapOpaque<OpaqueType extends Tagged<unknown>> = OpaqueType extends Opaque<
|
|
105
|
+
infer Type,
|
|
106
|
+
OpaqueType[typeof tag]
|
|
107
|
+
> ? Type
|
|
108
|
+
: OpaqueType
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { NonEmptyArray } from './_common'
|
|
2
|
+
import type { Fqn, Tag } from './element'
|
|
3
|
+
import type { Opaque } from './opaque'
|
|
4
|
+
import type { ThemeColor } from './theme'
|
|
5
|
+
|
|
6
|
+
export type RelationID = Opaque<string, 'RelationID'>
|
|
7
|
+
export type RelationshipKind = Opaque<string, 'RelationshipKind'>
|
|
8
|
+
|
|
9
|
+
export type RelationshipLineType = 'dashed' | 'solid' | 'dotted'
|
|
10
|
+
|
|
11
|
+
// reference: https://graphviz.org/docs/attr-types/arrowType/
|
|
12
|
+
export type RelationshipArrowType =
|
|
13
|
+
| 'none'
|
|
14
|
+
| 'normal'
|
|
15
|
+
| 'onormal'
|
|
16
|
+
| 'diamond'
|
|
17
|
+
| 'odiamond'
|
|
18
|
+
| 'crow'
|
|
19
|
+
| 'open'
|
|
20
|
+
| 'vee'
|
|
21
|
+
|
|
22
|
+
export const DefaultLineStyle = 'dashed' satisfies RelationshipLineType
|
|
23
|
+
export const DefaultArrowType = 'normal' satisfies RelationshipArrowType
|
|
24
|
+
export const DefaultRelationshipColor = 'gray' satisfies ThemeColor
|
|
25
|
+
|
|
26
|
+
export interface Relation {
|
|
27
|
+
readonly id: RelationID
|
|
28
|
+
readonly source: Fqn
|
|
29
|
+
readonly target: Fqn
|
|
30
|
+
readonly title: string
|
|
31
|
+
readonly tags?: NonEmptyArray<Tag>
|
|
32
|
+
readonly kind?: RelationshipKind
|
|
33
|
+
readonly color?: ThemeColor
|
|
34
|
+
readonly line?: RelationshipLineType
|
|
35
|
+
readonly head?: RelationshipArrowType
|
|
36
|
+
readonly tail?: RelationshipArrowType
|
|
37
|
+
readonly links?: NonEmptyArray<string>
|
|
38
|
+
}
|