Skip to content

Commit

Permalink
feat: support external HTML plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
BenoitZugmeyer committed Jul 25, 2022
1 parent 5cba3d1 commit 26a62b8
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 30 deletions.
32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"htmlparser2": "^8.0.1"
},
"devDependencies": {
"@html-eslint/eslint-plugin": "^0.13.2",
"@html-eslint/parser": "^0.13.2",
"eslint": "^8.5.0",
"eslint-config-prettier": "^8.5.0",
"jest": "^28.1.3",
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/fixtures/other-html-plugins-compatibility.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<img src="">
<script>
console.log("toto")
</script>
98 changes: 79 additions & 19 deletions src/__tests__/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const path = require("path")
const eslint = require("eslint")
const semver = require("semver")
const eslintVersion = require("eslint/package.json").version
const plugin = require("..")
require("..")

function matchVersion(versionSpec) {
return semver.satisfies(eslintVersion, versionSpec, {
Expand All @@ -17,48 +17,49 @@ function ifVersion(versionSpec, fn, ...args) {
execFn(...args)
}

async function execute(file, baseConfig) {
if (!baseConfig) baseConfig = {}

async function execute(file, options = {}) {
const files = [path.join(__dirname, "fixtures", file)]

const options = {
const eslintOptions = {
extensions: ["html"],
baseConfig: {
settings: baseConfig.settings,
settings: options.settings,
rules: Object.assign(
{
"no-console": 2,
},
baseConfig.rules
options.rules
),
globals: baseConfig.globals,
env: baseConfig.env,
parserOptions: baseConfig.parserOptions,
globals: options.globals,
env: options.env,
parserOptions: options.parserOptions,
parser: options.parser,
},
ignore: false,
useEslintrc: false,
fix: baseConfig.fix,
fix: options.fix,
reportUnusedDisableDirectives:
baseConfig.reportUnusedDisableDirectives || null,
options.reportUnusedDisableDirectives || null,
}

let results
if (eslint.ESLint) {
const instance = new eslint.ESLint({
...options,
plugins: { html: plugin },
})
eslintOptions.baseConfig.plugins = options.plugins
const instance = new eslint.ESLint(eslintOptions)
results = (await instance.lintFiles(files))[0]
} else if (eslint.CLIEngine) {
const cli = new eslint.CLIEngine(options)
cli.addPlugin("html", plugin)
const cli = new eslint.CLIEngine(eslintOptions)
if (options.plugins) {
for (const plugin of options.plugins) {
cli.addPlugin(plugin.split("/")[0], require(plugin))
}
}
results = cli.executeOnFiles(files).results[0]
} else {
throw new Error("invalid ESLint dependency")
}

return baseConfig.fix ? results : results && results.messages
return options.fix ? results : results && results.messages
}

it("should extract and remap messages", async () => {
Expand Down Expand Up @@ -790,3 +791,62 @@ describe("scope sharing", () => {
expect(messages[15].message).toBe("'ClassGloballyDeclared' is not defined.")
})
})

// For some reason @html-eslint is not compatible with ESLint < 5
ifVersion(">= 5", describe, "compatibility with external HTML plugins", () => {
it("check", async () => {
const messages = await execute("other-html-plugins-compatibility.html", {
plugins: ["@html-eslint/eslint-plugin"],
parser: "@html-eslint/parser",
rules: {
"@html-eslint/require-img-alt": ["error"],
},
})
expect(messages).toMatchInlineSnapshot(`
Array [
Object {
"column": 1,
"endColumn": 13,
"endLine": 1,
"line": 1,
"message": "Missing \`alt\` attribute at \`<img>\` tag",
"messageId": "missingAlt",
"nodeType": null,
"ruleId": "@html-eslint/require-img-alt",
"severity": 2,
},
Object {
"column": 3,
"endColumn": 14,
"endLine": 3,
"line": 3,
"message": "Unexpected console statement.",
"messageId": "unexpected",
"nodeType": "MemberExpression",
"ruleId": "no-console",
"severity": 2,
"source": " console.log(\\"toto\\")",
},
]
`)
})

it("fix", async () => {
const result = await execute("other-html-plugins-compatibility.html", {
plugins: ["@html-eslint/eslint-plugin"],
parser: "@html-eslint/parser",
rules: {
"@html-eslint/quotes": ["error", "single"],
quotes: ["error", "single"],
},
fix: true,
})
expect(result.output).toMatchInlineSnapshot(`
"<img src=''>
<script>
console.log('toto')
</script>
"
`)
})
})
86 changes: 75 additions & 11 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,22 +126,37 @@ function patch(Linter) {
filenameOrOptions,
saveState
) {
const callOriginalVerify = () =>
verify.call(this, textOrSourceCode, config, filenameOrOptions, saveState)

if (typeof config.extractConfig === "function") {
return verify.call(this, textOrSourceCode, config, filenameOrOptions)
return callOriginalVerify()
}

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

if (!mode || typeof textOrSourceCode !== "string") {
return verify.call(
this,
textOrSourceCode,
config,
filenameOrOptions,
saveState
)
return callOriginalVerify()
}

let messages
;[messages, config] = verifyExternalHtmlPlugin(config, callOriginalVerify)

if (config.parser && config.parser.id === "@html-eslint/parser") {
messages.push(...callOriginalVerify())
const rules = {}
for (const name in config.rules) {
if (!name.startsWith("@html-eslint/")) {
rules[name] = config.rules[name]
}
}
config = editConfig(config, {
parser: null,
rules,
})
}

const extractResult = extract(
textOrSourceCode,
pluginSettings.indent,
Expand All @@ -150,8 +165,6 @@ function patch(Linter) {
pluginSettings.isJavaScriptMIMEType
)

const messages = []

if (pluginSettings.reportBadIndent) {
messages.push(
...extractResult.badIndentationLines.map((line) => ({
Expand Down Expand Up @@ -181,7 +194,7 @@ function patch(Linter) {
const localMessages = verify.call(
this,
sourceCodes.get(codePart) || String(codePart),
Object.assign({}, config, {
editConfig(config, {
rules: Object.assign(
{ [PREPARE_RULE_NAME]: "error" },
!ignoreRules && config.rules
Expand Down Expand Up @@ -215,6 +228,57 @@ function patch(Linter) {
}
}

function editConfig(config, { parser = config.parser, rules = config.rules }) {
return {
...config,
parser,
rules,
}
}

const externalHtmlPluginPrefixes = [
"@html-eslint/",
"@angular-eslint/template-",
]

function getParserId(config) {
if (!config.parser) {
return
}

if (typeof config.parser === "string") {
// old versions of ESLint (ex: 4.7)
return config.parser
}

return config.parser.id
}

function verifyExternalHtmlPlugin(config, callOriginalVerify) {
const parserId = getParserId(config)
const externalHtmlPluginPrefix =
parserId &&
externalHtmlPluginPrefixes.find((prefix) => parserId.startsWith(prefix))
if (!externalHtmlPluginPrefix) {
return [[], config]
}

const rules = {}
for (const name in config.rules) {
if (!name.startsWith(externalHtmlPluginPrefix)) {
rules[name] = config.rules[name]
}
}

return [
callOriginalVerify(),
editConfig(config, {
parser: null,
rules,
}),
]
}

function verifyWithSharedScopes(codeParts, verifyCodePart, parserOptions) {
// First pass: collect needed globals and declared globals for each script tags.
const firstPassValues = []
Expand Down

0 comments on commit 26a62b8

Please sign in to comment.