Skip to content

Commit

Permalink
Add proper linting and pre-commit hooks
Browse files Browse the repository at this point in the history
Signed-off-by: Rūdolfs Ošiņš <[email protected]>
  • Loading branch information
rudolfs committed Aug 5, 2022
1 parent 96ee93f commit 4e08819
Show file tree
Hide file tree
Showing 8 changed files with 1,356 additions and 35 deletions.
81 changes: 81 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright © 2022 The Radicle Design System Contributors
//
// This file is part of radicle-design-system, distributed under the GPLv3
// with Radicle Linking Exception. For full terms see the included
// LICENSE file.

module.exports = {
env: {
node: true,
browser: true,
es6: true,
},
parser: "@typescript-eslint/parser",
parserOptions: {
createDefaultProgram: true,
ecmaVersion: 2019,
sourceType: "module",
},
ignorePatterns: ["!.license-compliancerc.js"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
plugins: ["svelte3", "@typescript-eslint"],
overrides: [
{
files: ["*.svelte"],
processor: "svelte3/svelte3",
},
{
files: ["scripts/*.ts"],
rules: {
// Script files are not bundled so we can’t use module imports.
"@typescript-eslint/no-var-requires": "off",
},
},
{
files: ["*.js", "*.mjs"],
rules: {
"@typescript-eslint/explicit-module-boundary-types": "off",
},
},
],
rules: {
// Disallow Unused Variables
// https://eslint.org/docs/rules/no-unused-vars
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
// require using arrow functions as callbacks
// https://eslint.org/docs/rules/prefer-arrow-callback
"prefer-arrow-callback": "error",
// require using template literals instead of string concatenation
// http://eslint.org/docs/rules/prefer-template
"prefer-template": "error",
// require using of const declaration for variables that are never modified after declared
// https://eslint.org/docs/rules/prefer-const
"prefer-const": "error",
// disallow modifying variables that are declared using const
// https://eslint.org/docs/rules/no-const-assign
"no-const-assign": "error",
// require let or const instead of var
// https://eslint.org/docs/rules/no-var
"no-var": "error",
// require at least one whitespace after comments( // and /*)
// https://eslint.org/docs/rules/spaced-comment
"spaced-comment": ["warn", "always"],
// Require `===` and `!==` comparisons
eqeqeq: "error",
// Enforce curly braces for if/else statements for better clarity.
curly: "error",

// We are ok with providing explict type annotations for additional
// clarity.
"@typescript-eslint/no-inferrable-types": "off",
// We are ok with empty functions. Often we need a no-op function
// as an argument.
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-implicit-any-catch": "error",
"@typescript-eslint/explicit-member-accessibility": "error",
"@typescript-eslint/explicit-module-boundary-types": "error",
},
settings: {
"svelte3/typescript": true,
},
};
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn run lint-staged
4 changes: 2 additions & 2 deletions .lintstagedrc.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"*.{ts,js,css,json,html,svelte}":
"*.{ts,css,json,html,svelte}":
- prettier --write
"*.{js,ts,svelte}":
"*.{ts,svelte}":
- eslint --fix --max-warnings=0
"*":
- ./scripts/license-header.ts check
17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"url": "https://github.com/radicle-dev/radicle-design-system.git"
},
"scripts": {
"start": "webpack serve --open --config-name design-system"
"start": "webpack serve --open --config-name design-system",
"lint": "eslint . --ignore-path .gitignore --ext .js,.svelte,.ts --max-warnings=0",
"postinstall": "husky install"
},
"dependencies": {
"@types/lodash-es": "^4.17.6",
Expand All @@ -34,10 +36,20 @@
"*.otf"
],
"devDependencies": {
"@types/yargs": "^17.0.11",
"@typescript-eslint/eslint-plugin": "^5.32.0",
"@typescript-eslint/parser": "^5.32.0",
"copy-webpack-plugin": "^11.0.0",
"crypto-browserify": "^3.12.0",
"eslint": "^8.21.0",
"eslint-plugin-svelte3": "^4.0.0",
"execa": "^5.1.1",
"html-webpack-plugin": "^5.5.0",
"husky": "^8.0.1",
"license-webpack-plugin": "^4.0.2",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"prettier-plugin-svelte": "^2.7.0",
"process": "^0.11.10",
"spdx-expression-parse": "^3.0.1",
"spdx-whitelisted": "^1.0.0",
Expand All @@ -50,6 +62,7 @@
"typescript": "^4.7.4",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
"webpack-dev-server": "^4.9.3",
"yargs": "^17.5.1"
}
}
155 changes: 155 additions & 0 deletions scripts/license-header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env -S node -r ts-node/register/transpile-only

// Copyright © 2022 The Radicle Design System Contributors
//
// This file is part of radicle-design-system, distributed under the GPLv3
// with Radicle Linking Exception. For full terms see the included
// LICENSE file.

import yargs from "yargs";
import * as fs from "fs/promises";
import * as Path from "path";
import execa from "execa";

// Error that is shown without a stacktrace to the user
class UserError extends Error {
public constructor(public message: string) {
super(message);
}
}

async function main() {
yargs
.command<{ files: string[] | undefined }>({
command: "check [files...]",
describe: "Check presence of license headers in files",
builder: yargs => {
return yargs.positional("files", {
describe:
"Files to check. If not provided, all files are checked for a license header",
array: true,
type: "string",
});
},
handler: async ({ files }) => {
if (!files || files.length === 0) {
files = await getPaths();
}

let failure = false;
for (const file of files) {
if (!requireLicenseHeader(file)) {
continue;
}

const content = await fs.readFile(file, "utf8");
if (!hasLicenseHeader(content)) {
failure = true;
console.error(`License missing from ${file}`);
}
}

if (failure) {
throw new UserError(
"License headers missing. Run `./scripts/license-header.ts add` to fix this."
);
}
},
})
.command({
command: "add",
describe: "Add missing license headers to files",
handler: async () => {
for (const path of await getPaths()) {
const content = await fs.readFile(path, "utf8");
if (!hasLicenseHeader(content)) {
console.log(`Writing license to ${path}`);
const licenseComment = makeLicenseComment(Path.extname(path));
const fixedContent = `${licenseComment}${content}`;
await fs.writeFile(path, fixedContent, "utf8");
}
}
},
})
.version(false)
.strict()
.wrap(Math.min(100, yargs.terminalWidth()))
// For `UserError` we don’t show the stack trace. We also don’t show the help
// when an error is thrown.
.fail((msg, err, yargs) => {
if (err === undefined) {
yargs.showHelp("error");
console.error("");
console.error(msg);
} else if (err instanceof UserError) {
console.error(err.message);
} else {
console.error(err);
}
process.exit(1);
})
.demandCommand().argv;
}

const licenseHeaderContent = [
` Copyright © ${new Date().getFullYear()} The Radicle Design System Contributors`,
"",
" This file is part of radicle-design-system, distributed under the GPLv3",
" with Radicle Linking Exception. For full terms see the included",
" LICENSE file.",
];

function makeLicenseComment(extName: string): string {
if (extName === ".js" || extName === ".ts" || extName === ".rs") {
const commentLines = licenseHeaderContent.map(x => `//${x}`);
return `${commentLines.join("\n")}\n\n`;
} else if (extName === ".sh") {
const commentLines = licenseHeaderContent.map(x => `#${x}`);
return `${commentLines.join("\n")}\n\n`;
} else if (extName === ".svelte") {
return `<!--\n${licenseHeaderContent.join("\n")}\n-->\n`;
} else if (extName === ".css") {
const commentLines = licenseHeaderContent.map(x => ` *${x}`);
return `/**\n${commentLines.join("\n")}\n */\n`;
} else {
throw new Error(`Unknown file extension ${extName}`);
}
}

const EXTENSIONS = [".js", ".rs", ".sh", ".ts", ".svelte"];

// Returns true if the file at path requires a license header. This is
// `true` if the path has one of `EXTENSIONS`.
function requireLicenseHeader(path: string): boolean {
if (path.endsWith("typings/node-fetch.d.ts")) {
return false;
} else {
return EXTENSIONS.includes(Path.extname(path));
}
}

// Returns the list of file paths that should include license headers.
//
// The list consists of all files checked into version control that
// have one of `EXTENSIONS`.
async function getPaths(): Promise<string[]> {
const result = await execa("git", ["ls-files"]);
const gitPaths = result.stdout.split("\n");
return gitPaths.filter(path => {
return EXTENSIONS.includes(Path.extname(path));
});
}

// Pattern we use to check for the presence for the license headers in
// a given line.
const licenseHeaderPattern =
/Copyright © \d{4} The Radicle Design System Contributors/;

function hasLicenseHeader(fileContent: string): boolean {
// We check the first three lines to account for shebangs and comment
// starts.
const head = fileContent.split("\n").slice(0, 3);
return head.some(line => licenseHeaderPattern.test(line));
}

main();
5 changes: 1 addition & 4 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
{
"include": [
"webpack.config.ts",
"*.ts"
],
"include": ["webpack.config.ts", "**/*.ts"],
"exclude": ["node_modules/*"],
"compilerOptions": {
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
Expand Down
10 changes: 5 additions & 5 deletions webpack.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright © 2021 The Radicle Upstream Contributors
// Copyright © 2022 The Radicle Design System Contributors
//
// This file is part of radicle-upstream, distributed under the GPLv3
// This file is part of radicle-design-system, distributed under the GPLv3
// with Radicle Linking Exception. For full terms see the included
// LICENSE file.

Expand Down Expand Up @@ -114,7 +114,7 @@ const allowedLicenses = [
"WTFPL",
// http://www.gnu.org/licenses/license-list.html#ZLib
// "Zlib",
].map((x) => spdxExpressionParse(x));
].map(x => spdxExpressionParse(x));

function licensePlugin(): WebpackPluginInstance {
const plugin = new LicenseWebpackPlugin({
Expand All @@ -140,7 +140,7 @@ function licensePlugin(): WebpackPluginInstance {
// properly. https://github.com/twitter/twemoji/pull/499
twemoji: "MIT AND CC-BY-4.0",
},
unacceptableLicenseTest: (licenseName) => {
unacceptableLicenseTest: licenseName => {
if (licenseName) {
return !spdxWhitelisted(
spdxExpressionParse(licenseName),
Expand All @@ -150,7 +150,7 @@ function licensePlugin(): WebpackPluginInstance {
return true;
}
},
excludedPackageTest: (packageName) => {
excludedPackageTest: packageName => {
// These packages have fake `package.json` files in
// subdirectories. We don’t want to pick those up.
return (
Expand Down
Loading

0 comments on commit 4e08819

Please sign in to comment.