markuplint 5.1.0 → 5.1.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/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [5.1.1](https://github.com/markuplint/markuplint/compare/v5.1.0...v5.1.1) (2026-10-09)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **markuplint:** fail a file whose config cannot be loaded instead of passing it ([4579d89](https://github.com/markuplint/markuplint/commit/4579d890e4b3e18f34fbe91f8b8d7f59ca886e92))
11
+
6
12
  # [5.1.0](https://github.com/markuplint/markuplint/compare/v5.0.1...v5.1.0) (2026-10-08)
7
13
 
8
14
  ### Bug Fixes
@@ -1,9 +1,9 @@
1
1
  import { ConfigProvider, createPretenderResolver, disambiguatePretendersForFile, invalidatePretenderResolutionCaches, resolveFiles, resolveParser, resolveRules, resolveSpecs, } from '@markuplint/file-resolver';
2
2
  import path from 'node:path';
3
3
  import { applyRuleAliasesToConfig, mergeConfig } from '@markuplint/ml-config';
4
- import { MLCore, convertRuleset } from '@markuplint/ml-core';
4
+ import { CONFIG_ERROR_RULE_ID, MLCore, convertRuleset } from '@markuplint/ml-core';
5
5
  import { ruleAliasTable } from '@markuplint/rules';
6
- import { isFatalError } from '@markuplint/shared';
6
+ import { ConfigLoadError, isFatalError } from '@markuplint/shared';
7
7
  import { Emitter } from 'strict-event-emitter';
8
8
  import { log as coreLog, verbosely } from '../debug.js';
9
9
  import { i18n } from '../i18n.js';
@@ -71,6 +71,15 @@ export class MLEngine extends Emitter {
71
71
  return files[0];
72
72
  }
73
73
  #configProvider;
74
+ /**
75
+ * Why the latest config resolution left nothing to lint with: a config that
76
+ * cannot be loaded (Tier 2). `null` while the latest resolution succeeded.
77
+ *
78
+ * Set by `#provide` and reported by `#reportConfigFailure` as one
79
+ * error-severity `config-error` for the file. It is not folded into an empty
80
+ * config plus a warning: a file that no rule has looked at must not pass.
81
+ */
82
+ #configFailure = null;
74
83
  #core = null;
75
84
  #file;
76
85
  #options;
@@ -168,6 +177,10 @@ export class MLEngine extends Emitter {
168
177
  log('exec: start');
169
178
  const core = await this.#setup();
170
179
  if (!core) {
180
+ if (this.#configFailure) {
181
+ log('exec: config cannot be loaded');
182
+ return this.#reportConfigFailure(this.#configFailure);
183
+ }
171
184
  log('exec: cancel (unsetuped yet)');
172
185
  return null;
173
186
  }
@@ -394,14 +407,32 @@ export class MLEngine extends Emitter {
394
407
  return;
395
408
  }
396
409
  const fabric = await this.#provide(false);
397
- if (!fabric || !this.#options?.watch) {
410
+ if (!fabric) {
411
+ if (this.#configFailure) {
412
+ // The core holds the config that no longer loads. Dropping it makes the
413
+ // next `exec()` / `setCode()` resolve again instead of linting on it.
414
+ this.#core = null;
415
+ if (this.#options?.watch) {
416
+ await this.#reportConfigFailure(this.#configFailure);
417
+ }
418
+ }
419
+ return;
420
+ }
421
+ if (!this.#options?.watch) {
398
422
  return;
399
423
  }
400
424
  if (fabric.configErrors) {
401
425
  this.emit('config-errors', this.#file.path, fabric.configErrors);
402
426
  }
403
427
  this.emit('log', 'update:core', this.#file.path);
404
- this.#core?.update(fabric);
428
+ if (this.#core) {
429
+ this.#core.update(fabric);
430
+ }
431
+ else {
432
+ // No core yet — the config did not load until now. `#exec()` would
433
+ // resolve once more through `#setup()`, for the fabric just resolved.
434
+ await this.#createCore(fabric);
435
+ }
405
436
  await this.#exec();
406
437
  }
407
438
  #enqueue(task) {
@@ -464,32 +495,42 @@ export class MLEngine extends Emitter {
464
495
  this.#pretenderDependencies = dependencies;
465
496
  await this.#syncWatch();
466
497
  }
498
+ /**
499
+ * Resolves everything a lint of the file needs and returns it, or `null`
500
+ * when there is nothing to lint with: the file is missing, excluded, or
501
+ * unmatched by the extension — or its config cannot be loaded, which
502
+ * `#configFailure` records.
503
+ *
504
+ * A config that cannot be loaded is a Tier 2 failure (see the module JSDoc
505
+ * of `@markuplint/shared`'s `errors/index.ts`): any non-fatal error thrown by
506
+ * the resolution, or a `ConfigLoadError` collected into the config set's
507
+ * `errs`, fails the file. Tier 1 errors — markuplint's own bugs
508
+ * (`isFatalError()`) — are not caught here. The other `errs` (a rule or a
509
+ * plugin that is not found, an invalid selector, a circular `extends`)
510
+ * leave the rest of the config usable, so they stay `config-error` warnings
511
+ * that `MLCore.verify()` reports.
512
+ */
467
513
  async #provide(cache = true) {
514
+ this.#configFailure = null;
468
515
  let configSet;
469
516
  try {
470
517
  configSet = await this.resolveConfig(cache);
471
518
  }
472
519
  catch (error) {
473
- if (error instanceof Error) {
474
- configSet = {
475
- config: {},
476
- plugins: [],
477
- files: new Set(),
478
- errs: [error],
479
- ruleDeprecations: [],
480
- };
481
- }
482
- else {
520
+ if (isFatalError(error) || !(error instanceof Error)) {
483
521
  throw error;
484
522
  }
523
+ return this.#failConfig(error);
485
524
  }
486
525
  fileLog('Fetched Config files: %O', configSet.files);
487
526
  fileLog('Resolved Config: %O', configSet.config);
488
527
  fileLog('Resolved Plugins: %O', configSet.plugins);
489
528
  fileLog('Resolve Errors: %O', configSet.errs);
490
- if (!(await this.#file.isFile())) {
491
- this.emit('log', 'file-no-exists', `The file doesn't exist or it is not a file: ${this.#file.path}`);
492
- fileLog("The file doesn't exist or it is not a file: %s", this.#file.path);
529
+ const loadError = configSet.errs.find(error => error instanceof ConfigLoadError);
530
+ if (loadError) {
531
+ return this.#failConfig(loadError);
532
+ }
533
+ if (!(await this.#isTargetFile())) {
493
534
  return null;
494
535
  }
495
536
  // Exclude
@@ -698,6 +739,58 @@ export class MLEngine extends Emitter {
698
739
  this.emit('schemas', this.#file.path, schemas);
699
740
  return schemas;
700
741
  }
742
+ async #isTargetFile() {
743
+ if (await this.#file.isFile()) {
744
+ return true;
745
+ }
746
+ this.emit('log', 'file-no-exists', `The file doesn't exist or it is not a file: ${this.#file.path}`);
747
+ fileLog("The file doesn't exist or it is not a file: %s", this.#file.path);
748
+ return false;
749
+ }
750
+ /**
751
+ * Records that the config cannot be loaded, unless the file is missing too,
752
+ * which is the more basic reason to skip it. Always resolves to `null`: the
753
+ * "nothing to lint with" answer of `#provide`.
754
+ */
755
+ async #failConfig(error) {
756
+ fileLog('Config cannot be loaded: %O', error);
757
+ if (await this.#isTargetFile()) {
758
+ this.#configFailure = error;
759
+ }
760
+ return null;
761
+ }
762
+ /**
763
+ * Reports a config that cannot be loaded as the file's only violation, at
764
+ * error severity, so the run fails instead of passing a file no rule has
765
+ * checked.
766
+ *
767
+ * Emits `lint` as well as `lint-error`: an editor shows its diagnostics from
768
+ * `lint`, and a config that stops loading while it is being edited must show
769
+ * up there. See `#exec` for the `lint-error` conversion of a failed
770
+ * `verify()`, which has no violations to publish.
771
+ */
772
+ async #reportConfigFailure(error) {
773
+ const sourceCode = await this.#file.getCode();
774
+ const violations = [
775
+ {
776
+ ruleId: CONFIG_ERROR_RULE_ID,
777
+ severity: 'error',
778
+ message: error.message,
779
+ line: 1,
780
+ col: 1,
781
+ raw: '',
782
+ },
783
+ ];
784
+ this.emit('lint-error', this.#file.path, sourceCode, error);
785
+ this.emit('lint', this.#file.path, sourceCode, violations, sourceCode, null, null);
786
+ return {
787
+ violations,
788
+ filePath: this.#file.path,
789
+ sourceCode,
790
+ fixedCode: sourceCode,
791
+ status: 'processed',
792
+ };
793
+ }
701
794
  async #setup() {
702
795
  if (this.#core) {
703
796
  return this.#core;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markuplint",
3
- "version": "5.1.0",
3
+ "version": "5.1.1",
4
4
  "description": "An HTML linter for all markup developers",
5
5
  "author": "Yusuke Hirao",
6
6
  "license": "MIT",
@@ -35,17 +35,17 @@
35
35
  "clean": "tsc --build --clean tsconfig.build.json"
36
36
  },
37
37
  "dependencies": {
38
- "@markuplint/cli-utils": "5.1.0",
39
- "@markuplint/file-resolver": "5.1.0",
40
- "@markuplint/html-parser": "5.1.0",
41
- "@markuplint/html-spec": "5.1.0",
42
- "@markuplint/i18n": "5.1.0",
43
- "@markuplint/ml-ast": "5.1.0",
44
- "@markuplint/ml-config": "5.1.0",
45
- "@markuplint/ml-core": "5.1.0",
46
- "@markuplint/ml-spec": "5.1.0",
47
- "@markuplint/rules": "5.1.0",
48
- "@markuplint/shared": "5.1.0",
38
+ "@markuplint/cli-utils": "5.1.1",
39
+ "@markuplint/file-resolver": "5.1.1",
40
+ "@markuplint/html-parser": "5.1.1",
41
+ "@markuplint/html-spec": "5.1.1",
42
+ "@markuplint/i18n": "5.1.1",
43
+ "@markuplint/ml-ast": "5.1.1",
44
+ "@markuplint/ml-config": "5.1.1",
45
+ "@markuplint/ml-core": "5.1.1",
46
+ "@markuplint/ml-spec": "5.1.1",
47
+ "@markuplint/rules": "5.1.1",
48
+ "@markuplint/shared": "5.1.1",
49
49
  "@types/debug": "4.1.13",
50
50
  "chokidar": "5.0.0",
51
51
  "debug": "4.4.3",
@@ -55,5 +55,5 @@
55
55
  "strip-ansi": "7.2.0",
56
56
  "type-fest": "5.6.0"
57
57
  },
58
- "gitHead": "3a061784a8211034ad0c1cbd64798c0213414f65"
58
+ "gitHead": "a940c8526e971d590d97395ce924910221208820"
59
59
  }