tileserver-gl-light 5.5.0-pre.4 → 5.5.0-pre.6

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.
@@ -531,30 +531,43 @@ async function respondImage(
531
531
  pool = item.map.renderersStatic[scale];
532
532
  }
533
533
 
534
+ if (!pool) {
535
+ console.error(`Pool not found for scale ${scale}, mode ${mode}`);
536
+ return res.status(500).send('Renderer pool not configured');
537
+ }
538
+
534
539
  pool.acquire(async (err, renderer) => {
535
540
  // Check if pool.acquire failed or returned null/invalid renderer
536
541
  if (err) {
537
542
  console.error('Failed to acquire renderer from pool:', err);
538
- return res.status(503).send('Renderer pool error');
543
+ if (!res.headersSent) {
544
+ return res.status(503).send('Renderer pool error');
545
+ }
546
+ return;
539
547
  }
540
548
 
541
549
  if (!renderer) {
542
550
  console.error(
543
551
  'Renderer is null - likely crashed or failed to initialize',
544
552
  );
545
- return res.status(503).send('Renderer unavailable');
553
+ if (!res.headersSent) {
554
+ return res.status(503).send('Renderer unavailable');
555
+ }
556
+ return;
546
557
  }
547
558
 
548
559
  // Validate renderer has required methods (basic health check)
549
560
  if (typeof renderer.render !== 'function') {
550
561
  console.error('Renderer is invalid - missing render method');
551
- // Destroy the bad renderer and remove from pool
552
562
  try {
553
- pool.destroy(renderer);
563
+ pool.removeBadObject(renderer);
554
564
  } catch (e) {
555
- console.error('Error destroying invalid renderer:', e);
565
+ console.error('Error removing bad renderer:', e);
566
+ }
567
+ if (!res.headersSent) {
568
+ return res.status(503).send('Renderer invalid');
556
569
  }
557
- return res.status(503).send('Renderer invalid');
570
+ return;
558
571
  }
559
572
 
560
573
  // For 512px tiles, use the actual maplibre-native zoom. For 256px tiles, use zoom - 1
@@ -588,13 +601,14 @@ async function respondImage(
588
601
 
589
602
  // Set a timeout for the render operation to detect hung renderers
590
603
  const renderTimeout = setTimeout(() => {
591
- console.error('Renderer timeout - destroying potentially hung renderer');
604
+ console.error('Renderer timeout - destroying hung renderer');
605
+
592
606
  try {
593
- // Don't release back to pool, destroy it
594
- pool.destroy(renderer);
607
+ pool.removeBadObject(renderer);
595
608
  } catch (e) {
596
- console.error('Error destroying timed-out renderer:', e);
609
+ console.error('Error removing timed-out renderer:', e);
597
610
  }
611
+
598
612
  if (!res.headersSent) {
599
613
  res.status(503).send('Renderer timeout');
600
614
  }
@@ -604,13 +618,17 @@ async function respondImage(
604
618
  renderer.render(params, (err, data) => {
605
619
  clearTimeout(renderTimeout);
606
620
 
621
+ if (res.headersSent) {
622
+ // Timeout already fired and sent response, don't process
623
+ return;
624
+ }
625
+
607
626
  if (err) {
608
627
  console.error('Render error:', err);
609
- // Destroy renderer instead of releasing it back to pool since it may be corrupted
610
628
  try {
611
- pool.destroy(renderer);
629
+ pool.removeBadObject(renderer);
612
630
  } catch (e) {
613
- console.error('Error destroying failed renderer:', e);
631
+ console.error('Error removing failed renderer:', e);
614
632
  }
615
633
  if (!res.headersSent) {
616
634
  return res
@@ -646,12 +664,11 @@ async function respondImage(
646
664
  height: height * scale,
647
665
  });
648
666
  }
649
- // HACK(Part 2) 256px tiles are a zoom level lower than maplibre-native default tiles. this hack allows tileserver-gl to support zoom 0 256px tiles, which would actually be zoom -1 in maplibre-native. Since zoom -1 isn't supported, a double sized zoom 0 tile is requested and resized here.
650
667
 
668
+ // HACK(Part 2) 256px tiles are a zoom level lower than maplibre-native default tiles. this hack allows tileserver-gl to support zoom 0 256px tiles, which would actually be zoom -1 in maplibre-native. Since zoom -1 isn't supported, a double sized zoom 0 tile is requested and resized here.
651
669
  if (z === 0 && width === 256) {
652
670
  image.resize(width * scale, height * scale);
653
671
  }
654
- // END HACK(Part 2)
655
672
 
656
673
  const composites = [];
657
674
  if (overlay) {
@@ -659,7 +676,6 @@ async function respondImage(
659
676
  }
660
677
  if (item.watermark) {
661
678
  const canvas = renderWatermark(width, height, scale, item.watermark);
662
-
663
679
  composites.push({ input: canvas.toBuffer() });
664
680
  }
665
681
 
@@ -670,7 +686,6 @@ async function respondImage(
670
686
  scale,
671
687
  item.staticAttributionText,
672
688
  );
673
-
674
689
  composites.push({ input: canvas.toBuffer() });
675
690
  }
676
691
 
@@ -687,7 +702,6 @@ async function respondImage(
687
702
  }
688
703
  // eslint-disable-next-line security/detect-object-injection -- format is validated above
689
704
  const formatQuality = formatQualities[format];
690
-
691
705
  // eslint-disable-next-line security/detect-object-injection -- format is validated above
692
706
  const formatOptions = (options.formatOptions || {})[format] || {};
693
707
 
@@ -710,25 +724,32 @@ async function respondImage(
710
724
  } else if (format === 'webp') {
711
725
  image.webp({ quality: formatOptions.quality || formatQuality || 90 });
712
726
  }
727
+
713
728
  image.toBuffer((err, buffer, info) => {
714
- if (!buffer) {
715
- return res.status(404).send('Not found');
729
+ if (err || !buffer) {
730
+ console.error('Sharp error:', err);
731
+ if (!res.headersSent) {
732
+ return res.status(500).send('Image processing failed');
733
+ }
734
+ return;
716
735
  }
717
736
 
718
- res.set({
719
- 'Last-Modified': item.lastModified,
720
- 'Content-Type': `image/${format}`,
721
- });
722
- return res.status(200).send(buffer);
737
+ if (!res.headersSent) {
738
+ res.set({
739
+ 'Last-Modified': item.lastModified,
740
+ 'Content-Type': `image/${format}`,
741
+ });
742
+ return res.status(200).send(buffer);
743
+ }
723
744
  });
724
745
  });
725
746
  } catch (error) {
726
747
  clearTimeout(renderTimeout);
727
748
  console.error('Unexpected error during render:', error);
728
749
  try {
729
- pool.destroy(renderer);
750
+ pool.removeBadObject(renderer);
730
751
  } catch (e) {
731
- console.error('Error destroying renderer after error:', e);
752
+ console.error('Error removing renderer after error:', e);
732
753
  }
733
754
  if (!res.headersSent) {
734
755
  return res.status(500).send('Render failed');
@@ -1073,7 +1094,7 @@ export const serve_rendered = {
1073
1094
  (!p1 && p2 === 'static') || (p1 === 'static' && p2 === 'raw')
1074
1095
  ? 'static'
1075
1096
  : 'tile';
1076
- if (verbose) {
1097
+ if (verbose >= 3) {
1077
1098
  console.log(
1078
1099
  `Handling rendered %s request for: /styles/%s%s/%s/%s/%s%s.%s`,
1079
1100
  requestType,
@@ -1133,7 +1154,7 @@ export const serve_rendered = {
1133
1154
  return res.sendStatus(404);
1134
1155
  }
1135
1156
  const tileSize = parseInt(req.params.tileSize, 10) || undefined;
1136
- if (verbose) {
1157
+ if (verbose >= 3) {
1137
1158
  console.log(
1138
1159
  `Handling rendered tilejson request for: /styles/%s%s.json`,
1139
1160
  req.params.tileSize
@@ -1184,11 +1205,16 @@ export const serve_rendered = {
1184
1205
  renderersStatic: [],
1185
1206
  sources: {},
1186
1207
  sourceTypes: {},
1208
+ sparseFlags: {},
1187
1209
  };
1188
1210
 
1189
- const { publicUrl, verbose } = programOpts;
1211
+ const { publicUrl, verbose, fetchTimeout } = programOpts;
1190
1212
 
1191
1213
  const styleJSON = clone(style);
1214
+
1215
+ // Global sparse flag for HTTP/remote sources (from config options)
1216
+ const globalSparse = options.sparse ?? true;
1217
+
1192
1218
  /**
1193
1219
  * Creates a pool of renderers.
1194
1220
  * @param {number} ratio Pixel ratio
@@ -1210,7 +1236,7 @@ export const serve_rendered = {
1210
1236
  ratio,
1211
1237
  request: async (req, callback) => {
1212
1238
  const protocol = req.url.split(':')[0];
1213
- if (verbose && verbose >= 3) {
1239
+ if (verbose >= 3) {
1214
1240
  console.log('Handling request:', req);
1215
1241
  }
1216
1242
  if (protocol === 'sprites') {
@@ -1266,22 +1292,18 @@ export const serve_rendered = {
1266
1292
  x,
1267
1293
  y,
1268
1294
  );
1269
- if (fetchTile == null && sourceInfo.sparse == true) {
1270
- if (verbose) {
1271
- console.log(
1272
- 'fetchTile warning on %s, sparse response',
1273
- req.url,
1274
- );
1295
+ if (fetchTile == null) {
1296
+ if (verbose >= 2) {
1297
+ console.log('fetchTile null on %s', req.url);
1275
1298
  }
1276
- callback();
1277
- return;
1278
- } else if (fetchTile == null) {
1279
- if (verbose) {
1280
- console.log(
1281
- 'fetchTile error on %s, serving empty response',
1282
- req.url,
1283
- );
1299
+ // eslint-disable-next-line security/detect-object-injection -- sourceId from internal style source names
1300
+ const sparse = map.sparseFlags[sourceId] ?? true;
1301
+ // sparse=true (default) -> return empty callback so MapLibre can overzoom
1302
+ if (sparse) {
1303
+ callback();
1304
+ return;
1284
1305
  }
1306
+ // sparse=false -> 204 (empty tile, no overzoom) - create blank response
1285
1307
  createEmptyResponse(
1286
1308
  sourceInfo.format,
1287
1309
  sourceInfo.color,
@@ -1320,40 +1342,58 @@ export const serve_rendered = {
1320
1342
 
1321
1343
  callback(null, response);
1322
1344
  } else if (protocol === 'http' || protocol === 'https') {
1323
- try {
1324
- // Add timeout to prevent hanging on unreachable hosts
1325
- const controller = new AbortController();
1326
- const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
1345
+ const controller = new AbortController();
1346
+ const timeoutMs = (fetchTimeout && Number(fetchTimeout)) || 15000;
1347
+ let timeoutId;
1327
1348
 
1349
+ try {
1350
+ timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1328
1351
  const response = await fetch(req.url, {
1329
1352
  signal: controller.signal,
1330
1353
  });
1331
-
1332
1354
  clearTimeout(timeoutId);
1333
1355
 
1334
- // Handle 410 Gone as sparse response
1335
- if (response.status === 410) {
1336
- if (verbose) {
1356
+ // HTTP 204 No Content means "empty tile" - generate a blank tile
1357
+ if (response.status === 204) {
1358
+ const parts = url.parse(req.url);
1359
+ const extension = path.extname(parts.pathname).toLowerCase();
1360
+ // eslint-disable-next-line security/detect-object-injection -- extension is from path.extname, limited set
1361
+ const format = extensionToFormat[extension] || '';
1362
+ createEmptyResponse(format, '', callback);
1363
+ return;
1364
+ }
1365
+
1366
+ if (!response.ok) {
1367
+ if (verbose >= 2) {
1337
1368
  console.log(
1338
- 'fetchTile warning on %s, sparse response due to 410 Gone',
1369
+ 'fetchTile HTTP %d on %s, %s',
1370
+ response.status,
1339
1371
  req.url,
1372
+ globalSparse
1373
+ ? 'allowing overzoom'
1374
+ : 'creating empty tile',
1340
1375
  );
1341
1376
  }
1342
- callback();
1343
- return;
1344
- }
1345
1377
 
1346
- // Check for other non-ok responses
1347
- if (!response.ok) {
1348
- throw new Error(
1349
- `HTTP ${response.status}: ${response.statusText}`,
1350
- );
1378
+ if (globalSparse) {
1379
+ // sparse=true -> allow overzoom
1380
+ callback();
1381
+ return;
1382
+ }
1383
+
1384
+ // sparse=false (default) -> create empty tile
1385
+ const parts = url.parse(req.url);
1386
+ const extension = path.extname(parts.pathname).toLowerCase();
1387
+ // eslint-disable-next-line security/detect-object-injection -- extension is from path.extname, limited set
1388
+ const format = extensionToFormat[extension] || '';
1389
+ createEmptyResponse(format, '', callback);
1390
+ return;
1351
1391
  }
1352
1392
 
1353
1393
  const responseHeaders = response.headers;
1354
1394
  const responseData = await response.arrayBuffer();
1355
-
1356
1395
  const parsedResponse = {};
1396
+
1357
1397
  if (responseHeaders.get('last-modified')) {
1358
1398
  parsedResponse.modified = new Date(
1359
1399
  responseHeaders.get('last-modified'),
@@ -1371,8 +1411,7 @@ export const serve_rendered = {
1371
1411
  parsedResponse.data = Buffer.from(responseData);
1372
1412
  callback(null, parsedResponse);
1373
1413
  } catch (error) {
1374
- // Log DNS failures more prominently as they often indicate config issues
1375
- // Native fetch wraps DNS errors in error.cause
1414
+ // Log DNS failures
1376
1415
  if (error.cause?.code === 'ENOTFOUND') {
1377
1416
  console.error(
1378
1417
  `DNS RESOLUTION FAILED for ${req.url}. ` +
@@ -1381,20 +1420,27 @@ export const serve_rendered = {
1381
1420
  );
1382
1421
  }
1383
1422
 
1384
- // Handle AbortController timeout
1423
+ // Log timeout
1385
1424
  if (error.name === 'AbortError') {
1386
1425
  console.error(
1387
1426
  `FETCH TIMEOUT for ${req.url}. ` +
1388
- `The request took longer than 10 seconds to complete.`,
1427
+ `The request took longer than ${timeoutMs} ms to complete.`,
1389
1428
  );
1390
1429
  }
1391
1430
 
1392
- // For all other errors (e.g., network errors, 404, 500, etc.) return empty content.
1431
+ // Log all other errors
1393
1432
  console.error(
1394
1433
  `Error fetching remote URL ${req.url}:`,
1395
1434
  error.message || error,
1396
1435
  );
1397
1436
 
1437
+ if (globalSparse) {
1438
+ // sparse=true -> allow overzoom
1439
+ callback();
1440
+ return;
1441
+ }
1442
+
1443
+ // sparse=false (default) -> create empty tile
1398
1444
  const parts = url.parse(req.url);
1399
1445
  const extension = path.extname(parts.pathname).toLowerCase();
1400
1446
  // eslint-disable-next-line security/detect-object-injection -- extension is from path.extname, limited set
@@ -1476,7 +1522,7 @@ export const serve_rendered = {
1476
1522
 
1477
1523
  // Remove (flatten) 3D buildings
1478
1524
  if (layer.paint['fill-extrusion-height']) {
1479
- if (verbose) {
1525
+ if (verbose >= 1) {
1480
1526
  console.warn(
1481
1527
  `Warning: Layer '${layerIdForWarning}' in style '${id}' has property 'fill-extrusion-height'. ` +
1482
1528
  `3D extrusion may appear distorted or misleading when rendered as a static image due to camera angle limitations. ` +
@@ -1487,7 +1533,7 @@ export const serve_rendered = {
1487
1533
  layer.paint['fill-extrusion-height'] = 0;
1488
1534
  }
1489
1535
  if (layer.paint['fill-extrusion-base']) {
1490
- if (verbose) {
1536
+ if (verbose >= 1) {
1491
1537
  console.warn(
1492
1538
  `Warning: Layer '${layerIdForWarning}' in style '${id}' has property 'fill-extrusion-base'. ` +
1493
1539
  `3D extrusion may appear distorted or misleading when rendered as a static image due to camera angle limitations. ` +
@@ -1507,7 +1553,7 @@ export const serve_rendered = {
1507
1553
 
1508
1554
  for (const prop of hillshadePropertiesToRemove) {
1509
1555
  if (prop in layer.paint) {
1510
- if (verbose) {
1556
+ if (verbose >= 1) {
1511
1557
  console.warn(
1512
1558
  `Warning: Layer '${layerIdForWarning}' in style '${id}' has property '${prop}'. ` +
1513
1559
  `This property is not supported by MapLibre Native. ` +
@@ -1522,7 +1568,7 @@ export const serve_rendered = {
1522
1568
 
1523
1569
  // --- Remove 'hillshade-shadow-color' if it is an array. It can only be a string in MapLibre Native ---
1524
1570
  if (Array.isArray(layer.paint['hillshade-shadow-color'])) {
1525
- if (verbose) {
1571
+ if (verbose >= 1) {
1526
1572
  console.warn(
1527
1573
  `Warning: Layer '${layerIdForWarning}' in style '${id}' has property 'hillshade-shadow-color'. ` +
1528
1574
  `An array value is not supported by MapLibre Native for this property (expected string/color). ` +
@@ -1568,7 +1614,6 @@ export const serve_rendered = {
1568
1614
 
1569
1615
  for (const name of Object.keys(styleJSON.sources)) {
1570
1616
  let sourceType;
1571
- let sparse;
1572
1617
  // eslint-disable-next-line security/detect-object-injection -- name is from style sources object keys
1573
1618
  let source = styleJSON.sources[name];
1574
1619
  let url = source.url;
@@ -1599,7 +1644,6 @@ export const serve_rendered = {
1599
1644
  if (dataInfo.inputFile) {
1600
1645
  inputFile = dataInfo.inputFile;
1601
1646
  sourceType = dataInfo.fileType;
1602
- sparse = dataInfo.sparse;
1603
1647
  s3Profile = dataInfo.s3Profile;
1604
1648
  requestPayer = dataInfo.requestPayer;
1605
1649
  s3Region = dataInfo.s3Region;
@@ -1643,7 +1687,6 @@ export const serve_rendered = {
1643
1687
  const type = source.type;
1644
1688
  Object.assign(source, metadata);
1645
1689
  source.type = type;
1646
- source.sparse = sparse;
1647
1690
  source.tiles = [
1648
1691
  // meta url which will be detected when requested
1649
1692
  `pmtiles://${name}/{z}/{x}/{y}.${metadata.format || 'pbf'}`,
@@ -1662,6 +1705,13 @@ export const serve_rendered = {
1662
1705
  tileJSON.attribution += source.attribution;
1663
1706
  }
1664
1707
  }
1708
+
1709
+ // Set sparse flag: user config overrides format-based default
1710
+ // Vector tiles (pbf) default to false (204), raster tiles default to true (404)
1711
+ const isVector = metadata.format === 'pbf';
1712
+ // eslint-disable-next-line security/detect-object-injection -- name is from style sources object keys
1713
+ map.sparseFlags[name] =
1714
+ dataInfo.sparse ?? options.sparse ?? !isVector;
1665
1715
  } else {
1666
1716
  // MBTiles does not support remote URLs
1667
1717
 
@@ -1687,7 +1737,6 @@ export const serve_rendered = {
1687
1737
  const type = source.type;
1688
1738
  Object.assign(source, info);
1689
1739
  source.type = type;
1690
- source.sparse = sparse;
1691
1740
  source.tiles = [
1692
1741
  // meta url which will be detected when requested
1693
1742
  `mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
@@ -1710,6 +1759,13 @@ export const serve_rendered = {
1710
1759
  tileJSON.attribution += source.attribution;
1711
1760
  }
1712
1761
  }
1762
+
1763
+ // Set sparse flag: user config overrides format-based default
1764
+ // Vector tiles (pbf) default to false (204), raster tiles default to true (404)
1765
+ const isVector = info.format === 'pbf';
1766
+ // eslint-disable-next-line security/detect-object-injection -- name is from style sources object keys
1767
+ map.sparseFlags[name] =
1768
+ dataInfo.sparse ?? options.sparse ?? !isVector;
1713
1769
  }
1714
1770
  }
1715
1771
  }
@@ -35,7 +35,7 @@ export const serve_style = {
35
35
  */
36
36
  app.get('/:id/style.json', (req, res, next) => {
37
37
  const { id } = req.params;
38
- if (verbose) {
38
+ if (verbose >= 1) {
39
39
  console.log(
40
40
  'Handling style request for: /styles/%s/style.json',
41
41
  String(id).replace(/\n|\r/g, ''),
@@ -95,7 +95,7 @@ export const serve_style = {
95
95
  const sanitizedFormat = format
96
96
  ? '.' + String(format).replace(/\n|\r/g, '')
97
97
  : '';
98
- if (verbose) {
98
+ if (verbose >= 1) {
99
99
  console.log(
100
100
  `Handling sprite request for: /styles/%s/sprite/%s%s%s`,
101
101
  sanitizedId,
@@ -108,7 +108,7 @@ export const serve_style = {
108
108
  const item = repo[id];
109
109
  const validatedFormat = allowedSpriteFormats(format);
110
110
  if (!item || !validatedFormat) {
111
- if (verbose)
111
+ if (verbose >= 1)
112
112
  console.error(
113
113
  `Sprite item or format not found for: /styles/%s/sprite/%s%s%s`,
114
114
  sanitizedId,
@@ -123,7 +123,7 @@ export const serve_style = {
123
123
  );
124
124
  const spriteScale = allowedSpriteScales(scale);
125
125
  if (!sprite || spriteScale === null) {
126
- if (verbose)
126
+ if (verbose >= 1)
127
127
  console.error(
128
128
  `Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`,
129
129
  sanitizedId,
@@ -147,7 +147,7 @@ export const serve_style = {
147
147
 
148
148
  const sanitizedSpritePath = sprite.path.replace(/^(\.\.\/)+/, '');
149
149
  const filename = `${sanitizedSpritePath}${spriteScale}.${validatedFormat}`;
150
- if (verbose) console.log(`Loading sprite from: %s`, filename);
150
+ if (verbose >= 1) console.log(`Loading sprite from: %s`, filename);
151
151
  try {
152
152
  const data = await readFile(filename);
153
153
 
@@ -156,7 +156,7 @@ export const serve_style = {
156
156
  } else if (validatedFormat === 'png') {
157
157
  res.header('Content-type', 'image/png');
158
158
  }
159
- if (verbose)
159
+ if (verbose >= 1)
160
160
  console.log(
161
161
  `Responding with sprite data for /styles/%s/sprite/%s%s%s`,
162
162
  sanitizedId,
@@ -167,7 +167,7 @@ export const serve_style = {
167
167
  res.set({ 'Last-Modified': item.lastModified });
168
168
  return res.send(data);
169
169
  } catch (err) {
170
- if (verbose) {
170
+ if (verbose >= 1) {
171
171
  console.error(
172
172
  'Sprite load error: %s, Error: %s',
173
173
  filename,
@@ -217,7 +217,28 @@ export const serve_style = {
217
217
  const styleFile = path.resolve(options.paths.styles, params.style);
218
218
  const styleJSON = clone(style);
219
219
 
220
- const validationErrors = validateStyleMin(styleJSON);
220
+ // Sanitize style for validation: remove non-spec properties (e.g., 'sparse')
221
+ // so that validateStyleMin doesn't reject valid styles containing our custom flags.
222
+ const styleForValidation = clone(styleJSON);
223
+ if (styleForValidation.sources) {
224
+ for (const name of Object.keys(styleForValidation.sources)) {
225
+ if (
226
+ // eslint-disable-next-line security/detect-object-injection -- name is from Object.keys of styleForValidation.sources
227
+ styleForValidation.sources[name] &&
228
+ // eslint-disable-next-line security/detect-object-injection -- name is from Object.keys of styleForValidation.sources
229
+ 'sparse' in styleForValidation.sources[name]
230
+ ) {
231
+ try {
232
+ // eslint-disable-next-line security/detect-object-injection -- name is from Object.keys of styleForValidation.sources
233
+ delete styleForValidation.sources[name].sparse;
234
+ } catch (_err) {
235
+ // ignore any deletion errors and continue validation
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ const validationErrors = validateStyleMin(styleForValidation);
221
242
  if (validationErrors.length > 0) {
222
243
  console.log(`The file "${params.style}" is not a valid style file:`);
223
244
  for (const err of validationErrors) {