Skip to content

Commit 7fa1769

Browse files
authored
feat: enable eslint (#108)
Signed-off-by: Filipe Mota <filipe@unevenlabs.com>
1 parent e1fecd7 commit 7fa1769

File tree

7 files changed

+229
-131
lines changed

7 files changed

+229
-131
lines changed

eslint-rules/pinned-deps.cjs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"use strict";
2+
3+
/**
4+
* Enforce pinned deps in package.json:
5+
* - allow protocols (workspace:, file:, link:, etc.) for internal packages
6+
* - require exact x.y.z for everything else
7+
* - no autofix
8+
*/
9+
10+
const DEFAULT_DEP_FIELDS = [
11+
"dependencies",
12+
"devDependencies",
13+
"peerDependencies",
14+
"optionalDependencies",
15+
];
16+
17+
function isObjectExpression(node) {
18+
return node && (node.type === "ObjectExpression" || node.type === "JSONObjectExpression");
19+
}
20+
21+
function getPropKeyString(prop) {
22+
const k = prop.key;
23+
if (!k) return null;
24+
// JS parser: Literal
25+
if (k.type === "Literal" && typeof k.value === "string") return k.value;
26+
// jsonc-eslint-parser: JSONLiteral
27+
if (k.type === "JSONLiteral" && typeof k.value === "string") return k.value;
28+
return null;
29+
}
30+
31+
function getLiteralString(node) {
32+
if (!node) return null;
33+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
34+
if (node.type === "JSONLiteral" && typeof node.value === "string") return node.value;
35+
return null;
36+
}
37+
38+
function getObjectPropertyValue(objExpr, keyName) {
39+
if (!isObjectExpression(objExpr)) return null;
40+
for (const p of objExpr.properties || []) {
41+
if (!p || (p.type !== "Property" && p.type !== "JSONProperty")) continue;
42+
const k = getPropKeyString(p);
43+
if (k === keyName) return p.value;
44+
}
45+
return null;
46+
}
47+
48+
module.exports = {
49+
meta: {
50+
type: "problem",
51+
docs: { description: "Require pinned dependency versions in package.json" },
52+
schema: [
53+
{
54+
type: "object",
55+
additionalProperties: false,
56+
properties: {
57+
depFields: { type: "array", items: { type: "string" } },
58+
excludeList: { type: "array", items: { type: "string" } },
59+
internalScopes: { type: "array", items: { type: "string" } },
60+
allowProtocols: { type: "array", items: { type: "string" } },
61+
allowProtocolsOnlyForInternal: { type: "boolean" },
62+
allowExactPrerelease: { type: "boolean" },
63+
},
64+
},
65+
],
66+
},
67+
68+
create(context) {
69+
const opt = context.options[0] || {};
70+
const depFields = opt.depFields || DEFAULT_DEP_FIELDS;
71+
const excludeList = opt.excludeList || [];
72+
const internalScopes = opt.internalScopes || [];
73+
const allowProtocols = opt.allowProtocols || ["workspace:", "file:", "link:"];
74+
const allowProtocolsOnlyForInternal = opt.allowProtocolsOnlyForInternal !== false; // default true
75+
const allowExactPrerelease = !!opt.allowExactPrerelease;
76+
77+
const exact = allowExactPrerelease
78+
? /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/
79+
: /^[0-9]+\.[0-9]+\.[0-9]+$/;
80+
81+
function shouldExclude(name) {
82+
return excludeList.some((p) => name.startsWith(p));
83+
}
84+
85+
function isInternal(name) {
86+
return internalScopes.some((p) => name.startsWith(p));
87+
}
88+
89+
function isAllowedProtocol(version) {
90+
return allowProtocols.some((p) => version.startsWith(p));
91+
}
92+
93+
function report(node, depName, field, version) {
94+
context.report({
95+
node,
96+
message:
97+
`Dependency "${depName}" in "${field}" must be pinned to an exact version (x.y.z). ` +
98+
`Got "${version}".` +
99+
(internalScopes.length ? ` Internal scopes: ${internalScopes.join(", ")}.` : "") +
100+
(allowProtocols.length
101+
? ` Allowed protocols: ${allowProtocols.join(", ")}.` +
102+
(allowProtocolsOnlyForInternal ? " (internal only)" : "")
103+
: ""),
104+
});
105+
}
106+
107+
return {
108+
"Program:exit"(program) {
109+
const expr = program.body?.[0]?.expression;
110+
if (!isObjectExpression(expr)) return;
111+
112+
for (const field of depFields) {
113+
const depObj = getObjectPropertyValue(expr, field);
114+
if (!isObjectExpression(depObj)) continue;
115+
116+
for (const p of depObj.properties || []) {
117+
if (!p || (p.type !== "Property" && p.type !== "JSONProperty")) continue;
118+
119+
const depName = getPropKeyString(p);
120+
const version = getLiteralString(p.value);
121+
if (!depName || version == null) continue;
122+
123+
if (shouldExclude(depName)) {
124+
continue;
125+
}
126+
127+
// allow protocols
128+
if (isAllowedProtocol(version)) {
129+
if (allowProtocolsOnlyForInternal && !isInternal(depName)) {
130+
report(p.value, depName, field, version);
131+
}
132+
continue;
133+
}
134+
135+
// require exact semver
136+
if (!exact.test(version)) {
137+
report(p.value, depName, field, version);
138+
}
139+
}
140+
}
141+
},
142+
};
143+
},
144+
};

eslint.config.js

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ const tseslint = require("@typescript-eslint/eslint-plugin");
22
const tsParser = require("@typescript-eslint/parser");
33
const pluginJsonc = require("eslint-plugin-jsonc");
44
const jsonParser = require("jsonc-eslint-parser");
5-
const jsonDependencies = require("eslint-plugin-package-json-dependencies");
65

76
module.exports = [
87
{
@@ -17,57 +16,67 @@ module.exports = [
1716
"jsonc/no-dupe-keys": "error",
1817
},
1918
},
20-
// {
21-
// files: ["**/*.ts", "**/*.tsx"],
22-
// languageOptions: {
23-
// parser: tsParser,
24-
// parserOptions: {
25-
// project: ["./tsconfig.eslint.json"],
26-
// ecmaVersion: 2019,
27-
// sourceType: "module",
28-
// },
29-
// },
30-
// ignores: [],
31-
// plugins: {
32-
// "@typescript-eslint": tseslint,
33-
// },
34-
// rules: {
35-
// ...tseslint.configs["recommended"].rules,
36-
// "@typescript-eslint/no-non-null-assertion": "off",
37-
// "@typescript-eslint/no-empty-interface": "off",
38-
// "@typescript-eslint/no-unused-vars": "off",
39-
// "@typescript-eslint/no-explicit-any": "off",
40-
// "@typescript-eslint/ban-ts-comment": "off",
41-
// "@typescript-eslint/no-non-null-asserted-optional-chain": "off",
42-
// "@typescript-eslint/switch-exhaustiveness-check": "off",
43-
// "quotes": [
44-
// "error",
45-
// "double",
46-
// {
47-
// avoidEscape: true,
48-
// allowTemplateLiterals: true,
49-
// },
50-
// ],
51-
// "no-console": "error",
52-
// "no-self-compare": "error",
53-
// },
54-
// },
55-
// {
56-
// files: ["**/*.spec.ts"],
57-
// rules: {
58-
// "no-console": "off",
59-
// },
60-
// },
6119
{
62-
files: ["**/package.json"],
20+
files: ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"],
21+
languageOptions: {
22+
parser: tsParser,
23+
parserOptions: {
24+
project: ["./tsconfig.eslint.json"],
25+
ecmaVersion: 2019,
26+
sourceType: "module",
27+
},
28+
},
29+
ignores: [],
6330
plugins: {
64-
"package-json-deps": jsonDependencies,
31+
"@typescript-eslint": tseslint,
6532
},
66-
languageOptions: {
67-
parser: jsonParser,
33+
rules: {
34+
...tseslint.configs["recommended"].rules,
35+
"@typescript-eslint/no-non-null-assertion": "off",
36+
"@typescript-eslint/no-empty-interface": "off",
37+
"@typescript-eslint/no-unused-vars": "off",
38+
"@typescript-eslint/no-explicit-any": "off",
39+
"@typescript-eslint/ban-ts-comment": "off",
40+
"@typescript-eslint/no-non-null-asserted-optional-chain": "off",
41+
"@typescript-eslint/switch-exhaustiveness-check": "off",
42+
"quotes": [
43+
"error",
44+
"double",
45+
{
46+
avoidEscape: true,
47+
allowTemplateLiterals: true,
48+
},
49+
],
50+
"no-console": "error",
51+
"no-self-compare": "error",
52+
},
53+
},
54+
{
55+
files: ["**/package.json"],
56+
languageOptions: { parser: jsonParser },
57+
plugins: {
58+
"unevenlabs-policy": {
59+
rules: {
60+
"pinned-deps": require("./eslint-rules/pinned-deps.cjs"),
61+
},
62+
},
6863
},
6964
rules: {
70-
"package-json-deps/controlled-versions": ["error", { granularity: "patch" }],
65+
"unevenlabs-policy/pinned-deps": [
66+
"error",
67+
{
68+
excludeList: [
69+
"@berachain-foundation/berancer-sdk",
70+
"@nktkas/hyperliquid",
71+
"@solana-developers/helpers",
72+
"@solana/spl-token",
73+
],
74+
internalScopes: ["@relay-vaults/"],
75+
allowProtocols: ["workspace:", "^workspace:"],
76+
allowProtocolsOnlyForInternal: true,
77+
allowExactPrerelease: true,
78+
},
79+
],
7180
},
7281
},
7382
];

package.json

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,7 @@
1515
"strip-ansi": "5.x"
1616
},
1717
"lint-staged": {
18-
"package.json": [
19-
"eslint --max-warnings 0"
20-
],
21-
"tsconfig*.json": [
22-
"eslint --max-warnings 0 --fix"
23-
],
24-
"*.{js,ts}": [
18+
"*.{js,ts,json}": [
2519
"eslint --max-warnings 0 --fix"
2620
]
2721
},
@@ -68,7 +62,6 @@
6862
"dotenv": "16.6.1",
6963
"eslint": "9.39.2",
7064
"eslint-plugin-jsonc": "2.21.0",
71-
"eslint-plugin-package-json-dependencies": "1.0.20",
7265
"husky": "9.1.7",
7366
"jest": "29.7.0",
7467
"jsonc-eslint-parser": "2.4.2",

src/common/db.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import PgPromise from "pg-promise";
22

33
import { config } from "../config";
4-
import { getIamToken } from './aws';
4+
import { getIamToken } from "./aws";
55

66
export const pgp = PgPromise();
77

src/scripts/run-migrations.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// wrapper for node-pg-migrate to inject the database password
2-
import { spawnSync } from 'node:child_process';
3-
import { getDatabaseUrlWithPassword } from '../common/db'
2+
import { spawnSync } from "node:child_process";
3+
import { getDatabaseUrlWithPassword } from "../common/db"
44

55
(async () => {
66
process.env.POSTGRES_URL = await getDatabaseUrlWithPassword(String(process.env.POSTGRES_URL));
77

88
spawnSync(
9-
'node-pg-migrate',
9+
"node-pg-migrate",
1010
process.argv.slice(2),
11-
{ stdio: 'inherit' }
11+
{ stdio: "inherit" }
1212
);
1313
})();

tsconfig.eslint.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"noEmit": true,
55
"rootDir": "."
66
},
7-
"include": ["src/**/*.ts", "tests/**/*.ts"],
7+
"include": ["src/**/*.ts", "test/**/*.ts"],
88
"exclude": ["node_modules", "dist"]
99
}

0 commit comments

Comments
 (0)