Skip to content

Commit 6559714

Browse files
fix: made component fields truly readonly
Before there were type hints that it is a readonly field. Now, there are only getters, no setters. Thus those fields can truly not change as soon as they are assigned in construction.
1 parent 67e4aab commit 6559714

File tree

7 files changed

+116
-65
lines changed

7 files changed

+116
-65
lines changed

lib/core/components/class.ts

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import type {
44
ComponentConstructorArguments,
55
ComponentInterface,
6+
ComponentLevels,
67
ComponentRenderArguments,
78
} from '#lib/core/components/interfaces';
89
import { Logger } from '#lib/logger';
@@ -11,46 +12,65 @@ import componentUtils from '#lib/core/components/utils/index';
1112

1213
/** Incorporates the constructor arguments into itself. */
1314
const Base = class Component implements ComponentConstructorArguments {
14-
/** Utils related to components and their workings. */
15-
// eslint-disable-next-line @typescript-eslint/naming-convention
16-
public static readonly utils = Object.freeze({ ...componentUtils });
17-
18-
/** The levels of certain components. Used for selectors: `slyde-component[level=1]`. */
19-
// eslint-disable-next-line @typescript-eslint/naming-convention
20-
public static readonly level = Object.freeze({
21-
/** The level at which the blocks can be placed. */
22-
block: 2,
23-
/** The level at which the presentation can be placed. */
24-
presentation: 0,
25-
/** The level at which the slides can be placed. */
26-
slide: 1,
27-
});
28-
29-
public readonly name;
30-
public readonly attributes;
31-
public readonly focusMode;
32-
public readonly level;
33-
public readonly path;
34-
public readonly id;
15+
readonly #name;
16+
readonly #attributes;
17+
readonly #focusMode;
18+
readonly #level;
19+
readonly #path;
20+
readonly #id;
3521

3622
/**
3723
* Creates a new `Component` from the arguments provided.
3824
*/
3925
public constructor(args: ComponentConstructorArguments) {
40-
this.name = new.target.name;
4126
Logger.debug(`constructing ${new.target.name} at ${args.path}`);
42-
this.attributes = args.attributes;
43-
this.focusMode = args.focusMode;
44-
this.level = args.level;
45-
this.path = args.path;
46-
this.id = args.id;
27+
this.#name = new.target.name;
28+
this.#attributes = args.attributes;
29+
this.#focusMode = args.focusMode;
30+
this.#level = args.level;
31+
this.#path = args.path;
32+
this.#id = args.id;
33+
}
34+
35+
/** Utils related to components and their workings. */
36+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
37+
public static get utils() {
38+
return Object.freeze({ ...componentUtils });
39+
}
40+
41+
/** The levels of certain components. Used for selectors: `slyde-component[level=1]`. */
42+
public static get level(): ComponentLevels {
43+
return Object.freeze({ block: 2, presentation: 0, slide: 1 });
44+
}
45+
46+
public get name(): ComponentInterface['name'] {
47+
return this.#name;
48+
}
49+
50+
public get attributes(): ComponentInterface['attributes'] {
51+
return this.#attributes;
52+
}
53+
54+
public get focusMode(): ComponentInterface['focusMode'] {
55+
return this.#focusMode;
56+
}
57+
58+
public get level(): ComponentInterface['level'] {
59+
return this.#level;
60+
}
61+
62+
public get path(): ComponentInterface['path'] {
63+
return this.#path;
64+
}
65+
66+
public get id(): ComponentInterface['id'] {
67+
return this.#id;
4768
}
4869
};
4970

5071
/** The `Component` base class before the registry is injected. */
5172
abstract class Component extends Base implements ComponentInterface {
52-
/** The width of this component. */
53-
public readonly width = Component.utils.extract({
73+
readonly #width = Component.utils.extract({
5474
aliases: ['width', 'w'],
5575
context: this,
5676
transform(value, context, key) {
@@ -61,8 +81,7 @@ abstract class Component extends Base implements ComponentInterface {
6181
},
6282
});
6383

64-
/** The height of this component. */
65-
public readonly height = Component.utils.extract({
84+
readonly #height = Component.utils.extract({
6685
aliases: ['height', 'h'],
6786
context: this,
6887
transform(value, context, key) {
@@ -73,8 +92,7 @@ abstract class Component extends Base implements ComponentInterface {
7392
},
7493
});
7594

76-
/** The maner of displaying this component. */
77-
public readonly display = Component.utils.extract({
95+
readonly #display = Component.utils.extract({
7896
aliases: ['display', 'd'],
7997
context: this,
8098
fallback: 'block',
@@ -96,8 +114,20 @@ abstract class Component extends Base implements ComponentInterface {
96114
}
97115
}
98116

117+
public get width(): string | undefined {
118+
return this.#width;
119+
}
120+
121+
public get height(): string | undefined {
122+
return this.#height;
123+
}
124+
125+
public get display(): string {
126+
return this.#display;
127+
}
128+
99129
public canBeAtLevel(level: number): ReturnType<ComponentInterface['canBeAtLevel']> {
100-
const hierarchy = (this as Partial<Pick<this, 'hierarchy'>>).hierarchy?.() ?? '*';
130+
const hierarchy = (this as Partial<this>).hierarchy?.() ?? '*';
101131
if (hierarchy === '*') return true;
102132
if (hierarchy.includes(level)) return true;
103133

@@ -131,6 +161,9 @@ declare namespace ComponentWithRegistry {
131161
/** The type for an instance of a `MarkupRenderer`. */
132162
export type Instance = Component;
133163

164+
/** The levels at which certain types of components live. */
165+
export type Levels = ComponentLevels;
166+
134167
/**
135168
* The arguments to provide to the constructor of a component.
136169
*/

lib/core/components/interfaces.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
/** The levels at which certain types of components live. */
2+
export interface ComponentLevels {
3+
/** The level at which the presentation can be placed. */
4+
presentation: number;
5+
/** The level at which the slides can be placed. */
6+
slide: number;
7+
/** The level at which the blocks can be placed. */
8+
block: number;
9+
}
10+
111
/**
212
* The arguments to provide to the constructor of a component.
313
*/
@@ -88,6 +98,15 @@ export interface ComponentInterface extends ComponentConstructorArguments {
8898
*/
8999
readonly name: string;
90100

101+
/** The width of this component. */
102+
readonly width?: string;
103+
104+
/** The height of this component. */
105+
readonly height?: string;
106+
107+
/** The maner of displaying this component. */
108+
readonly display: string;
109+
91110
/**
92111
* Render this component to HTML.
93112
*/

lib/core/render/render.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export const render = async function render(input: XmlParserResult): Promise<str
136136
const context = {
137137
attributes: input.root.attributes,
138138
canBeAtLevel: (): boolean => true,
139+
display: '',
139140
focusMode: 'default',
140141
hierarchy: (): '*' => '*',
141142
id: '0',

lib/core/render/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { XmlParserElementNode } from 'xml-parser-xo';
66
/** Wraps HTML in a `<slyde-component>` This is to make it easier for us to select elements. */
77
export const wrapper = function wrapper(
88
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
9-
component: Component.Instance,
9+
component: Component.Interface,
1010
state: RenderState,
1111
children: string
1212
): string {

test/lib/core/components/utils/extract.test.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,21 @@ describe('function extract', () => {
1212
spy.mockReset();
1313
});
1414

15+
const error = function error(): never {
16+
throw new Error('Function not implemented.');
17+
}
18+
1519
const instance = {
1620
attributes: {},
17-
canBeAtLevel(): ReturnType<Component.Interface['canBeAtLevel']> {
18-
throw new Error('Function not implemented.');
19-
},
21+
canBeAtLevel: error,
22+
display: '',
2023
focusMode: 'follows',
21-
hierarchy(): ReturnType<Component.Interface['hierarchy']> {
22-
throw new Error('Function not implemented.');
23-
},
24+
hierarchy: error,
2425
id: '0',
2526
level: 0,
2627
name: '',
2728
path: 'xpath://',
28-
render(): ReturnType<Component.Interface['render']> {
29-
throw new Error('Function not implemented.');
30-
},
29+
render: error,
3130
} satisfies Component.Interface;
3231

3332
test('looking for property that exists', () => {

test/lib/core/components/utils/transform.test.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,21 @@ import { describe, expect, test } from 'vitest';
22
import type { ComponentInterface } from '#lib/core/components';
33
import transform from '#lib/core/components/utils/transform';
44

5-
const fakeContext = {
5+
const error = function error(): never {
6+
throw new Error('Function not implemented.');
7+
}
8+
9+
const instance = {
610
attributes: {},
7-
canBeAtLevel(): boolean {
8-
throw new Error('Function not implemented.');
9-
},
11+
canBeAtLevel: error,
12+
display: '',
1013
focusMode: 'follows',
11-
hierarchy(): ReturnType<ComponentInterface['hierarchy']> {
12-
throw new Error('Function not implemented.');
13-
},
14-
id: '',
14+
hierarchy: error,
15+
id: '0',
1516
level: 0,
16-
name: 'TestComponent',
17-
path: '/test/path',
18-
render(): string {
19-
throw new Error('Function not implemented.');
20-
},
17+
name: '',
18+
path: 'xpath://',
19+
render: error,
2120
} satisfies ComponentInterface;
2221

2322
describe('function transform.boolean', () => {
@@ -36,11 +35,11 @@ describe('function transform.boolean', () => {
3635
['0', false],
3736
['-', false],
3837
])('converts "%s" to %s', (input, expected) => {
39-
expect(boolean(input, fakeContext, 'key')).toBe(expected);
38+
expect(boolean(input, instance, 'key')).toBe(expected);
4039
});
4140

4241
test('throws on invalid boolean', () => {
43-
expect(() => boolean('maybe', fakeContext, 'key')).toThrow();
42+
expect(() => boolean('maybe', instance, 'key')).toThrow();
4443
});
4544
});
4645

@@ -53,11 +52,11 @@ describe('function transform.number', () => {
5352
[10, 10],
5453
['0', 0],
5554
])('converts "%s" to %s', (input, expected) => {
56-
expect(number(input, fakeContext, 'padding')).toBe(expected);
55+
expect(number(input, instance, 'padding')).toBe(expected);
5756
});
5857

5958
test('throws on invalid number', () => {
60-
expect(() => number('abc', fakeContext, 'padding')).toThrow();
59+
expect(() => number('abc', instance, 'padding')).toThrow();
6160
});
6261
});
6362

@@ -66,10 +65,10 @@ describe('function transform.enum', () => {
6665
const colorEnum = transform.enum(colors);
6766

6867
test.each(colors)('accepts valid enum value "%s"', (value) => {
69-
expect(colorEnum(value, fakeContext, 'color')).toBe(value);
68+
expect(colorEnum(value, instance, 'color')).toBe(value);
7069
});
7170

7271
test('throws on invalid enum', () => {
73-
expect(() => colorEnum('yellow', fakeContext, 'color')).toThrow();
72+
expect(() => colorEnum('yellow', instance, 'color')).toThrow();
7473
});
7574
});

test/lib/core/render/utils.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { describe, expect, test } from 'vitest';
1111
import type { XmlParserElementNode } from 'xml-parser-xo';
1212

1313
describe('function wrapper', () => {
14-
const component: Component.Instance = {
14+
const component: Component.Interface = {
1515
attributes: {},
1616
canBeAtLevel(): never {
1717
throw new Error('Function not implemented.');
@@ -32,7 +32,7 @@ describe('function wrapper', () => {
3232
},
3333
// eslint-disable-next-line no-void
3434
width: void 0,
35-
} satisfies Component.Instance;
35+
} satisfies Component.Interface;
3636

3737
const state: RenderState = {
3838
globals: Globals.instance,

0 commit comments

Comments
 (0)