@verekia/warden 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -62
- package/src/checks/ci-workflow.ts +58 -0
- package/src/checks/config-files-present.ts +3 -1
- package/src/checks/exact-dependency-versions.ts +6 -1
- package/src/checks/matching-dependency-versions.ts +3 -1
- package/src/checks/next-config.ts +11 -2
- package/src/checks/portless-next-dev.ts +5 -3
- package/src/checks/required-scripts.ts +8 -1
- package/src/checks/test-script-consistency.ts +9 -4
- package/src/index.ts +4 -0
- package/src/types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verekia/warden",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Linter that checks repositories share consistent configs and tool versions.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"warden": "./src/index.ts"
|
|
@@ -37,71 +37,11 @@
|
|
|
37
37
|
"../svg-to-tsl"
|
|
38
38
|
],
|
|
39
39
|
"checks": {
|
|
40
|
-
"configFilesPresent": {
|
|
41
|
-
"enabled": true,
|
|
42
|
-
"files": [
|
|
43
|
-
".oxfmtrc.json",
|
|
44
|
-
".oxlintrc.json",
|
|
45
|
-
".github/workflows/ci.yml"
|
|
46
|
-
]
|
|
47
|
-
},
|
|
48
|
-
"matchingDependencyVersions": {
|
|
49
|
-
"enabled": true,
|
|
50
|
-
"packages": [
|
|
51
|
-
"oxfmt",
|
|
52
|
-
"oxlint"
|
|
53
|
-
]
|
|
54
|
-
},
|
|
55
|
-
"exactDependencyVersions": {
|
|
56
|
-
"enabled": true,
|
|
57
|
-
"packages": {
|
|
58
|
-
"oxfmt": "0.48.0",
|
|
59
|
-
"oxlint": "1.63.0"
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
"requiredScripts": {
|
|
63
|
-
"enabled": true,
|
|
64
|
-
"scripts": {
|
|
65
|
-
"format": "oxfmt .",
|
|
66
|
-
"format:check": "oxfmt --check .",
|
|
67
|
-
"lint": "oxlint .",
|
|
68
|
-
"lint:fix": "oxlint --fix ."
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
40
|
"typecheckScript": {
|
|
72
|
-
"enabled": true,
|
|
73
41
|
"modes": {
|
|
42
|
+
"gputex": "bun-filter-all",
|
|
74
43
|
"nanothree": "bun-filter-all"
|
|
75
44
|
}
|
|
76
|
-
},
|
|
77
|
-
"testScriptConsistency": {
|
|
78
|
-
"enabled": true,
|
|
79
|
-
"testFilePattern": "**/*.test.{ts,tsx,js,jsx}",
|
|
80
|
-
"testScript": "bun test",
|
|
81
|
-
"allWithoutTests": "bun run format:check && bun run lint && bun run typecheck && bun run warden",
|
|
82
|
-
"allWithTests": "bun run format:check && bun run lint && bun run typecheck && bun run warden && bun run test"
|
|
83
|
-
},
|
|
84
|
-
"tailwindOxfmtConfig": {
|
|
85
|
-
"enabled": true
|
|
86
|
-
},
|
|
87
|
-
"portlessNextDev": {
|
|
88
|
-
"enabled": true,
|
|
89
|
-
"version": "0.13.0"
|
|
90
|
-
},
|
|
91
|
-
"nextConfig": {
|
|
92
|
-
"enabled": true,
|
|
93
|
-
"options": {
|
|
94
|
-
"reactStrictMode": true,
|
|
95
|
-
"reactCompiler": true,
|
|
96
|
-
"output": "export"
|
|
97
|
-
},
|
|
98
|
-
"devDependencies": {
|
|
99
|
-
"babel-plugin-react-compiler": "1.0.0"
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
"wardenScript": {
|
|
103
|
-
"enabled": true,
|
|
104
|
-
"package": "@verekia/warden"
|
|
105
45
|
}
|
|
106
46
|
}
|
|
107
47
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CheckResult, Project, WardenConfig } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
type Step = { run?: unknown }
|
|
4
|
+
type Job = { steps?: unknown }
|
|
5
|
+
type Workflow = { jobs?: Record<string, unknown> }
|
|
6
|
+
|
|
7
|
+
const DEFAULT_FILE = '.github/workflows/ci.yml'
|
|
8
|
+
const DEFAULT_RUNS = 'bun run all'
|
|
9
|
+
|
|
10
|
+
const collectRunCommands = (workflow: Workflow): string[] => {
|
|
11
|
+
const commands: string[] = []
|
|
12
|
+
for (const job of Object.values(workflow.jobs ?? {})) {
|
|
13
|
+
const steps = (job as Job)?.steps
|
|
14
|
+
if (!Array.isArray(steps)) continue
|
|
15
|
+
for (const step of steps as Step[]) {
|
|
16
|
+
if (typeof step?.run === 'string') commands.push(step.run)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return commands
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const ciWorkflow = async (
|
|
23
|
+
projects: Project[],
|
|
24
|
+
options: NonNullable<NonNullable<WardenConfig['checks']>['ciWorkflow']>,
|
|
25
|
+
): Promise<CheckResult> => {
|
|
26
|
+
const messages: string[] = []
|
|
27
|
+
let passed = true
|
|
28
|
+
|
|
29
|
+
const file = options.file ?? DEFAULT_FILE
|
|
30
|
+
const runs = options.runs ?? DEFAULT_RUNS
|
|
31
|
+
|
|
32
|
+
for (const project of projects) {
|
|
33
|
+
const handle = Bun.file(`${project.path}/${file}`)
|
|
34
|
+
if (!(await handle.exists())) {
|
|
35
|
+
passed = false
|
|
36
|
+
messages.push(`${project.name}: missing ${file}`)
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
if (!runs) continue
|
|
40
|
+
|
|
41
|
+
let workflow: Workflow
|
|
42
|
+
try {
|
|
43
|
+
workflow = Bun.YAML.parse(await handle.text()) as Workflow
|
|
44
|
+
} catch (err) {
|
|
45
|
+
passed = false
|
|
46
|
+
messages.push(`${project.name}: ${file} is not valid YAML (${(err as Error).message})`)
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const commands = collectRunCommands(workflow)
|
|
51
|
+
if (!commands.some(cmd => cmd.includes(runs))) {
|
|
52
|
+
passed = false
|
|
53
|
+
messages.push(`${project.name}: ${file} has no step running ${JSON.stringify(runs)}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { name: 'ciWorkflow', passed, messages }
|
|
58
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { CheckResult, Project, WardenConfig } from '../types.ts'
|
|
2
2
|
|
|
3
|
+
const DEFAULT_FILES = ['.oxfmtrc.json', '.oxlintrc.json']
|
|
4
|
+
|
|
3
5
|
export const configFilesPresent = async (
|
|
4
6
|
projects: Project[],
|
|
5
7
|
options: NonNullable<NonNullable<WardenConfig['checks']>['configFilesPresent']>,
|
|
@@ -7,7 +9,7 @@ export const configFilesPresent = async (
|
|
|
7
9
|
const messages: string[] = []
|
|
8
10
|
let passed = true
|
|
9
11
|
|
|
10
|
-
const files = options.files ??
|
|
12
|
+
const files = options.files ?? DEFAULT_FILES
|
|
11
13
|
for (const project of projects) {
|
|
12
14
|
for (const file of files) {
|
|
13
15
|
const exists = await Bun.file(`${project.path}/${file}`).exists()
|
|
@@ -5,6 +5,11 @@ const findVersion = (pkg: PackageJson, name: string): string | undefined =>
|
|
|
5
5
|
|
|
6
6
|
const EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/
|
|
7
7
|
|
|
8
|
+
const DEFAULT_PACKAGES: Record<string, string> = {
|
|
9
|
+
oxfmt: '0.48.0',
|
|
10
|
+
oxlint: '1.63.0',
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export const exactDependencyVersions = (
|
|
9
14
|
projects: Project[],
|
|
10
15
|
options: NonNullable<NonNullable<WardenConfig['checks']>['exactDependencyVersions']>,
|
|
@@ -12,7 +17,7 @@ export const exactDependencyVersions = (
|
|
|
12
17
|
const messages: string[] = []
|
|
13
18
|
let passed = true
|
|
14
19
|
|
|
15
|
-
const packages = Object.entries(options.packages ??
|
|
20
|
+
const packages = Object.entries(options.packages ?? DEFAULT_PACKAGES)
|
|
16
21
|
if (packages.length === 0) {
|
|
17
22
|
return { name: 'exactDependencyVersions', passed, messages }
|
|
18
23
|
}
|
|
@@ -3,6 +3,8 @@ import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.t
|
|
|
3
3
|
const findVersion = (pkg: PackageJson, name: string): string | undefined =>
|
|
4
4
|
pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? pkg.peerDependencies?.[name]
|
|
5
5
|
|
|
6
|
+
const DEFAULT_PACKAGES = ['oxfmt', 'oxlint']
|
|
7
|
+
|
|
6
8
|
export const matchingDependencyVersions = (
|
|
7
9
|
projects: Project[],
|
|
8
10
|
options: NonNullable<NonNullable<WardenConfig['checks']>['matchingDependencyVersions']>,
|
|
@@ -10,7 +12,7 @@ export const matchingDependencyVersions = (
|
|
|
10
12
|
const messages: string[] = []
|
|
11
13
|
let passed = true
|
|
12
14
|
|
|
13
|
-
for (const packageName of options.packages ??
|
|
15
|
+
for (const packageName of options.packages ?? DEFAULT_PACKAGES) {
|
|
14
16
|
const versionToProjects = new Map<string, string[]>()
|
|
15
17
|
const missing: string[] = []
|
|
16
18
|
|
|
@@ -6,6 +6,15 @@ import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.t
|
|
|
6
6
|
const NEXT = 'next'
|
|
7
7
|
const CONFIG_VARIANTS = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs'] as const
|
|
8
8
|
|
|
9
|
+
const DEFAULT_OPTIONS: Record<string, unknown> = {
|
|
10
|
+
reactStrictMode: true,
|
|
11
|
+
reactCompiler: true,
|
|
12
|
+
output: 'export',
|
|
13
|
+
}
|
|
14
|
+
const DEFAULT_DEV_DEPENDENCIES: Record<string, string> = {
|
|
15
|
+
'babel-plugin-react-compiler': '1.0.0',
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
type WorkspaceField = string[] | { packages?: string[] } | undefined
|
|
10
19
|
|
|
11
20
|
const dependsOnNext = (pkg: PackageJson): boolean =>
|
|
@@ -106,7 +115,7 @@ export const nextConfig = async (
|
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
if (cfg) {
|
|
109
|
-
for (const [key, expected] of Object.entries(options.options ??
|
|
118
|
+
for (const [key, expected] of Object.entries(options.options ?? DEFAULT_OPTIONS)) {
|
|
110
119
|
if (!equal(cfg[key], expected)) {
|
|
111
120
|
passed = false
|
|
112
121
|
const actual = key in cfg ? JSON.stringify(cfg[key]) : 'missing'
|
|
@@ -118,7 +127,7 @@ export const nextConfig = async (
|
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
for (const [name, expected] of Object.entries(options.devDependencies ??
|
|
130
|
+
for (const [name, expected] of Object.entries(options.devDependencies ?? DEFAULT_DEV_DEPENDENCIES)) {
|
|
122
131
|
const declared = pkg.devDependencies?.[name]
|
|
123
132
|
if (declared !== expected) {
|
|
124
133
|
const actual = declared === undefined ? 'missing' : JSON.stringify(declared)
|
|
@@ -5,6 +5,7 @@ import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.t
|
|
|
5
5
|
|
|
6
6
|
const NEXT_DEV_RE = /\bnext dev\b/
|
|
7
7
|
const PORTLESS_WRAP_RE = /\bportless\s+\S+\s+next dev\b/
|
|
8
|
+
const DEFAULT_VERSION = '0.13.0'
|
|
8
9
|
|
|
9
10
|
type WorkspaceField = string[] | { packages?: string[] } | undefined
|
|
10
11
|
|
|
@@ -66,13 +67,14 @@ export const portlessNextDev = async (
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
if (usesNextDev
|
|
70
|
+
if (usesNextDev) {
|
|
71
|
+
const expectedVersion = options.version ?? DEFAULT_VERSION
|
|
70
72
|
const declared = pkg.devDependencies?.portless
|
|
71
|
-
if (declared !==
|
|
73
|
+
if (declared !== expectedVersion) {
|
|
72
74
|
const actual = declared === undefined ? 'missing' : JSON.stringify(declared)
|
|
73
75
|
passed = false
|
|
74
76
|
messages.push(
|
|
75
|
-
`${project.name}/${relPath}: uses "next dev" but devDependencies.portless is ${actual} — expected exact "${
|
|
77
|
+
`${project.name}/${relPath}: uses "next dev" but devDependencies.portless is ${actual} — expected exact "${expectedVersion}" (must be declared in this package.json)`,
|
|
76
78
|
)
|
|
77
79
|
}
|
|
78
80
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { CheckResult, Project, WardenConfig } from '../types.ts'
|
|
2
2
|
|
|
3
|
+
const DEFAULT_SCRIPTS: Record<string, string> = {
|
|
4
|
+
format: 'oxfmt .',
|
|
5
|
+
'format:check': 'oxfmt --check .',
|
|
6
|
+
lint: 'oxlint .',
|
|
7
|
+
'lint:fix': 'oxlint --fix .',
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
export const requiredScripts = (
|
|
4
11
|
projects: Project[],
|
|
5
12
|
options: NonNullable<NonNullable<WardenConfig['checks']>['requiredScripts']>,
|
|
@@ -7,7 +14,7 @@ export const requiredScripts = (
|
|
|
7
14
|
const messages: string[] = []
|
|
8
15
|
let passed = true
|
|
9
16
|
|
|
10
|
-
const required = Object.entries(options.scripts ??
|
|
17
|
+
const required = Object.entries(options.scripts ?? DEFAULT_SCRIPTS)
|
|
11
18
|
if (required.length === 0) {
|
|
12
19
|
return { name: 'requiredScripts', passed, messages }
|
|
13
20
|
}
|
|
@@ -13,6 +13,9 @@ const projectHasTests = async (projectPath: string, pattern: string): Promise<bo
|
|
|
13
13
|
|
|
14
14
|
const DEFAULT_TEST_FILE_PATTERN = '**/*.test.{ts,tsx,js,jsx}'
|
|
15
15
|
const DEFAULT_TEST_SCRIPT = 'bun test'
|
|
16
|
+
const DEFAULT_ALL_WITHOUT_TESTS = 'bun run format:check && bun run lint && bun run typecheck && bun run warden'
|
|
17
|
+
const DEFAULT_ALL_WITH_TESTS =
|
|
18
|
+
'bun run format:check && bun run lint && bun run typecheck && bun run warden && bun run test'
|
|
16
19
|
|
|
17
20
|
export const testScriptConsistency = async (
|
|
18
21
|
projects: Project[],
|
|
@@ -23,6 +26,8 @@ export const testScriptConsistency = async (
|
|
|
23
26
|
|
|
24
27
|
const testFilePattern = options.testFilePattern ?? DEFAULT_TEST_FILE_PATTERN
|
|
25
28
|
const testScript = options.testScript ?? DEFAULT_TEST_SCRIPT
|
|
29
|
+
const allWithTests = options.allWithTests ?? DEFAULT_ALL_WITH_TESTS
|
|
30
|
+
const allWithoutTests = options.allWithoutTests ?? DEFAULT_ALL_WITHOUT_TESTS
|
|
26
31
|
|
|
27
32
|
for (const project of projects) {
|
|
28
33
|
const scripts = project.packageJson?.scripts ?? {}
|
|
@@ -35,17 +40,17 @@ export const testScriptConsistency = async (
|
|
|
35
40
|
`${project.name}: has test files but "test" script is ${JSON.stringify(scripts.test)}, expected ${JSON.stringify(testScript)}`,
|
|
36
41
|
)
|
|
37
42
|
}
|
|
38
|
-
if (
|
|
43
|
+
if (scripts.all !== allWithTests) {
|
|
39
44
|
passed = false
|
|
40
45
|
messages.push(
|
|
41
|
-
`${project.name}: "all" is ${JSON.stringify(scripts.all)}, expected ${JSON.stringify(
|
|
46
|
+
`${project.name}: "all" is ${JSON.stringify(scripts.all)}, expected ${JSON.stringify(allWithTests)} (project has tests)`,
|
|
42
47
|
)
|
|
43
48
|
}
|
|
44
49
|
} else {
|
|
45
|
-
if (
|
|
50
|
+
if (scripts.all !== allWithoutTests) {
|
|
46
51
|
passed = false
|
|
47
52
|
messages.push(
|
|
48
|
-
`${project.name}: "all" is ${JSON.stringify(scripts.all)}, expected ${JSON.stringify(
|
|
53
|
+
`${project.name}: "all" is ${JSON.stringify(scripts.all)}, expected ${JSON.stringify(allWithoutTests)} (no tests detected)`,
|
|
49
54
|
)
|
|
50
55
|
}
|
|
51
56
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
3
|
|
|
4
|
+
import { ciWorkflow } from './checks/ci-workflow.ts'
|
|
4
5
|
import { configFilesPresent } from './checks/config-files-present.ts'
|
|
5
6
|
import { exactDependencyVersions } from './checks/exact-dependency-versions.ts'
|
|
6
7
|
import { matchingDependencyVersions } from './checks/matching-dependency-versions.ts'
|
|
@@ -79,6 +80,9 @@ if (enabled(checks.nextConfig)) {
|
|
|
79
80
|
if (enabled(checks.wardenScript)) {
|
|
80
81
|
results.push(wardenScript(projects, checks.wardenScript ?? {}))
|
|
81
82
|
}
|
|
83
|
+
if (enabled(checks.ciWorkflow)) {
|
|
84
|
+
results.push(await ciWorkflow(projects, checks.ciWorkflow ?? {}))
|
|
85
|
+
}
|
|
82
86
|
|
|
83
87
|
for (const result of results) {
|
|
84
88
|
const tag = result.passed ? '[PASS]' : '[FAIL]'
|