From f91653a79746061cd03335b2bbb5bf207ba37c81 Mon Sep 17 00:00:00 2001 From: joao10lima Date: Wed, 1 Sep 2021 14:44:35 -0300 Subject: [PATCH] feat: add html stack Adds prettier as a formatter for .html and Vue files. Uses ESLint and Stylelint for Vue linting, choosing ESLint as a default. Resolves: https://github.com/pipelinit/pipelinit-cli/issues/7 --- plugins/stack/html/formatters.ts | 43 ++++++++++++++++ plugins/stack/html/linters.ts | 56 ++++++++++++++++++++ plugins/stack/html/mod.test.ts | 88 ++++++++++++++++++++++++++++++++ plugins/stack/html/mod.ts | 81 +++++++++++++++++++++++++++++ plugins/stack/mod.ts | 8 ++- templates/github/html/format.yml | 29 +++++++++++ templates/github/html/lint.yml | 31 +++++++++++ 7 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 plugins/stack/html/formatters.ts create mode 100644 plugins/stack/html/linters.ts create mode 100644 plugins/stack/html/mod.test.ts create mode 100644 plugins/stack/html/mod.ts create mode 100644 templates/github/html/format.yml create mode 100644 templates/github/html/lint.yml diff --git a/plugins/stack/html/formatters.ts b/plugins/stack/html/formatters.ts new file mode 100644 index 0000000..e221c08 --- /dev/null +++ b/plugins/stack/html/formatters.ts @@ -0,0 +1,43 @@ +import { IntrospectFn } from "../deps.ts"; +import { + introspect as introspectPrettier, + Prettier, +} from "../_shared/prettier/mod.ts"; + +// deno-lint-ignore no-empty-interface +interface Deno {} + +export type Formatters = { + prettier?: Prettier | null; + deno?: Deno; +} | null; + +function anyValue(records: Record): boolean { + return Object.values(records).some((v) => v); +} + +export const introspect: IntrospectFn = async (context) => { + const logger = context.getLogger("html"); + logger.debug("detecting formatter"); + + const prettier = await introspectPrettier(context); + if (prettier !== null) { + logger.debug("detected Prettier"); + } + + const formatters: Formatters = { + prettier, + }; + + if (anyValue(formatters)) return formatters; + + if (context.suggestDefault) { + logger.warning("No Vue or Html formatter detected, using Prettier"); + return { + prettier: { name: "prettier", hasIgnoreFile: false }, + }; + } + + logger.debug("no supported formatter detected"); + return null; +}; diff --git a/plugins/stack/html/linters.ts b/plugins/stack/html/linters.ts new file mode 100644 index 0000000..87e66bf --- /dev/null +++ b/plugins/stack/html/linters.ts @@ -0,0 +1,56 @@ +import { IntrospectFn } from "../deps.ts"; + +import { + ESLint, + introspect as introspectESLint, +} from "../_shared/eslint/mod.ts"; + +import { + introspect as introspectStylelint, + Stylelint, +} from "../_shared/stylelint/mod.ts"; + +// deno-lint-ignore no-empty-interface +interface Deno {} + +export type Linters = { + eslint?: ESLint | null; + stylelint?: Stylelint | null; + deno?: Deno; +} | null; + +function anyValue(records: Record): boolean { + return Object.values(records).some((v) => v); +} + +export const introspect: IntrospectFn = async (context) => { + const logger = context.getLogger("html"); + logger.debug("detecting linter"); + + const eslint = await introspectESLint(context); + if (eslint !== null) { + logger.debug("detected ESLint"); + } + + const styleLint = await introspectStylelint(context); + if (styleLint !== null) { + logger.debug("detected stylelint"); + } + + const linters = { + eslint, + styleLint, + }; + + if (anyValue(linters)) return linters; + + if (context.suggestDefault) { + logger.warning("No Vue or HTML linter detected, using ESLint"); + return { + eslint: { name: "eslint", hasIgnoreFile: false }, + }; + } + + logger.debug("no supported linter detected"); + return null; +}; diff --git a/plugins/stack/html/mod.test.ts b/plugins/stack/html/mod.test.ts new file mode 100644 index 0000000..fdd794d --- /dev/null +++ b/plugins/stack/html/mod.test.ts @@ -0,0 +1,88 @@ +import { context } from "../../../src/plugin/mod.ts"; +import { assertEquals, deepMerge, WalkEntry } from "../../../deps.ts"; + +import { introspector } from "./mod.ts"; + +Deno.test("Plugins > Html has stylelint and eslint configured", async () => { + const fakeContext = deepMerge( + context, + { + files: { + // deno-lint-ignore require-await + includes: async (glob: string): Promise => { + if (glob === "**/.eslintrc.{js,cjs,yaml,yml,json}") { + return true; + } + return false; + }, + each: async function* (glob: string): AsyncIterableIterator { + if (glob === "**/*.{html,vue}") { + yield { + name: "test.html", + path: "fake-path", + isFile: true, + isSymlink: false, + isDirectory: false, + }; + yield { + name: "test.vue", + path: "fake-path", + isFile: true, + isSymlink: false, + isDirectory: false, + }; + } + if (glob === "**/package.json") { + yield { + name: "package.json", + path: "fake-path", + isFile: true, + isSymlink: false, + isDirectory: false, + }; + } + if (glob === "**/.eslintrc.{js,cjs,yaml,yml,json}") { + yield { + name: ".eslintrc.js", + path: "fake-path", + isFile: true, + isSymlink: false, + isDirectory: false, + }; + } + if (glob === "**/.eslintignore") { + yield { + name: ".eslintignore", + path: "fake-path", + isFile: true, + isSymlink: false, + isDirectory: false, + }; + } + return; + }, + // deno-lint-ignore require-await + readJSON: async (path: string): Promise> => { + const deps = { stylelint: "1.0.0", eslint: "7.2.2" }; + if (path === "fake-path") { + return { devDependencies: deps }; + } + return {}; + }, + }, + }, + ); + const result = await introspector.introspect( + fakeContext, + ); + + assertEquals(result, { + runtime: { name: "node", version: "16" }, + packageManager: { name: "npm" }, + linters: { + eslint: { name: "eslint", hasIgnoreFile: false }, + styleLint: { name: "stylelint" }, + }, + formatters: { prettier: { name: "prettier", hasIgnoreFile: false } }, + }); +}); diff --git a/plugins/stack/html/mod.ts b/plugins/stack/html/mod.ts new file mode 100644 index 0000000..7827d2b --- /dev/null +++ b/plugins/stack/html/mod.ts @@ -0,0 +1,81 @@ +import { Introspector } from "../deps.ts"; +import { Formatters, introspect as introspectFormatter } from "./formatters.ts"; +import { introspect as introspectLinter, Linters } from "./linters.ts"; +import { + introspect as introspectRuntime, + Runtime, +} from "../javascript/runtime.ts"; +import { + introspect as introspectPackageManager, + NodePackageManager, +} from "../_shared/node_package_manager/mod.ts"; + +// Available package managers +type PackageManager = NodePackageManager | null; + +/** + * Introspected information about a project with JavaScript + */ +export default interface HtmlProject { + /** + * Which package manager is used in the project + * + * A package manager may not exist in a JavaScript project. + * For example, a project that uses Deno doesn't need to use + * npm, yarn or any other package manager. + */ + packageManager?: PackageManager; + /** + * Which runtime the project uses + */ + runtime: Runtime; + /** + * Which linter the project uses, if any + */ + linters: Linters; + /** + * Which formatter the project uses, if any + */ + formatters: Formatters; +} + +export const introspector: Introspector = { + detect: async (context) => { + return await context.files.includes("**/*.{html,vue}"); + }, + introspect: async (context) => { + const logger = context.getLogger("html"); + + // Runtime + logger.debug("detecting runtime"); + const runtime = await introspectRuntime(context); + logger.debug(`detected runtime "${runtime.name}"`); + if (runtime.name === "deno") { + return { + runtime, + linters: { + deno: {}, + }, + formatters: { + deno: {}, + }, + }; + } + + // Package manager + logger.debug("detecting package manager"); + const packageManager = await introspectPackageManager(context); + logger.debug(`detected package manager "${packageManager.name}"`); + // Linter and Formatter + const linters = await introspectLinter(context); + logger.debug(`detecting linters for html`); + const formatters = await introspectFormatter(context); + + return { + runtime, + packageManager, + linters, + formatters, + }; + }, +}; diff --git a/plugins/stack/mod.ts b/plugins/stack/mod.ts index 8a2d083..a7b5a3c 100644 --- a/plugins/stack/mod.ts +++ b/plugins/stack/mod.ts @@ -1,20 +1,24 @@ import { introspector as CssIntrospector } from "./css/mod.ts"; import { introspector as JavaScriptIntrospector } from "./javascript/mod.ts"; import { introspector as PythonIntrospector } from "./python/mod.ts"; +import { introspector as HtmlIntrospector } from "./html/mod.ts"; import type CSSProject from "./css/mod.ts"; import type JavaScriptProject from "./javascript/mod.ts"; import type PythonProject from "./python/mod.ts"; +import type HtmlProject from "./html/mod.ts"; export type ProjectData = | CSSProject | JavaScriptProject - | PythonProject; + | PythonProject + | HtmlProject; -export type { CSSProject, JavaScriptProject, PythonProject }; +export type { CSSProject, HtmlProject, JavaScriptProject, PythonProject }; export const introspectors = [ { name: "css", ...CssIntrospector }, { name: "javascript", ...JavaScriptIntrospector }, { name: "python", ...PythonIntrospector }, + { name: "html", ...HtmlIntrospector }, ]; diff --git a/templates/github/html/format.yml b/templates/github/html/format.yml new file mode 100644 index 0000000..db32488 --- /dev/null +++ b/templates/github/html/format.yml @@ -0,0 +1,29 @@ +<% if (it.runtime.name === "node" && it.formatters) { -%> +name: Format HTML +on: + pull_request: + paths: + - '**.html' + - '**.vue' +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '<%= it.runtime.version %>' + cache: '<%= it.packageManager.name %>' + + <%_ let installCmd; -%> + <%_ if (it.packageManager.name === "npm") { -%> + <%_ installCmd = "npm ci" %> + <%_ } else { -%> + <%_ installCmd = "yarn" %> + <% } -%> + + - run: <%= installCmd %> + <%_ if (it.formatters.prettier) { %> + - run: npx prettier --no-error-on-unmatched-pattern --check "**/*.vue" "**/*.html" + <% } -%> +<% } -%> diff --git a/templates/github/html/lint.yml b/templates/github/html/lint.yml new file mode 100644 index 0000000..f9c4425 --- /dev/null +++ b/templates/github/html/lint.yml @@ -0,0 +1,31 @@ +<% if (it.runtime.name === "node" && it.linters) { -%> +name: Lint Vue +on: + pull_request: + paths: + - '**.vue' +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '<%= it.runtime.version %>' + cache: '<%= it.packageManager.name %>' + + <%_ let installCmd; -%> + <%_ if (it.packageManager.name === "npm") { -%> + <%_ installCmd = "npm ci" %> + <%_ } else { -%> + <%_ installCmd = "yarn" %> + <% } -%> + + - run: <%= installCmd %> + <%_ if (it.linters.eslint) { %> + - run: npx eslint --ext .vue . + <% } -%> + <%_ if (it.linters.styleLint) { %> + - run: npx stylelint --aei **/*.vue + <% } -%> +<% } -%>