@markuplint/vue-parser 5.2.0 → 5.3.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/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
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.3.0](https://github.com/markuplint/markuplint/compare/v5.2.0...v5.3.0) (2026-10-21)
7
+
8
+ ### Bug Fixes
9
+
10
+ * make published packages loadable from require() and put types first ([723106f](https://github.com/markuplint/markuplint/commit/723106f51aac4e60dc02db2c1a894617368b0253)), closes [#4198](https://github.com/markuplint/markuplint/issues/4198)
11
+ * **vue-parser:** do not report a root that is neither a component nor an element ([325825f](https://github.com/markuplint/markuplint/commit/325825ffa5a3e7d23f4b38fc1d5d80cf9fbeeacc))
12
+ * **vue-parser:** take every tag that Vue resolves to a component for one ([0e79724](https://github.com/markuplint/markuplint/commit/0e79724b292344c0cf2dc0ca82d173d0e0add462))
13
+
14
+ ### Features
15
+
16
+ * **parser-utils:** let a parser say how an attribute name gets into the DOM ([abd81e5](https://github.com/markuplint/markuplint/commit/abd81e5d20f4ffeb0ba878e77fd4122c1a803a9f))
17
+ * **vue-parser:** report the lang of the script setup block from the component scanner ([45a8273](https://github.com/markuplint/markuplint/commit/45a827388329cc4beb8c245e23c500d267648412))
18
+
19
+
6
20
  # [5.2.0](https://github.com/markuplint/markuplint/compare/v5.1.1...v5.2.0) (2026-10-13)
7
21
 
8
22
  ### Bug Fixes
@@ -58,6 +58,12 @@ export interface ComponentScanSlotWrapper {
58
58
  export interface ComponentScanScriptSource {
59
59
  readonly content: string;
60
60
  readonly offset: number;
61
+ /**
62
+ * The `lang` attribute of the block as written (`tsx`, `ts`). The language a block is written in is
63
+ * the block's own declaration (https://vuejs.org/api/sfc-spec.html#pre-processors), which decides
64
+ * whether the script holds JSX. Absent when the block has none.
65
+ */
66
+ readonly lang?: string;
61
67
  }
62
68
  /**
63
69
  * Component scanner for Vue SFC files.
@@ -5,6 +5,14 @@ function extractComponentInfo(doc) {
5
5
  if (!root) {
6
6
  return null;
7
7
  }
8
+ // The parser says the root is authored, yet its name is not the one of a component
9
+ // (`component`, `slot` of Vue): it renders what it is given, so there is no element
10
+ // to pretend to be. `pretenders` would otherwise emit that name as the target, and
11
+ // nothing tells it from the name of an element. What a component's name is, is the
12
+ // parser's rule (`Parser#isComponentName`).
13
+ if (root.elementType === 'authored' && !parser.isComponentName(root.nodeName)) {
14
+ return null;
15
+ }
8
16
  const spreads = extractSpreads(root);
9
17
  return {
10
18
  rootElement: root.nodeName,
@@ -255,11 +263,29 @@ function extractVueScriptSetup(source) {
255
263
  if (!endMatch) {
256
264
  return null;
257
265
  }
266
+ const lang = langOf(startTag);
258
267
  return {
259
268
  content: remaining.slice(0, endMatch.index),
260
269
  offset: contentStart,
270
+ ...(lang ? { lang } : {}),
261
271
  };
262
272
  }
273
+ /**
274
+ * The value of the `lang` attribute of a start tag. The attributes are read one by one, so that
275
+ * ` lang='tsx'` inside the quoted value of another attribute is not one.
276
+ *
277
+ * `@markuplint/pretenders` reads the `lang` of the other `<script>` block with the same function
278
+ * (`extractVueScript`), which it cannot import from here (a parser package is loaded by it
279
+ * dynamically, never imported), so a change to one is a change to the other.
280
+ */
281
+ function langOf(startTag) {
282
+ for (const [, name, double, single, bare] of startTag.matchAll(/\s([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g)) {
283
+ if (name?.toLowerCase() === 'lang') {
284
+ return double ?? single ?? bare;
285
+ }
286
+ }
287
+ return undefined;
288
+ }
263
289
  /**
264
290
  * Component scanner for Vue SFC files.
265
291
  *
package/lib/parser.d.ts CHANGED
@@ -47,11 +47,65 @@ declare class VueParser extends Parser<ASTNode, State> {
47
47
  * > Vue SFC is a compiled format so we are able to use case-sensitive tag names in it.
48
48
  * > We are also able to use `/>` to close a tag.
49
49
  *
50
+ * A tag that is not a native one is a component to Vue, however it is written: Vue resolves
51
+ * the tag `T` to a component registered as `T`, `camelize(T)` or `capitalize(camelize(T))`,
52
+ * so `<fooBar>` and `<foo-bar>` are the component `FooBar`, and `<foobar>` is not. Here
53
+ * a name is a component, `authored`, when it
54
+ *
55
+ * - is a built-in component, as written (`Transition`) or in kebab-case (`transition-group`),
56
+ * - is a special element that renders what it is given (`component`, `slot`), which is `authored`
57
+ * but is no component's name for {@link isComponentName},
58
+ * - has a capital initial (`FooBar`), a capital letter after a lowercase letter or a digit
59
+ * (`fooBar`), or a `.` (`ui.Item`, a member of a namespace import of `<script setup>`).
60
+ *
61
+ * `foo-bar` is a `web-component`: the name of a custom element is also the kebab-case of a component,
62
+ * and markuplint does not know `compilerOptions.isCustomElement` of the project. Like the others it is
63
+ * matched by `FooBar` (see `MLParser.componentNameVariants`).
64
+ *
65
+ * A camelCase name of SVG (`clipPath`, `foreignObject`) is an element: Vue's `isNativeTag` takes
66
+ * it, and `isSVGElement` is case-sensitive.
67
+ *
68
+ * The user's `authoredElementName` replaces all of this, as it replaces any default of a parser.
69
+ *
50
70
  * @see https://vuejs.org/guide/essentials/component-basics#using-a-component
51
71
  * @see https://vuejs.org/api/built-in-components.html
52
72
  * @see https://vuejs.org/api/built-in-special-elements.html#built-in-special-elements
73
+ * @see https://github.com/vuejs/core/blob/main/packages/compiler-core/src/parser.ts (`isComponent`)
53
74
  */
54
75
  detectElementType(nodeName: string): import("@markuplint/ml-ast").ElementType;
76
+ /**
77
+ * Whether Vue takes a tag name for a native element: the lists of the HTML, SVG and MathML elements
78
+ * of `@vue/shared` that `isNativeTag` of `@vue/compiler-dom` is made of. Vue resolves any other tag to a
79
+ * component (`<card>` is the component `Card`), unless `compilerOptions.isCustomElement` of the project
80
+ * says it is a custom element, which markuplint does not know.
81
+ *
82
+ * The lists are Vue's own and the names are case-sensitive, as Vue's: an obsolete element that they
83
+ * do not hold (`marquee`) is no native tag to Vue, though markuplint has a specification for it.
84
+ *
85
+ * It is `MLParser#isNativeTag`, so what Vue resolves is what the document takes for native. The type
86
+ * selector (`Marquee` stands for `<marquee>`) reads it, and so does the guard that keeps a pretender off
87
+ * a native element, as `@markuplint/pretenders` does. The rules keyed on the specification still see
88
+ * `<marquee>`.
89
+ *
90
+ * It is not the rule of {@link isComponentName}, which tells by the form of a name alone whether it is
91
+ * a component's, for the target of a pretender and the root a scanner reads: `as: 'marquee'` is meant
92
+ * as the element there.
93
+ *
94
+ * @param name - A tag name as written, case kept
95
+ * @returns Whether the name is a native tag
96
+ */
97
+ isNativeTag(name: string): boolean;
98
+ /**
99
+ * A name that Vue takes for a component (see {@link detectElementType}) and that names no special
100
+ * element: a built-in component as written or in kebab-case (`Transition`, `transition-group`), a capital
101
+ * initial (`FooBar`), a capital letter after a lowercase letter or a digit (`fooBar`), or a `.`
102
+ * (`ui.Item`). `foo-bar` is not one, as it may be a custom element, nor are `component` and `slot`, which render
103
+ * what they are given: they have no element to pretend to be.
104
+ *
105
+ * @param name - A tag name as written, case kept
106
+ * @returns Whether the name is a component's
107
+ */
108
+ isComponentName(name: string): boolean;
55
109
  }
56
110
  export declare const parser: VueParser;
57
111
  export {};
package/lib/parser.js CHANGED
@@ -1,5 +1,24 @@
1
- import { ParserError, Parser } from '@markuplint/parser-utils';
1
+ import { ParserError, Parser, isPotentialCustomElementName, isSVGElement } from '@markuplint/parser-utils';
2
+ import { isHTMLTag, isMathMLTag, isSVGTag } from '@vue/shared';
2
3
  import { vueParse } from './vue-parser/index.js';
4
+ /**
5
+ * The built-in components, as written or in kebab-case: `isBuiltInType(tag, name)` of Vue is
6
+ * `tag === name || tag === hyphenate(name)`.
7
+ *
8
+ * @see https://vuejs.org/api/built-in-components.html
9
+ */
10
+ const BUILT_IN_COMPONENTS = [
11
+ 'Transition',
12
+ 'transition',
13
+ 'TransitionGroup',
14
+ 'transition-group',
15
+ 'KeepAlive',
16
+ 'keep-alive',
17
+ 'Teleport',
18
+ 'teleport',
19
+ 'Suspense',
20
+ 'suspense',
21
+ ];
3
22
  /**
4
23
  * Directive resolution is intentionally not performed by this parser:
5
24
  * `directivePatterns` declared in `@markuplint/vue-spec` are applied later by
@@ -27,6 +46,11 @@ class VueParser extends Parser {
27
46
  // Vue SFC is a compiled format that uses explicit XML-style
28
47
  // closing tags (including `/>`), not HTML void-element rules.
29
48
  endTagType: 'xml',
49
+ // Vue resolves `<FooBar>`, `<fooBar>` and `<foo-bar>` to the same component.
50
+ componentNameVariants: true,
51
+ // The compiler of Vue makes an element by `setAttribute` unless it can put the whole static tree into
52
+ // an HTML string, which it does only for a name it knows by its exact spelling.
53
+ attrNameRoute: 'dom-api',
30
54
  }, {
31
55
  comments: [],
32
56
  });
@@ -154,24 +178,90 @@ class VueParser extends Parser {
154
178
  * > Vue SFC is a compiled format so we are able to use case-sensitive tag names in it.
155
179
  * > We are also able to use `/>` to close a tag.
156
180
  *
181
+ * A tag that is not a native one is a component to Vue, however it is written: Vue resolves
182
+ * the tag `T` to a component registered as `T`, `camelize(T)` or `capitalize(camelize(T))`,
183
+ * so `<fooBar>` and `<foo-bar>` are the component `FooBar`, and `<foobar>` is not. Here
184
+ * a name is a component, `authored`, when it
185
+ *
186
+ * - is a built-in component, as written (`Transition`) or in kebab-case (`transition-group`),
187
+ * - is a special element that renders what it is given (`component`, `slot`), which is `authored`
188
+ * but is no component's name for {@link isComponentName},
189
+ * - has a capital initial (`FooBar`), a capital letter after a lowercase letter or a digit
190
+ * (`fooBar`), or a `.` (`ui.Item`, a member of a namespace import of `<script setup>`).
191
+ *
192
+ * `foo-bar` is a `web-component`: the name of a custom element is also the kebab-case of a component,
193
+ * and markuplint does not know `compilerOptions.isCustomElement` of the project. Like the others it is
194
+ * matched by `FooBar` (see `MLParser.componentNameVariants`).
195
+ *
196
+ * A camelCase name of SVG (`clipPath`, `foreignObject`) is an element: Vue's `isNativeTag` takes
197
+ * it, and `isSVGElement` is case-sensitive.
198
+ *
199
+ * The user's `authoredElementName` replaces all of this, as it replaces any default of a parser.
200
+ *
157
201
  * @see https://vuejs.org/guide/essentials/component-basics#using-a-component
158
202
  * @see https://vuejs.org/api/built-in-components.html
159
203
  * @see https://vuejs.org/api/built-in-special-elements.html#built-in-special-elements
204
+ * @see https://github.com/vuejs/core/blob/main/packages/compiler-core/src/parser.ts (`isComponent`)
160
205
  */
161
206
  detectElementType(nodeName) {
162
207
  return super.detectElementType(nodeName, [
163
208
  // Built-in components
164
- 'Transition',
165
- 'TransitionGroup',
166
- 'KeepAlive',
167
- 'Teleport',
168
- 'Suspense',
209
+ ...BUILT_IN_COMPONENTS,
169
210
  // Special elements
170
211
  'component',
171
212
  'slot',
172
213
  // Backward compatibility
173
214
  /^[A-Z]/,
215
+ // A member of a namespace import
216
+ /\./,
217
+ // camelCase
218
+ name => /[a-z\d][A-Z]/.test(name) && !isSVGElement(name),
174
219
  ]);
175
220
  }
221
+ /**
222
+ * Whether Vue takes a tag name for a native element: the lists of the HTML, SVG and MathML elements
223
+ * of `@vue/shared` that `isNativeTag` of `@vue/compiler-dom` is made of. Vue resolves any other tag to a
224
+ * component (`<card>` is the component `Card`), unless `compilerOptions.isCustomElement` of the project
225
+ * says it is a custom element, which markuplint does not know.
226
+ *
227
+ * The lists are Vue's own and the names are case-sensitive, as Vue's: an obsolete element that they
228
+ * do not hold (`marquee`) is no native tag to Vue, though markuplint has a specification for it.
229
+ *
230
+ * It is `MLParser#isNativeTag`, so what Vue resolves is what the document takes for native. The type
231
+ * selector (`Marquee` stands for `<marquee>`) reads it, and so does the guard that keeps a pretender off
232
+ * a native element, as `@markuplint/pretenders` does. The rules keyed on the specification still see
233
+ * `<marquee>`.
234
+ *
235
+ * It is not the rule of {@link isComponentName}, which tells by the form of a name alone whether it is
236
+ * a component's, for the target of a pretender and the root a scanner reads: `as: 'marquee'` is meant
237
+ * as the element there.
238
+ *
239
+ * @param name - A tag name as written, case kept
240
+ * @returns Whether the name is a native tag
241
+ */
242
+ isNativeTag(name) {
243
+ return isHTMLTag(name) || isSVGTag(name) || isMathMLTag(name);
244
+ }
245
+ /**
246
+ * A name that Vue takes for a component (see {@link detectElementType}) and that names no special
247
+ * element: a built-in component as written or in kebab-case (`Transition`, `transition-group`), a capital
248
+ * initial (`FooBar`), a capital letter after a lowercase letter or a digit (`fooBar`), or a `.`
249
+ * (`ui.Item`). `foo-bar` is not one, as it may be a custom element, nor are `component` and `slot`, which render
250
+ * what they are given: they have no element to pretend to be.
251
+ *
252
+ * @param name - A tag name as written, case kept
253
+ * @returns Whether the name is a component's
254
+ */
255
+ isComponentName(name) {
256
+ // `<transition>` is `<Transition>`, and a tag with a `.` is resolved by the name before it (`my-widget.v2` by
257
+ // `my-widget`): a component by Vue, whatever a custom element name would say of it
258
+ if (BUILT_IN_COMPONENTS.includes(name) || name.includes('.')) {
259
+ return true;
260
+ }
261
+ if (isPotentialCustomElementName(name)) {
262
+ return false;
263
+ }
264
+ return /^[A-Z]/.test(name) || (/[a-z\d][A-Z]/.test(name) && !isSVGElement(name));
265
+ }
176
266
  }
177
267
  export const parser = new VueParser();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/vue-parser",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Vue parser for markuplint",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,12 +15,12 @@
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
18
- "import": "./lib/index.js",
19
- "types": "./lib/index.d.ts"
18
+ "types": "./lib/index.d.ts",
19
+ "default": "./lib/index.js"
20
20
  },
21
21
  "./component-scanner": {
22
- "import": "./lib/component-scanner.js",
23
- "types": "./lib/component-scanner.d.ts"
22
+ "types": "./lib/component-scanner.d.ts",
23
+ "default": "./lib/component-scanner.js"
24
24
  }
25
25
  },
26
26
  "publishConfig": {
@@ -32,10 +32,11 @@
32
32
  "clean": "tsc --build --clean tsconfig.build.json"
33
33
  },
34
34
  "dependencies": {
35
- "@markuplint/html-parser": "5.2.0",
36
- "@markuplint/ml-ast": "5.1.1",
37
- "@markuplint/parser-utils": "5.2.0",
35
+ "@markuplint/html-parser": "5.3.0",
36
+ "@markuplint/ml-ast": "5.3.0",
37
+ "@markuplint/parser-utils": "5.3.0",
38
+ "@vue/shared": "^3.5.44",
38
39
  "vue-eslint-parser": "^10.4.1"
39
40
  },
40
- "gitHead": "d8b2feab77c9c862513615bea6b80d2817431878"
41
+ "gitHead": "f587cf0206c8426e57369a48d6d5fb22306eafde"
41
42
  }