Skip to content

Commit 26a62b8

Browse files
committedJul 25, 2022
feat: support external HTML plugins
1 parent 5cba3d1 commit 26a62b8

File tree

5 files changed

+192
-30
lines changed

5 files changed

+192
-30
lines changed
 

‎package-lock.json

+32
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"htmlparser2": "^8.0.1"
2121
},
2222
"devDependencies": {
23+
"@html-eslint/eslint-plugin": "^0.13.2",
24+
"@html-eslint/parser": "^0.13.2",
2325
"eslint": "^8.5.0",
2426
"eslint-config-prettier": "^8.5.0",
2527
"jest": "^28.1.3",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<img src="">
2+
<script>
3+
console.log("toto")
4+
</script>

‎src/__tests__/plugin.js

+79-19
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const path = require("path")
44
const eslint = require("eslint")
55
const semver = require("semver")
66
const eslintVersion = require("eslint/package.json").version
7-
const plugin = require("..")
7+
require("..")
88

99
function matchVersion(versionSpec) {
1010
return semver.satisfies(eslintVersion, versionSpec, {
@@ -17,48 +17,49 @@ function ifVersion(versionSpec, fn, ...args) {
1717
execFn(...args)
1818
}
1919

20-
async function execute(file, baseConfig) {
21-
if (!baseConfig) baseConfig = {}
22-
20+
async function execute(file, options = {}) {
2321
const files = [path.join(__dirname, "fixtures", file)]
2422

25-
const options = {
23+
const eslintOptions = {
2624
extensions: ["html"],
2725
baseConfig: {
28-
settings: baseConfig.settings,
26+
settings: options.settings,
2927
rules: Object.assign(
3028
{
3129
"no-console": 2,
3230
},
33-
baseConfig.rules
31+
options.rules
3432
),
35-
globals: baseConfig.globals,
36-
env: baseConfig.env,
37-
parserOptions: baseConfig.parserOptions,
33+
globals: options.globals,
34+
env: options.env,
35+
parserOptions: options.parserOptions,
36+
parser: options.parser,
3837
},
3938
ignore: false,
4039
useEslintrc: false,
41-
fix: baseConfig.fix,
40+
fix: options.fix,
4241
reportUnusedDisableDirectives:
43-
baseConfig.reportUnusedDisableDirectives || null,
42+
options.reportUnusedDisableDirectives || null,
4443
}
4544

4645
let results
4746
if (eslint.ESLint) {
48-
const instance = new eslint.ESLint({
49-
...options,
50-
plugins: { html: plugin },
51-
})
47+
eslintOptions.baseConfig.plugins = options.plugins
48+
const instance = new eslint.ESLint(eslintOptions)
5249
results = (await instance.lintFiles(files))[0]
5350
} else if (eslint.CLIEngine) {
54-
const cli = new eslint.CLIEngine(options)
55-
cli.addPlugin("html", plugin)
51+
const cli = new eslint.CLIEngine(eslintOptions)
52+
if (options.plugins) {
53+
for (const plugin of options.plugins) {
54+
cli.addPlugin(plugin.split("/")[0], require(plugin))
55+
}
56+
}
5657
results = cli.executeOnFiles(files).results[0]
5758
} else {
5859
throw new Error("invalid ESLint dependency")
5960
}
6061

61-
return baseConfig.fix ? results : results && results.messages
62+
return options.fix ? results : results && results.messages
6263
}
6364

6465
it("should extract and remap messages", async () => {
@@ -790,3 +791,62 @@ describe("scope sharing", () => {
790791
expect(messages[15].message).toBe("'ClassGloballyDeclared' is not defined.")
791792
})
792793
})
794+
795+
// For some reason @html-eslint is not compatible with ESLint < 5
796+
ifVersion(">= 5", describe, "compatibility with external HTML plugins", () => {
797+
it("check", async () => {
798+
const messages = await execute("other-html-plugins-compatibility.html", {
799+
plugins: ["@html-eslint/eslint-plugin"],
800+
parser: "@html-eslint/parser",
801+
rules: {
802+
"@html-eslint/require-img-alt": ["error"],
803+
},
804+
})
805+
expect(messages).toMatchInlineSnapshot(`
806+
Array [
807+
Object {
808+
"column": 1,
809+
"endColumn": 13,
810+
"endLine": 1,
811+
"line": 1,
812+
"message": "Missing \`alt\` attribute at \`<img>\` tag",
813+
"messageId": "missingAlt",
814+
"nodeType": null,
815+
"ruleId": "@html-eslint/require-img-alt",
816+
"severity": 2,
817+
},
818+
Object {
819+
"column": 3,
820+
"endColumn": 14,
821+
"endLine": 3,
822+
"line": 3,
823+
"message": "Unexpected console statement.",
824+
"messageId": "unexpected",
825+
"nodeType": "MemberExpression",
826+
"ruleId": "no-console",
827+
"severity": 2,
828+
"source": " console.log(\\"toto\\")",
829+
},
830+
]
831+
`)
832+
})
833+
834+
it("fix", async () => {
835+
const result = await execute("other-html-plugins-compatibility.html", {
836+
plugins: ["@html-eslint/eslint-plugin"],
837+
parser: "@html-eslint/parser",
838+
rules: {
839+
"@html-eslint/quotes": ["error", "single"],
840+
quotes: ["error", "single"],
841+
},
842+
fix: true,
843+
})
844+
expect(result.output).toMatchInlineSnapshot(`
845+
"<img src=''>
846+
<script>
847+
console.log('toto')
848+
</script>
849+
"
850+
`)
851+
})
852+
})

‎src/index.js

+75-11
Original file line numberDiff line numberDiff line change
@@ -126,22 +126,37 @@ function patch(Linter) {
126126
filenameOrOptions,
127127
saveState
128128
) {
129+
const callOriginalVerify = () =>
130+
verify.call(this, textOrSourceCode, config, filenameOrOptions, saveState)
131+
129132
if (typeof config.extractConfig === "function") {
130-
return verify.call(this, textOrSourceCode, config, filenameOrOptions)
133+
return callOriginalVerify()
131134
}
132135

133136
const pluginSettings = getSettings(config.settings || {})
134137
const mode = getFileMode(pluginSettings, filenameOrOptions)
135138

136139
if (!mode || typeof textOrSourceCode !== "string") {
137-
return verify.call(
138-
this,
139-
textOrSourceCode,
140-
config,
141-
filenameOrOptions,
142-
saveState
143-
)
140+
return callOriginalVerify()
141+
}
142+
143+
let messages
144+
;[messages, config] = verifyExternalHtmlPlugin(config, callOriginalVerify)
145+
146+
if (config.parser && config.parser.id === "@html-eslint/parser") {
147+
messages.push(...callOriginalVerify())
148+
const rules = {}
149+
for (const name in config.rules) {
150+
if (!name.startsWith("@html-eslint/")) {
151+
rules[name] = config.rules[name]
152+
}
153+
}
154+
config = editConfig(config, {
155+
parser: null,
156+
rules,
157+
})
144158
}
159+
145160
const extractResult = extract(
146161
textOrSourceCode,
147162
pluginSettings.indent,
@@ -150,8 +165,6 @@ function patch(Linter) {
150165
pluginSettings.isJavaScriptMIMEType
151166
)
152167

153-
const messages = []
154-
155168
if (pluginSettings.reportBadIndent) {
156169
messages.push(
157170
...extractResult.badIndentationLines.map((line) => ({
@@ -181,7 +194,7 @@ function patch(Linter) {
181194
const localMessages = verify.call(
182195
this,
183196
sourceCodes.get(codePart) || String(codePart),
184-
Object.assign({}, config, {
197+
editConfig(config, {
185198
rules: Object.assign(
186199
{ [PREPARE_RULE_NAME]: "error" },
187200
!ignoreRules && config.rules
@@ -215,6 +228,57 @@ function patch(Linter) {
215228
}
216229
}
217230

231+
function editConfig(config, { parser = config.parser, rules = config.rules }) {
232+
return {
233+
...config,
234+
parser,
235+
rules,
236+
}
237+
}
238+
239+
const externalHtmlPluginPrefixes = [
240+
"@html-eslint/",
241+
"@angular-eslint/template-",
242+
]
243+
244+
function getParserId(config) {
245+
if (!config.parser) {
246+
return
247+
}
248+
249+
if (typeof config.parser === "string") {
250+
// old versions of ESLint (ex: 4.7)
251+
return config.parser
252+
}
253+
254+
return config.parser.id
255+
}
256+
257+
function verifyExternalHtmlPlugin(config, callOriginalVerify) {
258+
const parserId = getParserId(config)
259+
const externalHtmlPluginPrefix =
260+
parserId &&
261+
externalHtmlPluginPrefixes.find((prefix) => parserId.startsWith(prefix))
262+
if (!externalHtmlPluginPrefix) {
263+
return [[], config]
264+
}
265+
266+
const rules = {}
267+
for (const name in config.rules) {
268+
if (!name.startsWith(externalHtmlPluginPrefix)) {
269+
rules[name] = config.rules[name]
270+
}
271+
}
272+
273+
return [
274+
callOriginalVerify(),
275+
editConfig(config, {
276+
parser: null,
277+
rules,
278+
}),
279+
]
280+
}
281+
218282
function verifyWithSharedScopes(codeParts, verifyCodePart, parserOptions) {
219283
// First pass: collect needed globals and declared globals for each script tags.
220284
const firstPassValues = []

0 commit comments

Comments
 (0)
Please sign in to comment.