@versatiles/style 5.8.4 → 5.9.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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@versatiles/style",
3
- "version": "5.8.4",
3
+ "version": "5.9.0",
4
4
  "description": "Generate StyleJSON for MapLibre",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "devEngines": {
8
8
  "runtime": {
9
9
  "name": "node",
10
- "version": ">=20.0.0 <24.0.0"
10
+ "version": ">= 20.0.0"
11
11
  }
12
12
  },
13
13
  "scripts": {
@@ -48,8 +48,8 @@
48
48
  "src/*"
49
49
  ],
50
50
  "devDependencies": {
51
- "@maplibre/maplibre-gl-native": "^6.2.0",
52
- "@maplibre/maplibre-gl-style-spec": "^24.3.1",
51
+ "@maplibre/maplibre-gl-native": "^6.3.0",
52
+ "@maplibre/maplibre-gl-style-spec": "^24.4.1",
53
53
  "@rollup/plugin-commonjs": "^29.0.0",
54
54
  "@rollup/plugin-node-resolve": "^16.0.3",
55
55
  "@rollup/plugin-terser": "^0.4.4",
@@ -57,26 +57,26 @@
57
57
  "@types/bin-pack": "^1.0.3",
58
58
  "@types/brace-expansion": "^1.1.2",
59
59
  "@types/inquirer": "^9.0.9",
60
- "@types/node": "^24.10.2",
60
+ "@types/node": "^25.2.1",
61
61
  "@types/tar-stream": "^3.1.4",
62
- "@typescript-eslint/eslint-plugin": "^8.49.0",
63
- "@typescript-eslint/parser": "^8.49.0",
64
- "@versatiles/release-tool": "^2.5.0",
65
- "@vitest/coverage-v8": "^4.0.15",
62
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
63
+ "@typescript-eslint/parser": "^8.54.0",
64
+ "@versatiles/release-tool": "^2.7.0",
65
+ "@vitest/coverage-v8": "^4.0.18",
66
66
  "bin-pack": "^1.0.2",
67
- "esbuild": "^0.27.1",
68
- "eslint": "^9.39.1",
67
+ "esbuild": "^0.27.3",
68
+ "eslint": "^9.39.2",
69
69
  "husky": "^9.1.7",
70
- "inquirer": "^13.0.2",
71
- "prettier": "^3.7.4",
72
- "rollup": "^4.53.3",
70
+ "inquirer": "^13.2.2",
71
+ "prettier": "^3.8.1",
72
+ "rollup": "^4.57.1",
73
73
  "rollup-plugin-dts": "^6.3.0",
74
74
  "rollup-plugin-sourcemaps2": "^0.5.4",
75
75
  "sharp": "^0.34.5",
76
76
  "tar-stream": "^3.1.7",
77
77
  "tsx": "^4.21.0",
78
78
  "typescript": "^5.9.3",
79
- "typescript-eslint": "^8.49.0",
80
- "vitest": "^4.0.15"
79
+ "typescript-eslint": "^8.54.0",
80
+ "vitest": "^4.0.18"
81
81
  }
82
82
  }
@@ -38,10 +38,9 @@ describe('HSL Class', () => {
38
38
  expect(color2.asString()).toBe('hsla(120,50%,50%,0.5)');
39
39
  });
40
40
 
41
- it('asHSL and toHSL should return the same instance', () => {
41
+ it('asHSL should return a clone', () => {
42
42
  const color = new HSL(120, 50, 50);
43
43
  expect(color.asHSL()).toStrictEqual(color);
44
- expect(color.toHSL()).toStrictEqual(color);
45
44
  });
46
45
 
47
46
  it('asHSV should correctly convert HSL to HSV', () => {
package/src/color/hsl.ts CHANGED
@@ -87,14 +87,6 @@ export class HSL extends Color {
87
87
  return this.clone();
88
88
  }
89
89
 
90
- /**
91
- * Returns the current HSL color.
92
- * @returns The current HSL color.
93
- */
94
- toHSL(): HSL {
95
- return this;
96
- }
97
-
98
90
  /**
99
91
  * Converts the HSL color to an HSV color.
100
92
  * @returns A new HSV color representing the same color.
package/src/color/hsv.ts CHANGED
@@ -85,7 +85,7 @@ export class HSV extends Color {
85
85
  const v = this.v / 100;
86
86
  const k = (2 - s) * v;
87
87
  const q = k < 1 ? k : 2 - k;
88
- return new HSL(this.h, q == 0 ? 0 : (100 * s * v) / q, (100 * k) / 2, this.a);
88
+ return new HSL(this.h, q === 0 ? 0 : (100 * s * v) / q, (100 * k) / 2, this.a);
89
89
  }
90
90
 
91
91
  /**
@@ -73,7 +73,9 @@ describe('Color.parse', () => {
73
73
  });
74
74
 
75
75
  it('throws an error for unsupported formats', () => {
76
- expect(() => Color.parse('invalid color string')).toThrow('Unknown color format: invalid color string');
76
+ expect(() => Color.parse('invalid color string')).toThrow(
77
+ 'Color.parse: Unknown color format "invalid color string"'
78
+ );
77
79
  });
78
80
  });
79
81
 
@@ -97,3 +99,89 @@ describe('Exported Module', () => {
97
99
  expect(module.Color).toBe(Color);
98
100
  });
99
101
  });
102
+
103
+ describe('Color Transformation Methods', () => {
104
+ it('gamma() applies gamma correction', () => {
105
+ const color = Color.parse('#808080');
106
+ expect(color.gamma(2.2).asString()).toBe('rgb(56,56,56)');
107
+ });
108
+
109
+ it('gamma() works from HSL', () => {
110
+ const hsl = new HSL(120, 50, 50, 1);
111
+ expect(hsl.gamma(2.2).asString()).toBe('rgb(12,135,12)');
112
+ });
113
+
114
+ it('contrast() adjusts contrast', () => {
115
+ const color = Color.parse('#FF8040');
116
+ expect(color.contrast(1.5).asString()).toBe('rgb(255,128,32)');
117
+ });
118
+
119
+ it('contrast() works from HSV', () => {
120
+ const hsv = new HSV(180, 50, 50, 1);
121
+ expect(hsv.contrast(1.5).asString()).toBe('rgb(32,128,128)');
122
+ });
123
+
124
+ it('brightness() works from HSL', () => {
125
+ const hsl = new HSL(240, 100, 50, 1);
126
+ expect(hsl.brightness(0.3).asString()).toBe('rgb(77,77,255)');
127
+ });
128
+
129
+ it('lighten() works from HSL', () => {
130
+ const hsl = new HSL(0, 100, 30, 1);
131
+ expect(hsl.lighten(0.2).asString()).toBe('rgb(173,51,51)');
132
+ });
133
+
134
+ it('darken() works from HSV', () => {
135
+ const hsv = new HSV(60, 100, 80, 1);
136
+ expect(hsv.darken(0.3).asString()).toBe('rgb(143,143,0)');
137
+ });
138
+
139
+ it('tint() blends with a tint color', () => {
140
+ const color = Color.parse('#FF0000');
141
+ const tintColor = Color.parse('#0000FF');
142
+ expect(color.tint(0.5, tintColor).asString()).toBe('rgb(128,0,128)');
143
+ });
144
+
145
+ it('tint() works from HSL', () => {
146
+ const hsl = new HSL(0, 100, 50, 1);
147
+ const tintColor = new HSL(120, 100, 50, 1);
148
+ expect(hsl.tint(0.5, tintColor).asString()).toBe('rgb(128,128,0)');
149
+ });
150
+
151
+ it('blend() blends with another color', () => {
152
+ const color1 = Color.parse('#FF0000');
153
+ const color2 = Color.parse('#0000FF');
154
+ expect(color1.blend(0.5, color2).asString()).toBe('rgb(128,0,128)');
155
+ });
156
+
157
+ it('blend() works from HSV', () => {
158
+ const hsv1 = new HSV(0, 100, 100, 1);
159
+ const hsv2 = new HSV(240, 100, 100, 1);
160
+ expect(hsv1.blend(0.3, hsv2).asString()).toBe('rgb(179,0,77)');
161
+ });
162
+
163
+ it('setHue() changes the hue', () => {
164
+ const color = Color.parse('#FF0000');
165
+ expect(color.setHue(180).asString()).toBe('hsl(180,100%,50%)');
166
+ });
167
+
168
+ it('invertLuminosity() inverts from HSV', () => {
169
+ const hsv = new HSV(200, 50, 60, 1);
170
+ expect(hsv.invertLuminosity().asString()).toBe('hsl(200,33%,55%)');
171
+ });
172
+
173
+ it('rotateHue() rotates from RGB', () => {
174
+ const rgb = new RGB(255, 0, 0, 1);
175
+ expect(rgb.rotateHue(120).asString()).toBe('hsl(120,100%,50%)');
176
+ });
177
+
178
+ it('saturate() increases saturation from HSV', () => {
179
+ const hsv = new HSV(100, 30, 70, 1);
180
+ expect(hsv.saturate(1.5).asString()).toBe('hsl(100,65%,60%)');
181
+ });
182
+
183
+ it('invert() inverts from HSL', () => {
184
+ const hsl = new HSL(180, 100, 50, 1);
185
+ expect(hsl.invert().asString()).toBe('rgb(255,0,0)');
186
+ });
187
+ });
@@ -20,7 +20,9 @@ Color.parse = function (input: string | Color): Color {
20
20
  case 'hsla(':
21
21
  return HSL.parse(input);
22
22
  default:
23
- throw Error('Unknown color format: ' + input);
23
+ throw new Error(
24
+ `Color.parse: Unknown color format "${input}". Expected formats: "#RRGGBB", "#RGB", "rgb(...)", "rgba(...)", "hsl(...)", or "hsla(...)".`
25
+ );
24
26
  }
25
27
  };
26
28
 
@@ -42,6 +42,60 @@ describe('RandomColor', () => {
42
42
  expect(t({ seed: 'testSeed' })).toBe('hsl(185,90%,23%)');
43
43
  });
44
44
 
45
+ it('generates light colors with luminosity: "light"', () => {
46
+ const color = randomColor({ seed: 'lightSeed', luminosity: 'light' });
47
+ const hsv = color.asArray();
48
+ // Light colors should have higher brightness values
49
+ expect(hsv[2]).toBeGreaterThan(50);
50
+ expect(color).toBeInstanceOf(HSV);
51
+ });
52
+
53
+ it('generates random luminosity colors with luminosity: "random"', () => {
54
+ const color = randomColor({ seed: 'randomSeed', luminosity: 'random' });
55
+ const hsv = color.asArray();
56
+ // Random luminosity can be anywhere from 0-100
57
+ expect(hsv[2]).toBeGreaterThanOrEqual(0);
58
+ expect(hsv[2]).toBeLessThanOrEqual(100);
59
+ expect(color).toBeInstanceOf(HSV);
60
+ });
61
+
62
+ it('generates light saturation with luminosity: "light"', () => {
63
+ // Test light luminosity affects saturation picking
64
+ const color1 = randomColor({ seed: 'lightTest1', luminosity: 'light', hue: 'blue' });
65
+ const color2 = randomColor({ seed: 'lightTest2', luminosity: 'light', hue: 'green' });
66
+ expect(color1).toBeInstanceOf(HSV);
67
+ expect(color2).toBeInstanceOf(HSV);
68
+ });
69
+
70
+ it('generates colors with various saturation options', () => {
71
+ const weak = randomColor({ seed: 'satTest', saturation: 'weak' });
72
+ const strong = randomColor({ seed: 'satTest', saturation: 'strong' });
73
+
74
+ expect(weak).toBeInstanceOf(HSV);
75
+ expect(strong).toBeInstanceOf(HSV);
76
+ // Strong saturation should have higher saturation values
77
+ expect(strong.s).toBeGreaterThan(80);
78
+ });
79
+
80
+ it('generates colors with all hue name options', () => {
81
+ const hues: Array<string | number> = [
82
+ 'red',
83
+ 'orange',
84
+ 'yellow',
85
+ 'green',
86
+ 'blue',
87
+ 'purple',
88
+ 'pink',
89
+ 'monochrome',
90
+ 180,
91
+ ];
92
+
93
+ hues.forEach((hue) => {
94
+ const color = randomColor({ seed: `hue-${hue}`, hue });
95
+ expect(color).toBeInstanceOf(HSV);
96
+ });
97
+ });
98
+
45
99
  it('consistent color generation with a seed', () => {
46
100
  const color1 = randomColor({ seed: 'consistentSeed' });
47
101
  const color2 = randomColor({ seed: 'consistentSeed' });
@@ -263,5 +263,7 @@ function getColorInfo(hue: number): ColorInfo {
263
263
  return color;
264
264
  }
265
265
  }
266
- throw Error('Color hue value not found');
266
+ throw new Error(
267
+ `getColorInfo: No color info found for hue value ${hue}. This indicates a gap in the color dictionary hue ranges.`
268
+ );
267
269
  }
@@ -84,10 +84,9 @@ describe('RGB Class', () => {
84
84
  expect(RGB.parse('#FF0080').asHSV().round().asArray()).toStrictEqual([330, 100, 100, 1]);
85
85
  });
86
86
 
87
- it('asRGB and toRGB return the same instance or clone', () => {
87
+ it('asRGB returns a clone', () => {
88
88
  const color = new RGB(255, 128, 64, 0.5);
89
89
  expect(color.asRGB()).toStrictEqual(color);
90
- expect(color.toRGB()).toStrictEqual(color);
91
90
  });
92
91
 
93
92
  it('handles black correctly', () => {
@@ -182,6 +181,8 @@ describe('RGB Class', () => {
182
181
  expect(pc((c) => c.tint(0.5, tintColor))).toStrictEqual([125, 100, 125, 0.8]);
183
182
  expect(pc((c) => c.tint(1, tintColor))).toStrictEqual([200, 50, 50, 0.8]);
184
183
  expect(pc((c) => c.tint(0, tintColor))).toStrictEqual([50, 150, 200, 0.8]);
184
+ expect(pc((c) => c.tint(-1, tintColor))).toStrictEqual([50, 150, 200, 0.8]);
185
+ expect(pc((c) => c.tint(2, tintColor))).toStrictEqual([200, 50, 50, 0.8]);
185
186
  });
186
187
 
187
188
  it('blends color correctly', () => {
@@ -191,6 +192,8 @@ describe('RGB Class', () => {
191
192
  expect(pc((c) => c.blend(0.8, blendColor))).toStrictEqual([214, 30, 40, 0.8]);
192
193
  expect(pc((c) => c.blend(1, blendColor))).toStrictEqual([255, 0, 0, 0.8]);
193
194
  expect(pc((c) => c.blend(0, blendColor))).toStrictEqual([50, 150, 200, 0.8]);
195
+ expect(pc((c) => c.blend(-1, blendColor))).toStrictEqual([50, 150, 200, 0.8]);
196
+ expect(pc((c) => c.blend(2, blendColor))).toStrictEqual([255, 0, 0, 0.8]);
194
197
  });
195
198
 
196
199
  it('lightens the color correctly', () => {
package/src/color/rgb.ts CHANGED
@@ -181,15 +181,6 @@ export class RGB extends Color {
181
181
  return this.clone();
182
182
  }
183
183
 
184
- /**
185
- * Returns the RGB color.
186
- *
187
- * @returns The current RGB instance.
188
- */
189
- toRGB(): RGB {
190
- return this;
191
- }
192
-
193
184
  /**
194
185
  * Parses a string or Color instance into an RGB color.
195
186
  *
@@ -8,7 +8,7 @@ export function clamp(value: number, min: number, max: number): number {
8
8
  export function mod(value: number, max: number): number {
9
9
  value = value % max;
10
10
  if (value < 0) value += max;
11
- if (value == 0) return 0;
11
+ if (value === 0) return 0;
12
12
  return value;
13
13
  }
14
14
 
@@ -58,7 +58,9 @@ export function guessStyle(tileJSON: TileJSONSpecification, options?: GuessStyle
58
58
  tileJSON.tiles = tileJSON.tiles.map((url) => resolveUrl(baseUrl, url));
59
59
  }
60
60
 
61
- if (!isTileJSONSpecification(tileJSON)) throw Error('Invalid TileJSON specification');
61
+ if (!isTileJSONSpecification(tileJSON)) {
62
+ throw new Error('guessStyle: Invalid TileJSON specification (this error should never be reached)');
63
+ }
62
64
 
63
65
  let style: StyleSpecification;
64
66
  if (isRasterTileJSONSpecification(tileJSON)) {
package/src/index.test.ts CHANGED
@@ -110,7 +110,7 @@ describe('exports', () => {
110
110
  it('should export styles', () => {
111
111
  type something = Record<string, unknown>;
112
112
  expect(typeof lib.styles).toBe('object');
113
- const styleNames = ['colorful', 'eclipse', 'graybeard', 'neutrino', 'shadow'];
113
+ const styleNames = ['colorful', 'eclipse', 'graybeard', 'neutrino', 'shadow', 'satellite'];
114
114
  for (const name of styleNames) {
115
115
  expect(typeof (lib as something)[name]).toBe('function');
116
116
  expect(typeof (lib.styles as something)[name]).toBe('function');
package/src/index.ts CHANGED
@@ -80,14 +80,24 @@
80
80
  * @module
81
81
  */
82
82
 
83
- export { colorful, eclipse, graybeard, neutrino, shadow, type StyleBuilderFunction } from './styles/index.js';
84
- import { colorful, eclipse, graybeard, neutrino, shadow } from './styles/index.js';
83
+ export {
84
+ colorful,
85
+ eclipse,
86
+ graybeard,
87
+ neutrino,
88
+ shadow,
89
+ satellite,
90
+ type StyleBuilderFunction,
91
+ type SatelliteStyleOptions,
92
+ } from './styles/index.js';
93
+ import { colorful, eclipse, graybeard, neutrino, shadow, satellite } from './styles/index.js';
85
94
  export const styles = {
86
95
  colorful,
87
96
  eclipse,
88
97
  graybeard,
89
98
  shadow,
90
99
  neutrino,
100
+ satellite,
91
101
  };
92
102
 
93
103
  export type { GuessStyleOptions } from './guess_style/index.js';
@@ -85,7 +85,7 @@ describe('isBasicType', () => {
85
85
  });
86
86
 
87
87
  it('throws an error for unsupported types like functions', () => {
88
- expect(() => isBasicType(() => true)).toThrow('unknown type: function');
88
+ expect(() => isBasicType(() => true)).toThrow('isBasicType: Unknown type "function"');
89
89
  });
90
90
  });
91
91
 
@@ -123,6 +123,89 @@ describe('deepMerge', () => {
123
123
  const source = { a: { b: 1 } };
124
124
  expect(() => deepMerge(target, source)).toThrow('Not implemented yet: "function" case');
125
125
  });
126
+
127
+ it('merges multiple source objects', () => {
128
+ const target = { a: 1, b: 2, c: 3 };
129
+ const source1 = { b: 10, d: 4 };
130
+ const source2 = { c: 20, e: 5 };
131
+ const result = deepMerge(target, source1, source2);
132
+ expect(result).toEqual({ a: 1, b: 10, c: 20, d: 4, e: 5 });
133
+ });
134
+
135
+ it('handles undefined values in source', () => {
136
+ const target = { a: 1, b: 2 };
137
+ const source = { a: undefined, c: 3 };
138
+ const result = deepMerge(target, source);
139
+ expect(result).toEqual({ a: undefined, b: 2, c: 3 });
140
+ });
141
+
142
+ it('overwrites with null when target is a basic type', () => {
143
+ const target = { a: 1, b: 'string' };
144
+ const source = { a: null, b: null };
145
+ const result = deepMerge(target, source);
146
+ expect(result).toEqual({ a: null, b: null });
147
+ });
148
+
149
+ it('throws error when merging null with object', () => {
150
+ const target = { a: { x: 10 } } as { a: object | null };
151
+ const source = { a: null };
152
+ expect(() => deepMerge(target, source)).toThrow('deepMerge: Cannot merge incompatible types for key "a"');
153
+ });
154
+
155
+ it('merges with empty source object', () => {
156
+ const target = { a: 1, b: 2 };
157
+ const source = {};
158
+ const result = deepMerge(target, source);
159
+ expect(result).toEqual({ a: 1, b: 2 });
160
+ });
161
+
162
+ it('skips non-object sources', () => {
163
+ const target = { a: 1, b: 2 };
164
+ // @ts-expect-error Testing runtime behavior with invalid input
165
+ const result = deepMerge(target, null, undefined, 'string', 123, { c: 3 });
166
+ expect(result).toEqual({ a: 1, b: 2, c: 3 });
167
+ });
168
+
169
+ it('throws error when merging incompatible types (array with object)', () => {
170
+ const target = { a: [1, 2, 3] } as { a: object };
171
+ const source = { a: { b: 1 } };
172
+ expect(() => deepMerge(target, source)).toThrow('deepMerge: Cannot merge incompatible types for key "a"');
173
+ });
174
+
175
+ it('throws error when merging incompatible types (object with array)', () => {
176
+ const target = { a: { b: 1 } } as { a: object };
177
+ const source = { a: [1, 2, 3] };
178
+ expect(() => deepMerge(target, source)).toThrow('deepMerge: Cannot merge incompatible types for key "a"');
179
+ });
180
+
181
+ it('overwrites basic types with arrays', () => {
182
+ const target = { a: 1, b: 'string' };
183
+ const source = { a: [1, 2, 3], b: [4, 5] };
184
+ const result = deepMerge(target, source);
185
+ expect(result).toEqual({ a: [1, 2, 3], b: [4, 5] });
186
+ expect(result.a).not.toBe(source.a); // Should be a deep clone
187
+ });
188
+
189
+ it('throws error when merging array with array', () => {
190
+ const target = { a: [1, 2, 3] };
191
+ const source = { a: [4, 5] };
192
+ expect(() => deepMerge(target, source)).toThrow('deepMerge: Cannot merge incompatible types for key "a"');
193
+ });
194
+
195
+ it('deeply merges nested objects across multiple levels', () => {
196
+ const target = { a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 };
197
+ const source = { a: { b: { c: 10 }, g: 5 }, h: 6 };
198
+ const result = deepMerge(target, source);
199
+ expect(result).toEqual({ a: { b: { c: 10, d: 2 }, e: 3, g: 5 }, f: 4, h: 6 });
200
+ });
201
+
202
+ it('does not mutate the original target object', () => {
203
+ const target = { a: { b: 1 }, c: 2 };
204
+ const source = { a: { b: 10 }, d: 3 };
205
+ const result = deepMerge(target, source);
206
+ expect(target).toEqual({ a: { b: 1 }, c: 2 }); // Original unchanged
207
+ expect(result).toEqual({ a: { b: 10 }, c: 2, d: 3 });
208
+ });
126
209
  });
127
210
 
128
211
  describe('resolveUrl', () => {
package/src/lib/utils.ts CHANGED
@@ -29,9 +29,7 @@ export function deepClone<T>(obj: T): T {
29
29
 
30
30
  if (obj == null) return obj;
31
31
 
32
- console.log('obj', obj);
33
- console.log('obj.prototype', Object.getPrototypeOf(obj));
34
- throw Error();
32
+ throw new Error(`deepClone: Unsupported object type "${Object.getPrototypeOf(obj)?.constructor?.name ?? 'unknown'}"`);
35
33
  }
36
34
 
37
35
  export function isSimpleObject(item: unknown): item is object {
@@ -54,7 +52,7 @@ export function isBasicType(item: unknown): item is boolean | number | string |
54
52
  case 'object':
55
53
  return false;
56
54
  default:
57
- throw Error('unknown type: ' + typeof item);
55
+ throw new Error(`isBasicType: Unknown type "${typeof item}"`);
58
56
  }
59
57
  }
60
58
 
@@ -68,9 +66,7 @@ export function deepMerge<T extends object>(source0: T, ...sources: Partial<T>[]
68
66
 
69
67
  const sourceValue = source[key] as T[typeof key];
70
68
 
71
- // *********
72
- // overwrite
73
- // *********
69
+ // Handle basic types (number, string, boolean) - always overwrite
74
70
  switch (typeof sourceValue) {
75
71
  case 'number':
76
72
  case 'string':
@@ -80,33 +76,28 @@ export function deepMerge<T extends object>(source0: T, ...sources: Partial<T>[]
80
76
  default:
81
77
  }
82
78
 
79
+ // If target is a basic type, overwrite with deep clone of source
83
80
  if (isBasicType(target[key])) {
84
81
  target[key] = deepClone(sourceValue);
85
82
  continue;
86
83
  }
87
84
 
85
+ // Handle Color instances - clone the source color
88
86
  if (sourceValue instanceof Color) {
89
87
  target[key] = sourceValue.clone() as T[typeof key];
90
88
  continue;
91
89
  }
92
90
 
91
+ // If both are simple objects, merge them recursively
93
92
  if (isSimpleObject(target[key]) && isSimpleObject(sourceValue)) {
94
93
  target[key] = deepMerge(target[key], sourceValue);
95
94
  continue;
96
95
  }
97
96
 
98
- // *********
99
- // merge
100
- // *********
101
-
102
- if (isSimpleObject(target[key]) && isSimpleObject(sourceValue)) {
103
- target[key] = deepMerge(target[key], sourceValue);
104
- continue;
105
- }
106
-
107
- console.log('target[key]:', target[key]);
108
- console.log('source[key]:', source[key]);
109
- throw Error('unpredicted case');
97
+ // Incompatible types - throw error
98
+ throw new Error(
99
+ `deepMerge: Cannot merge incompatible types for key "${String(key)}" (target: ${typeof target[key]}, source: ${typeof sourceValue})`
100
+ );
110
101
  }
111
102
  }
112
103
  return target;
@@ -22,7 +22,9 @@ export function decorate(layers: MaplibreLayer[], rules: StyleRules, recolor: Ca
22
22
  if (!id.includes('*')) return id;
23
23
  const regExpString = id.replace(/[^a-z_:-]/g, (c) => {
24
24
  if (c === '*') return '[a-z_-]*';
25
- throw new Error('unknown char to process. Do not know how to make a RegExp from: ' + JSON.stringify(c));
25
+ throw new Error(
26
+ `decorator: Invalid character ${JSON.stringify(c)} in layer ID pattern "${id}". Only alphanumeric, underscore, colon, hyphen, and asterisk are allowed.`
27
+ );
26
28
  });
27
29
  const regExp = new RegExp(`^${regExpString}$`, 'i');
28
30
  return layerIds.filter((layerId) => regExp.test(layerId));
@@ -78,26 +80,27 @@ export function decorate(layers: MaplibreLayer[], rules: StyleRules, recolor: Ca
78
80
  value = processExpression(value);
79
81
  break;
80
82
  default:
81
- throw new Error(`unknown propertyDef.valueType "${propertyDef.valueType}" for key "${key}"`);
83
+ throw new Error(
84
+ `decorator: Unknown property value type "${propertyDef.valueType}" for key "${key}" on layer type "${layer.type}". This may indicate a MapLibre property definition mismatch.`
85
+ );
82
86
  }
83
87
 
84
88
  switch (propertyDef.parent) {
85
89
  case 'layer':
86
- // @ts-expect-error: too complex to handle
87
- layer[key] = value;
90
+ (layer as Record<string, unknown>)[key] = value;
88
91
  break;
89
92
  case 'layout':
90
93
  if (!layer.layout) layer.layout = {};
91
- // @ts-expect-error: too complex to handle
92
- layer.layout[key] = value;
94
+ (layer.layout as Record<string, unknown>)[key] = value;
93
95
  break;
94
96
  case 'paint':
95
97
  if (!layer.paint) layer.paint = {};
96
- // @ts-expect-error: too complex to handle
97
- layer.paint[key] = value;
98
+ (layer.paint as Record<string, unknown>)[key] = value;
98
99
  break;
99
100
  default:
100
- throw new Error(`unknown parent "${propertyDef.parent}" for key "${key}"`);
101
+ throw new Error(
102
+ `decorator: Unknown property parent "${propertyDef.parent}" for key "${key}" on layer type "${layer.type}". Expected "layer", "layout", or "paint".`
103
+ );
101
104
  }
102
105
  });
103
106
  }
@@ -108,12 +111,16 @@ export function decorate(layers: MaplibreLayer[], rules: StyleRules, recolor: Ca
108
111
  const color = recolor.do(value as Color);
109
112
  return color.asString();
110
113
  }
111
- throw new Error(`unknown color type "${typeof value}"`);
114
+ throw new Error(
115
+ `decorator.processColor: Expected a color string or Color instance, but got ${typeof value}. Value: ${JSON.stringify(value)}`
116
+ );
112
117
  }
113
118
 
114
119
  function processFont(value: StyleRuleValue): string[] {
115
120
  if (typeof value === 'string') return [value];
116
- throw new Error(`unknown font type "${typeof value}"`);
121
+ throw new Error(
122
+ `decorator.processFont: Expected a font name string, but got ${typeof value}. Value: ${JSON.stringify(value)}`
123
+ );
117
124
  }
118
125
 
119
126
  function processExpression(