Skip to content

Commit 4e08819

Browse files
committed
Add proper linting and pre-commit hooks
Signed-off-by: Rūdolfs Ošiņš <[email protected]>
1 parent 96ee93f commit 4e08819

File tree

8 files changed

+1356
-35
lines changed

8 files changed

+1356
-35
lines changed

.eslintrc.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright © 2022 The Radicle Design System Contributors
2+
//
3+
// This file is part of radicle-design-system, distributed under the GPLv3
4+
// with Radicle Linking Exception. For full terms see the included
5+
// LICENSE file.
6+
7+
module.exports = {
8+
env: {
9+
node: true,
10+
browser: true,
11+
es6: true,
12+
},
13+
parser: "@typescript-eslint/parser",
14+
parserOptions: {
15+
createDefaultProgram: true,
16+
ecmaVersion: 2019,
17+
sourceType: "module",
18+
},
19+
ignorePatterns: ["!.license-compliancerc.js"],
20+
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
21+
plugins: ["svelte3", "@typescript-eslint"],
22+
overrides: [
23+
{
24+
files: ["*.svelte"],
25+
processor: "svelte3/svelte3",
26+
},
27+
{
28+
files: ["scripts/*.ts"],
29+
rules: {
30+
// Script files are not bundled so we can’t use module imports.
31+
"@typescript-eslint/no-var-requires": "off",
32+
},
33+
},
34+
{
35+
files: ["*.js", "*.mjs"],
36+
rules: {
37+
"@typescript-eslint/explicit-module-boundary-types": "off",
38+
},
39+
},
40+
],
41+
rules: {
42+
// Disallow Unused Variables
43+
// https://eslint.org/docs/rules/no-unused-vars
44+
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
45+
// require using arrow functions as callbacks
46+
// https://eslint.org/docs/rules/prefer-arrow-callback
47+
"prefer-arrow-callback": "error",
48+
// require using template literals instead of string concatenation
49+
// http://eslint.org/docs/rules/prefer-template
50+
"prefer-template": "error",
51+
// require using of const declaration for variables that are never modified after declared
52+
// https://eslint.org/docs/rules/prefer-const
53+
"prefer-const": "error",
54+
// disallow modifying variables that are declared using const
55+
// https://eslint.org/docs/rules/no-const-assign
56+
"no-const-assign": "error",
57+
// require let or const instead of var
58+
// https://eslint.org/docs/rules/no-var
59+
"no-var": "error",
60+
// require at least one whitespace after comments( // and /*)
61+
// https://eslint.org/docs/rules/spaced-comment
62+
"spaced-comment": ["warn", "always"],
63+
// Require `===` and `!==` comparisons
64+
eqeqeq: "error",
65+
// Enforce curly braces for if/else statements for better clarity.
66+
curly: "error",
67+
68+
// We are ok with providing explict type annotations for additional
69+
// clarity.
70+
"@typescript-eslint/no-inferrable-types": "off",
71+
// We are ok with empty functions. Often we need a no-op function
72+
// as an argument.
73+
"@typescript-eslint/no-empty-function": "off",
74+
"@typescript-eslint/no-implicit-any-catch": "error",
75+
"@typescript-eslint/explicit-member-accessibility": "error",
76+
"@typescript-eslint/explicit-module-boundary-types": "error",
77+
},
78+
settings: {
79+
"svelte3/typescript": true,
80+
},
81+
};

.husky/pre-commit

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/sh
2+
. "$(dirname "$0")/_/husky.sh"
3+
4+
yarn run lint-staged

.lintstagedrc.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
"*.{ts,js,css,json,html,svelte}":
1+
"*.{ts,css,json,html,svelte}":
22
- prettier --write
3-
"*.{js,ts,svelte}":
3+
"*.{ts,svelte}":
44
- eslint --fix --max-warnings=0
55
"*":
66
- ./scripts/license-header.ts check

package.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"url": "https://github.com/radicle-dev/radicle-design-system.git"
1414
},
1515
"scripts": {
16-
"start": "webpack serve --open --config-name design-system"
16+
"start": "webpack serve --open --config-name design-system",
17+
"lint": "eslint . --ignore-path .gitignore --ext .js,.svelte,.ts --max-warnings=0",
18+
"postinstall": "husky install"
1719
},
1820
"dependencies": {
1921
"@types/lodash-es": "^4.17.6",
@@ -34,10 +36,20 @@
3436
"*.otf"
3537
],
3638
"devDependencies": {
39+
"@types/yargs": "^17.0.11",
40+
"@typescript-eslint/eslint-plugin": "^5.32.0",
41+
"@typescript-eslint/parser": "^5.32.0",
3742
"copy-webpack-plugin": "^11.0.0",
3843
"crypto-browserify": "^3.12.0",
44+
"eslint": "^8.21.0",
45+
"eslint-plugin-svelte3": "^4.0.0",
46+
"execa": "^5.1.1",
3947
"html-webpack-plugin": "^5.5.0",
48+
"husky": "^8.0.1",
4049
"license-webpack-plugin": "^4.0.2",
50+
"lint-staged": "^13.0.3",
51+
"prettier": "^2.7.1",
52+
"prettier-plugin-svelte": "^2.7.0",
4153
"process": "^0.11.10",
4254
"spdx-expression-parse": "^3.0.1",
4355
"spdx-whitelisted": "^1.0.0",
@@ -50,6 +62,7 @@
5062
"typescript": "^4.7.4",
5163
"webpack": "^5.74.0",
5264
"webpack-cli": "^4.10.0",
53-
"webpack-dev-server": "^4.9.3"
65+
"webpack-dev-server": "^4.9.3",
66+
"yargs": "^17.5.1"
5467
}
5568
}

scripts/license-header.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env -S node -r ts-node/register/transpile-only
2+
3+
// Copyright © 2022 The Radicle Design System Contributors
4+
//
5+
// This file is part of radicle-design-system, distributed under the GPLv3
6+
// with Radicle Linking Exception. For full terms see the included
7+
// LICENSE file.
8+
9+
import yargs from "yargs";
10+
import * as fs from "fs/promises";
11+
import * as Path from "path";
12+
import execa from "execa";
13+
14+
// Error that is shown without a stacktrace to the user
15+
class UserError extends Error {
16+
public constructor(public message: string) {
17+
super(message);
18+
}
19+
}
20+
21+
async function main() {
22+
yargs
23+
.command<{ files: string[] | undefined }>({
24+
command: "check [files...]",
25+
describe: "Check presence of license headers in files",
26+
builder: yargs => {
27+
return yargs.positional("files", {
28+
describe:
29+
"Files to check. If not provided, all files are checked for a license header",
30+
array: true,
31+
type: "string",
32+
});
33+
},
34+
handler: async ({ files }) => {
35+
if (!files || files.length === 0) {
36+
files = await getPaths();
37+
}
38+
39+
let failure = false;
40+
for (const file of files) {
41+
if (!requireLicenseHeader(file)) {
42+
continue;
43+
}
44+
45+
const content = await fs.readFile(file, "utf8");
46+
if (!hasLicenseHeader(content)) {
47+
failure = true;
48+
console.error(`License missing from ${file}`);
49+
}
50+
}
51+
52+
if (failure) {
53+
throw new UserError(
54+
"License headers missing. Run `./scripts/license-header.ts add` to fix this."
55+
);
56+
}
57+
},
58+
})
59+
.command({
60+
command: "add",
61+
describe: "Add missing license headers to files",
62+
handler: async () => {
63+
for (const path of await getPaths()) {
64+
const content = await fs.readFile(path, "utf8");
65+
if (!hasLicenseHeader(content)) {
66+
console.log(`Writing license to ${path}`);
67+
const licenseComment = makeLicenseComment(Path.extname(path));
68+
const fixedContent = `${licenseComment}${content}`;
69+
await fs.writeFile(path, fixedContent, "utf8");
70+
}
71+
}
72+
},
73+
})
74+
.version(false)
75+
.strict()
76+
.wrap(Math.min(100, yargs.terminalWidth()))
77+
// For `UserError` we don’t show the stack trace. We also don’t show the help
78+
// when an error is thrown.
79+
.fail((msg, err, yargs) => {
80+
if (err === undefined) {
81+
yargs.showHelp("error");
82+
console.error("");
83+
console.error(msg);
84+
} else if (err instanceof UserError) {
85+
console.error(err.message);
86+
} else {
87+
console.error(err);
88+
}
89+
process.exit(1);
90+
})
91+
.demandCommand().argv;
92+
}
93+
94+
const licenseHeaderContent = [
95+
` Copyright © ${new Date().getFullYear()} The Radicle Design System Contributors`,
96+
"",
97+
" This file is part of radicle-design-system, distributed under the GPLv3",
98+
" with Radicle Linking Exception. For full terms see the included",
99+
" LICENSE file.",
100+
];
101+
102+
function makeLicenseComment(extName: string): string {
103+
if (extName === ".js" || extName === ".ts" || extName === ".rs") {
104+
const commentLines = licenseHeaderContent.map(x => `//${x}`);
105+
return `${commentLines.join("\n")}\n\n`;
106+
} else if (extName === ".sh") {
107+
const commentLines = licenseHeaderContent.map(x => `#${x}`);
108+
return `${commentLines.join("\n")}\n\n`;
109+
} else if (extName === ".svelte") {
110+
return `<!--\n${licenseHeaderContent.join("\n")}\n-->\n`;
111+
} else if (extName === ".css") {
112+
const commentLines = licenseHeaderContent.map(x => ` *${x}`);
113+
return `/**\n${commentLines.join("\n")}\n */\n`;
114+
} else {
115+
throw new Error(`Unknown file extension ${extName}`);
116+
}
117+
}
118+
119+
const EXTENSIONS = [".js", ".rs", ".sh", ".ts", ".svelte"];
120+
121+
// Returns true if the file at path requires a license header. This is
122+
// `true` if the path has one of `EXTENSIONS`.
123+
function requireLicenseHeader(path: string): boolean {
124+
if (path.endsWith("typings/node-fetch.d.ts")) {
125+
return false;
126+
} else {
127+
return EXTENSIONS.includes(Path.extname(path));
128+
}
129+
}
130+
131+
// Returns the list of file paths that should include license headers.
132+
//
133+
// The list consists of all files checked into version control that
134+
// have one of `EXTENSIONS`.
135+
async function getPaths(): Promise<string[]> {
136+
const result = await execa("git", ["ls-files"]);
137+
const gitPaths = result.stdout.split("\n");
138+
return gitPaths.filter(path => {
139+
return EXTENSIONS.includes(Path.extname(path));
140+
});
141+
}
142+
143+
// Pattern we use to check for the presence for the license headers in
144+
// a given line.
145+
const licenseHeaderPattern =
146+
/Copyright © \d{4} The Radicle Design System Contributors/;
147+
148+
function hasLicenseHeader(fileContent: string): boolean {
149+
// We check the first three lines to account for shebangs and comment
150+
// starts.
151+
const head = fileContent.split("\n").slice(0, 3);
152+
return head.some(line => licenseHeaderPattern.test(line));
153+
}
154+
155+
main();

tsconfig.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
{
2-
"include": [
3-
"webpack.config.ts",
4-
"*.ts"
5-
],
2+
"include": ["webpack.config.ts", "**/*.ts"],
63
"exclude": ["node_modules/*"],
74
"compilerOptions": {
85
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */

webpack.config.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
// Copyright © 2021 The Radicle Upstream Contributors
1+
// Copyright © 2022 The Radicle Design System Contributors
22
//
3-
// This file is part of radicle-upstream, distributed under the GPLv3
3+
// This file is part of radicle-design-system, distributed under the GPLv3
44
// with Radicle Linking Exception. For full terms see the included
55
// LICENSE file.
66

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

119119
function licensePlugin(): WebpackPluginInstance {
120120
const plugin = new LicenseWebpackPlugin({
@@ -140,7 +140,7 @@ function licensePlugin(): WebpackPluginInstance {
140140
// properly. https://github.com/twitter/twemoji/pull/499
141141
twemoji: "MIT AND CC-BY-4.0",
142142
},
143-
unacceptableLicenseTest: (licenseName) => {
143+
unacceptableLicenseTest: licenseName => {
144144
if (licenseName) {
145145
return !spdxWhitelisted(
146146
spdxExpressionParse(licenseName),
@@ -150,7 +150,7 @@ function licensePlugin(): WebpackPluginInstance {
150150
return true;
151151
}
152152
},
153-
excludedPackageTest: (packageName) => {
153+
excludedPackageTest: packageName => {
154154
// These packages have fake `package.json` files in
155155
// subdirectories. We don’t want to pick those up.
156156
return (

0 commit comments

Comments
 (0)