astro-eslint-parser 0.0.1 → 0.0.4

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/README.md CHANGED
@@ -11,9 +11,14 @@ You can check it on [Online DEMO](https://ota-meshi.github.io/astro-eslint-parse
11
11
  [![NPM downloads](https://img.shields.io/npm/dy/astro-eslint-parser.svg)](http://www.npmtrends.com/astro-eslint-parser)
12
12
  [![NPM downloads](https://img.shields.io/npm/dt/astro-eslint-parser.svg)](http://www.npmtrends.com/astro-eslint-parser)
13
13
  [![Build Status](https://github.com/ota-meshi/astro-eslint-parser/workflows/CI/badge.svg?branch=main)](https://github.com/ota-meshi/astro-eslint-parser/actions?query=workflow%3ACI)
14
+ [![Coverage Status](https://coveralls.io/repos/github/ota-meshi/astro-eslint-parser/badge.svg?branch=main)](https://coveralls.io/github/ota-meshi/astro-eslint-parser?branch=main)
14
15
 
15
16
  This parser is in the ***experimental stages*** of development.
16
17
 
18
+ Currently this parser relies heavily on the internal API of [@astrojs/compiler]. It may stop working in a future update of [@astrojs/compiler].
19
+
20
+ [@astrojs/compiler]: https://github.com/withastro/compiler
21
+
17
22
  <!--
18
23
  ### ESLint Plugins Using astro-eslint-parser
19
24
 
@@ -74,7 +79,7 @@ For example:
74
79
 
75
80
  ### parserOptions.parser
76
81
 
77
- You can use `parserOptions.parser` property to specify a custom parser to parse `<script>` tags.
82
+ You can use `parserOptions.parser` property to specify a custom parser to parse scripts.
78
83
  Other properties than parser would be given to the specified parser.
79
84
  For example:
80
85
 
@@ -87,7 +92,7 @@ For example:
87
92
  }
88
93
  ```
89
94
 
90
- For example, if you are using the `"@typescript-eslint/parser"`, and if you want to use TypeScript in `<script>` of `.astro`, you need to add more `parserOptions` configuration.
95
+ For example, if you are using the `"@typescript-eslint/parser"`, and if you want to use TypeScript in `.astro`, you need to add more `parserOptions` configuration.
91
96
 
92
97
  ```js
93
98
  module.exports = {
@@ -96,13 +101,13 @@ module.exports = {
96
101
  parserOptions: {
97
102
  // ...
98
103
  project: "path/to/your/tsconfig.json",
99
- extraFileExtensions: [".astro"], // This is a required setting in `@typescript-eslint/parser` v4.24.0.
104
+ extraFileExtensions: [".astro"], // This is a required setting in `@typescript-eslint/parser` v5.
100
105
  },
101
106
  overrides: [
102
107
  {
103
108
  files: ["*.astro"],
104
109
  parser: "astro-eslint-parser",
105
- // Parse the `<script>` in `.astro` as TypeScript by adding the following configuration.
110
+ // Parse the script in `.astro` as TypeScript by adding the following configuration.
106
111
  parserOptions: {
107
112
  parser: "@typescript-eslint/parser",
108
113
  },
@@ -133,9 +138,20 @@ Example **.vscode/settings.json**:
133
138
  }
134
139
  ```
135
140
 
141
+ ## Compatibility With Existing ESLint Rules
142
+
143
+ Most of the rules in the ESLint core work for the script part, but some rules are incompatible.
144
+ This parser will generate a JSX compatible AST for most of the HTML part of the Astro component. Therefore, some rules of [eslint-plugin-react] may work.
145
+ For example, the [react/jsx-no-target-blank] rule works fine.
146
+
147
+ [semi]: https://eslint.org/docs/rules/semi
148
+ [eslint-plugin-react]: https://github.com/jsx-eslint/eslint-plugin-react/
149
+ [react/jsx-no-target-blank]: https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-target-blank.md
150
+
136
151
  ## Usage for Custom Rules / Plugins
137
152
 
138
153
  - TBA
154
+ - You can check it on the [Online DEMO](https://ota-meshi.github.io/astro-eslint-parser/). However, AST is subject to major changes in the future.
139
155
 
140
156
  <!-- - [AST.md](./docs/AST.md) is AST specification. You can check it on the [Online DEMO](https://ota-meshi.github.io/astro-eslint-parser/). -->
141
157
  <!-- - I have already [implemented some rules] in the [`@ota-meshi/eslint-plugin-astro`]. The source code for these rules will be helpful to you. -->
package/lib/ast.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { TSESTree } from "@typescript-eslint/types";
2
- export declare type AstroNode = AstroProgram | AstroRootFragment | AstroHTMLComment | AstroDoctype | AstroShorthandAttribute | AstroTemplateLiteralAttribute;
2
+ export declare type AstroNode = AstroProgram | AstroRootFragment | AstroHTMLComment | AstroDoctype | AstroShorthandAttribute | AstroTemplateLiteralAttribute | AstroRawText;
3
3
  /** Node of Astro program root */
4
4
  export interface AstroProgram extends Omit<TSESTree.Program, "type" | "body"> {
5
5
  type: "Program";
@@ -40,3 +40,8 @@ export interface AstroTemplateLiteralAttribute extends Omit<TSESTree.JSXAttribut
40
40
  };
41
41
  parent: TSESTree.JSXElement | TSESTree.JSXFragment;
42
42
  }
43
+ /** Node of Astro raw text */
44
+ export interface AstroRawText extends Omit<TSESTree.JSXText, "type" | "parent"> {
45
+ type: "AstroRawText";
46
+ parent: AstroRootFragment | TSESTree.JSXElement | TSESTree.JSXFragment;
47
+ }
@@ -46,6 +46,11 @@ class ScriptContext {
46
46
  if (last.expression.type !== "JSXFragment") {
47
47
  throw new errors_1.ParseError("Unknown state error: Expected JSXFragment", last.expression.range[0], this.ctx);
48
48
  }
49
+ // Process for Astro
50
+ const rootFragment = (result.ast.body[result.ast.body.length - 1] = last.expression);
51
+ delete rootFragment.closingFragment;
52
+ delete rootFragment.openingFragment;
53
+ rootFragment.type = "AstroRootFragment";
49
54
  // remap locations
50
55
  const traversed = new Set();
51
56
  (0, traverse_1.traverseNodes)(result.ast, {
@@ -72,11 +77,6 @@ class ScriptContext {
72
77
  for (const token of result.ast.comments || []) {
73
78
  this.remapLocation(token);
74
79
  }
75
- // Process for Astro
76
- delete last.expression.closingFragment;
77
- delete last.expression.openingFragment;
78
- last.expression.type =
79
- "AstroRootFragment";
80
80
  let restoreNodeProcesses = this.restoreNodeProcesses;
81
81
  for (const node of traversed) {
82
82
  restoreNodeProcesses = restoreNodeProcesses.filter((proc) => !proc(node, result));
@@ -41,9 +41,7 @@ exports.parse = parse;
41
41
  function fixLocations(node, code) {
42
42
  // FIXME: Adjust because the parser does not return the correct location.
43
43
  let start = 0;
44
- (0, astro_1.walk)(node,
45
- // eslint-disable-next-line complexity -- ignore
46
- (node) => {
44
+ (0, astro_1.walk)(node, (node) => {
47
45
  if (node.type === "frontmatter") {
48
46
  start = node.position.start.offset = tokenIndex(code, "---", start);
49
47
  start = node.position.end.offset =
@@ -58,11 +56,7 @@ function fixLocations(node, code) {
58
56
  }
59
57
  start = node.position.start.offset = tokenIndex(code, "<", start);
60
58
  start += 1;
61
- if (node.type === "element" ||
62
- node.type === "component" ||
63
- node.type === "custom-element") {
64
- start += node.name.length;
65
- }
59
+ start += node.name.length;
66
60
  if (!node.attributes.length) {
67
61
  start = (0, astro_1.getStartTagEndOffset)(node, code);
68
62
  }
@@ -106,20 +100,18 @@ function fixLocations(node, code) {
106
100
  if (node.type === "expression") {
107
101
  start = tokenIndex(code, "}", start) + 1;
108
102
  }
109
- else if (node.type === "fragment") {
110
- start = tokenIndex(code, "</>", start) + 3;
111
- }
112
- else if (node.type === "element" ||
103
+ else if (node.type === "fragment" ||
104
+ node.type === "element" ||
113
105
  node.type === "component" ||
114
106
  node.type === "custom-element") {
115
107
  if (!node.position.end) {
116
108
  return;
117
109
  }
118
- start =
119
- tokenIndex(code, `</${node.name}`, start) +
120
- 2 +
121
- node.name.length;
122
- start = tokenIndex(code, ">", start) + 1;
110
+ const closeTagStart = tokenIndexSafe(code, `</${node.name}`, start);
111
+ if (closeTagStart != null) {
112
+ start = closeTagStart + 2 + node.name.length;
113
+ start = tokenIndex(code, ">", start) + 1;
114
+ }
123
115
  }
124
116
  else {
125
117
  return;
@@ -159,9 +151,20 @@ function fixLocationForAttr(node, code, start) {
159
151
  * Get token index
160
152
  */
161
153
  function tokenIndex(string, token, position) {
154
+ const index = tokenIndexSafe(string, token, position);
155
+ if (index == null) {
156
+ const start = token.trim() === token ? (0, astro_1.skipSpaces)(string, position) : position;
157
+ throw new Error(`Unknown token at ${start}, expected: ${JSON.stringify(token)}, actual: ${JSON.stringify(string.slice(start, start + 10))}`);
158
+ }
159
+ return index;
160
+ }
161
+ /**
162
+ * Get token index
163
+ */
164
+ function tokenIndexSafe(string, token, position) {
162
165
  const index = token.trim() === token ? (0, astro_1.skipSpaces)(string, position) : position;
163
166
  if (string.startsWith(token, index)) {
164
167
  return index;
165
168
  }
166
- throw new Error(`Unknown token at ${index}, expected: ${JSON.stringify(token)}, actual: ${JSON.stringify(string.slice(index, index + 10))}`);
169
+ return null;
167
170
  }
@@ -15,6 +15,7 @@ function processTemplate(ctx, resultTemplate) {
15
15
  script.appendScript("<>");
16
16
  fragmentOpened = true;
17
17
  }
18
+ // eslint-disable-next-line complexity -- X(
18
19
  (0, astro_1.walkElements)(resultTemplate.ast, (node, parent) => {
19
20
  if (node.type === "frontmatter") {
20
21
  const start = node.position.start.offset;
@@ -45,6 +46,56 @@ function processTemplate(ctx, resultTemplate) {
45
46
  }
46
47
  else if ((0, astro_1.isTag)(node)) {
47
48
  for (const attr of node.attributes) {
49
+ if ((node.type === "component" || node.type === "fragment") &&
50
+ (attr.kind === "quoted" ||
51
+ attr.kind === "empty" ||
52
+ attr.kind === "expression" ||
53
+ attr.kind === "template-literal")) {
54
+ const colonIndex = attr.name.indexOf(":");
55
+ if (colonIndex >= 0) {
56
+ const start = attr.position.start.offset;
57
+ script.appendOriginal(start + colonIndex);
58
+ script.skipOriginalOffset(1);
59
+ script.appendScript(`_`);
60
+ script.addToken(types_1.AST_TOKEN_TYPES.JSXIdentifier, [
61
+ start,
62
+ start + colonIndex,
63
+ ]);
64
+ script.addToken(types_1.AST_TOKEN_TYPES.Punctuator, [
65
+ start + colonIndex,
66
+ start + colonIndex + 1,
67
+ ]);
68
+ script.addToken(types_1.AST_TOKEN_TYPES.JSXIdentifier, [
69
+ start + colonIndex + 1,
70
+ start + attr.name.length,
71
+ ]);
72
+ script.addRestoreNodeProcess((scriptNode, result) => {
73
+ if (scriptNode.type ===
74
+ types_1.AST_NODE_TYPES.JSXAttribute &&
75
+ scriptNode.range[0] === start) {
76
+ const baseNameNode = scriptNode.name;
77
+ const nsn = Object.assign(Object.assign({}, baseNameNode), { type: types_1.AST_NODE_TYPES.JSXNamespacedName, namespace: Object.assign({ type: types_1.AST_NODE_TYPES.JSXIdentifier, name: attr.name.slice(0, colonIndex) }, ctx.getLocations(baseNameNode.range[0], baseNameNode.range[0] + colonIndex)), name: Object.assign({ type: types_1.AST_NODE_TYPES.JSXIdentifier, name: attr.name.slice(colonIndex + 1) }, ctx.getLocations(baseNameNode.range[0] +
78
+ colonIndex +
79
+ 1, baseNameNode.range[1])) });
80
+ scriptNode.name = nsn;
81
+ nsn.namespace.parent = nsn;
82
+ nsn.name.parent = nsn;
83
+ const tokens = result.ast.tokens || [];
84
+ for (let index = 0; index < tokens.length; index++) {
85
+ const token = tokens[index];
86
+ if (token.range[0] ===
87
+ baseNameNode.range[0] &&
88
+ token.range[1] === baseNameNode.range[1]) {
89
+ tokens.splice(index, 1);
90
+ break;
91
+ }
92
+ }
93
+ return true;
94
+ }
95
+ return false;
96
+ });
97
+ }
98
+ }
48
99
  if (attr.kind === "shorthand") {
49
100
  const start = attr.position.start.offset;
50
101
  script.appendOriginal(start);
@@ -96,8 +147,10 @@ function processTemplate(ctx, resultTemplate) {
96
147
  script.addRestoreNodeProcess((scriptNode) => {
97
148
  if (scriptNode.type === types_1.AST_NODE_TYPES.JSXElement &&
98
149
  scriptNode.range[0] === styleNodeStart) {
99
- const textNode = Object.assign({ type: types_1.AST_NODE_TYPES.JSXText, value: text.value, raw: text.value, parent: scriptNode }, ctx.getLocations(start, start + text.value.length));
100
- scriptNode.children = [textNode];
150
+ const textNode = Object.assign({ type: "AstroRawText", value: text.value, raw: text.value, parent: scriptNode }, ctx.getLocations(start, start + text.value.length));
151
+ scriptNode.children = [
152
+ textNode,
153
+ ];
101
154
  return true;
102
155
  }
103
156
  return false;
@@ -181,10 +234,9 @@ exports.processTemplate = processTemplate;
181
234
  * If the given tag is a void tag, get the self-closing tag.
182
235
  */
183
236
  function getVoidSelfClosingTag(node, parent, ctx) {
184
- if (node.type === "fragment") {
185
- return false;
186
- }
187
- if (node.children.length > 0) {
237
+ var _a;
238
+ const children = node.children.filter((c) => c.type !== "text" || c.value.trim());
239
+ if (children.length > 0) {
188
240
  return false;
189
241
  }
190
242
  const code = ctx.code;
@@ -192,8 +244,10 @@ function getVoidSelfClosingTag(node, parent, ctx) {
192
244
  const childIndex = parent.children.indexOf(node);
193
245
  if (childIndex === parent.children.length - 1) {
194
246
  // last
195
- nextElementIndex = parent.position.end.offset;
196
- nextElementIndex = code.lastIndexOf("</", nextElementIndex);
247
+ if ((_a = parent.position) === null || _a === void 0 ? void 0 : _a.end) {
248
+ nextElementIndex = parent.position.end.offset;
249
+ nextElementIndex = code.lastIndexOf("</", nextElementIndex);
250
+ }
197
251
  }
198
252
  else {
199
253
  const next = parent.children[childIndex + 1];
@@ -9,5 +9,6 @@ const astroKeys = {
9
9
  AstroDoctype: [],
10
10
  AstroShorthandAttribute: ["name", "value"],
11
11
  AstroTemplateLiteralAttribute: ["name", "value"],
12
+ AstroRawText: [],
12
13
  };
13
14
  exports.KEYS = (0, eslint_visitor_keys_1.unionWith)(astroKeys);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-eslint-parser",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "description": "Astro parser for ESLint",
5
5
  "main": "lib/index.js",
6
6
  "files": [
@@ -62,7 +62,7 @@
62
62
  "benchmark": "^2.1.4",
63
63
  "chai": "^4.3.4",
64
64
  "code-red": "^0.2.3",
65
- "eslint": "^8.2.0",
65
+ "eslint": "^8.14.0",
66
66
  "eslint-config-prettier": "^8.3.0",
67
67
  "eslint-formatter-codeframe": "^7.32.1",
68
68
  "eslint-plugin-eslint-comments": "^3.2.0",
@@ -71,12 +71,13 @@
71
71
  "eslint-plugin-node": "^11.1.0",
72
72
  "eslint-plugin-node-dependencies": "^0.8.0",
73
73
  "eslint-plugin-prettier": "^4.0.0",
74
+ "eslint-plugin-react": "^7.29.4",
74
75
  "eslint-plugin-regexp": "^1.5.0",
75
76
  "eslint-plugin-vue": "^8.0.3",
76
77
  "estree-walker": "^3.0.0",
77
78
  "locate-character": "^2.0.5",
78
79
  "magic-string": "^0.26.0",
79
- "mocha": "^9.1.3",
80
+ "mocha": "^10.0.0",
80
81
  "mocha-chai-jest-snapshot": "^1.1.3",
81
82
  "nyc": "^15.1.0",
82
83
  "prettier": "^2.0.5",