@verekia/warden 0.0.3 → 0.0.5
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 +20 -4
- package/src/all-siblings.ts +53 -0
- package/src/checks/ci-workflow.ts +6 -10
- package/src/checks/config-files-present.ts +4 -6
- package/src/checks/exact-dependency-versions.ts +11 -15
- package/src/checks/matching-dependency-versions.ts +18 -12
- package/src/checks/next-config.ts +16 -18
- package/src/checks/pinned-dependency-versions.ts +99 -0
- package/src/checks/portless-next-dev.ts +11 -12
- package/src/checks/required-scripts.ts +13 -13
- package/src/checks/tailwind-oxfmt-config.ts +11 -10
- package/src/checks/test-script-consistency.ts +15 -16
- package/src/checks/typecheck-script.ts +12 -12
- package/src/checks/warden-script.ts +11 -12
- package/src/ci-status.ts +98 -0
- package/src/index.ts +32 -7
- package/src/types.ts +12 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verekia/warden",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Linter that checks repositories share consistent configs and tool versions.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"warden": "./src/index.ts"
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"type": "module",
|
|
12
12
|
"scripts": {
|
|
13
13
|
"warden": "bun run src/index.ts",
|
|
14
|
+
"all-siblings": "bun run src/all-siblings.ts",
|
|
15
|
+
"ci-status": "bun run src/ci-status.ts",
|
|
14
16
|
"format": "oxfmt .",
|
|
15
17
|
"format:check": "oxfmt --check .",
|
|
16
18
|
"lint": "oxlint .",
|
|
@@ -34,13 +36,27 @@
|
|
|
34
36
|
"../gputex",
|
|
35
37
|
"../gradient-normal-textures",
|
|
36
38
|
"../nanothree",
|
|
37
|
-
"../svg-to-
|
|
39
|
+
"../svg-to-msdf",
|
|
40
|
+
"../manalab",
|
|
41
|
+
"../tslfx",
|
|
42
|
+
"../polydraw",
|
|
43
|
+
"../r3f-gamedev",
|
|
44
|
+
"../webgamer",
|
|
45
|
+
"../voidcore",
|
|
46
|
+
"../jsblender"
|
|
38
47
|
],
|
|
39
48
|
"checks": {
|
|
40
49
|
"typecheckScript": {
|
|
41
50
|
"modes": {
|
|
42
|
-
"
|
|
43
|
-
|
|
51
|
+
"voidcore": "bun-filter-all"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"pinnedDependencyVersions": {
|
|
55
|
+
"exclude": {
|
|
56
|
+
"webgamer": [
|
|
57
|
+
"react",
|
|
58
|
+
"react-dom"
|
|
59
|
+
]
|
|
44
60
|
}
|
|
45
61
|
}
|
|
46
62
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
import { loadConfig, resolveProjects } from './config.ts'
|
|
5
|
+
|
|
6
|
+
const baseDir = process.cwd()
|
|
7
|
+
|
|
8
|
+
const config = await loadConfig(baseDir)
|
|
9
|
+
const allProjects = await resolveProjects(config, baseDir)
|
|
10
|
+
|
|
11
|
+
const filterNames = process.argv.slice(2)
|
|
12
|
+
const knownNames = new Set(allProjects.map(p => p.name))
|
|
13
|
+
const unknown = filterNames.filter(n => !knownNames.has(n))
|
|
14
|
+
if (unknown.length > 0) {
|
|
15
|
+
console.error(
|
|
16
|
+
`Unknown project(s): ${unknown.join(', ')}\nConfigured projects: ${allProjects.map(p => p.name).join(', ')}`,
|
|
17
|
+
)
|
|
18
|
+
process.exit(2)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const projects = filterNames.length > 0 ? allProjects.filter(p => filterNames.includes(p.name)) : allProjects
|
|
22
|
+
|
|
23
|
+
const missing = projects.filter(p => !existsSync(p.path))
|
|
24
|
+
if (missing.length > 0) {
|
|
25
|
+
const detail = missing.map(p => ` - ${p.name} (expected at ${p.path})`).join('\n')
|
|
26
|
+
const noun = missing.length === 1 ? 'directory is' : 'directories are'
|
|
27
|
+
console.error(`Cannot run all-siblings — the following project ${noun} not on disk:\n${detail}`)
|
|
28
|
+
process.exit(2)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(
|
|
32
|
+
`all-siblings — running "bun run all" in ${projects.length} project(s): ${projects.map(p => p.name).join(', ')}`,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const failed: string[] = []
|
|
36
|
+
for (const project of projects) {
|
|
37
|
+
console.log(`\n=== ${project.name} ===`)
|
|
38
|
+
const proc = Bun.spawn(['bun', 'run', 'all'], {
|
|
39
|
+
cwd: project.path,
|
|
40
|
+
stdout: 'inherit',
|
|
41
|
+
stderr: 'inherit',
|
|
42
|
+
})
|
|
43
|
+
const exitCode = await proc.exited
|
|
44
|
+
if (exitCode !== 0) {
|
|
45
|
+
failed.push(project.name)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`\n${projects.length - failed.length}/${projects.length} projects passed`)
|
|
50
|
+
if (failed.length > 0) {
|
|
51
|
+
console.log(`Failed: ${failed.join(', ')}`)
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckResult, Project, WardenConfig } from '../types.ts'
|
|
1
|
+
import type { CheckResult, Finding, Project, WardenConfig } from '../types.ts'
|
|
2
2
|
|
|
3
3
|
type Step = { run?: unknown }
|
|
4
4
|
type Job = { steps?: unknown }
|
|
@@ -23,8 +23,7 @@ export const ciWorkflow = async (
|
|
|
23
23
|
projects: Project[],
|
|
24
24
|
options: NonNullable<NonNullable<WardenConfig['checks']>['ciWorkflow']>,
|
|
25
25
|
): Promise<CheckResult> => {
|
|
26
|
-
const
|
|
27
|
-
let passed = true
|
|
26
|
+
const findings: Finding[] = []
|
|
28
27
|
|
|
29
28
|
const file = options.file ?? DEFAULT_FILE
|
|
30
29
|
const runs = options.runs ?? DEFAULT_RUNS
|
|
@@ -32,8 +31,7 @@ export const ciWorkflow = async (
|
|
|
32
31
|
for (const project of projects) {
|
|
33
32
|
const handle = Bun.file(`${project.path}/${file}`)
|
|
34
33
|
if (!(await handle.exists())) {
|
|
35
|
-
|
|
36
|
-
messages.push(`${project.name}: missing ${file}`)
|
|
34
|
+
findings.push({ project: project.name, message: `missing ${file}` })
|
|
37
35
|
continue
|
|
38
36
|
}
|
|
39
37
|
if (!runs) continue
|
|
@@ -42,17 +40,15 @@ export const ciWorkflow = async (
|
|
|
42
40
|
try {
|
|
43
41
|
workflow = Bun.YAML.parse(await handle.text()) as Workflow
|
|
44
42
|
} catch (err) {
|
|
45
|
-
|
|
46
|
-
messages.push(`${project.name}: ${file} is not valid YAML (${(err as Error).message})`)
|
|
43
|
+
findings.push({ project: project.name, message: `${file} is not valid YAML (${(err as Error).message})` })
|
|
47
44
|
continue
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
const commands = collectRunCommands(workflow)
|
|
51
48
|
if (!commands.some(cmd => cmd.includes(runs))) {
|
|
52
|
-
|
|
53
|
-
messages.push(`${project.name}: ${file} has no step running ${JSON.stringify(runs)}`)
|
|
49
|
+
findings.push({ project: project.name, message: `${file} has no step running ${JSON.stringify(runs)}` })
|
|
54
50
|
}
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
return { name: 'ciWorkflow',
|
|
53
|
+
return { name: 'ciWorkflow', findings }
|
|
58
54
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckResult, Project, WardenConfig } from '../types.ts'
|
|
1
|
+
import type { CheckResult, Finding, Project, WardenConfig } from '../types.ts'
|
|
2
2
|
|
|
3
3
|
const DEFAULT_FILES = ['.oxfmtrc.json', '.oxlintrc.json']
|
|
4
4
|
|
|
@@ -6,19 +6,17 @@ export const configFilesPresent = async (
|
|
|
6
6
|
projects: Project[],
|
|
7
7
|
options: NonNullable<NonNullable<WardenConfig['checks']>['configFilesPresent']>,
|
|
8
8
|
): Promise<CheckResult> => {
|
|
9
|
-
const
|
|
10
|
-
let passed = true
|
|
9
|
+
const findings: Finding[] = []
|
|
11
10
|
|
|
12
11
|
const files = options.files ?? DEFAULT_FILES
|
|
13
12
|
for (const project of projects) {
|
|
14
13
|
for (const file of files) {
|
|
15
14
|
const exists = await Bun.file(`${project.path}/${file}`).exists()
|
|
16
15
|
if (!exists) {
|
|
17
|
-
|
|
18
|
-
messages.push(`${project.name}: missing ${file}`)
|
|
16
|
+
findings.push({ project: project.name, message: `missing ${file}` })
|
|
19
17
|
}
|
|
20
18
|
}
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
return { name: 'configFilesPresent',
|
|
21
|
+
return { name: 'configFilesPresent', findings }
|
|
24
22
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
1
|
+
import type { CheckResult, Finding, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
2
2
|
|
|
3
3
|
const findVersion = (pkg: PackageJson, name: string): string | undefined =>
|
|
4
4
|
pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? pkg.peerDependencies?.[name]
|
|
@@ -14,40 +14,36 @@ export const exactDependencyVersions = (
|
|
|
14
14
|
projects: Project[],
|
|
15
15
|
options: NonNullable<NonNullable<WardenConfig['checks']>['exactDependencyVersions']>,
|
|
16
16
|
): CheckResult => {
|
|
17
|
-
const
|
|
18
|
-
let passed = true
|
|
17
|
+
const findings: Finding[] = []
|
|
19
18
|
|
|
20
19
|
const packages = Object.entries(options.packages ?? DEFAULT_PACKAGES)
|
|
21
20
|
if (packages.length === 0) {
|
|
22
|
-
return { name: 'exactDependencyVersions',
|
|
21
|
+
return { name: 'exactDependencyVersions', findings }
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
for (const project of projects) {
|
|
26
25
|
if (!project.packageJson) {
|
|
27
|
-
|
|
28
|
-
messages.push(`${project.name}: no package.json`)
|
|
26
|
+
findings.push({ project: project.name, message: 'no package.json' })
|
|
29
27
|
continue
|
|
30
28
|
}
|
|
31
29
|
for (const [name, expected] of packages) {
|
|
32
30
|
const declared = findVersion(project.packageJson, name)
|
|
33
31
|
if (declared === undefined) {
|
|
34
|
-
|
|
35
|
-
messages.push(`${project.name}: ${name} not declared (expected ${expected})`)
|
|
32
|
+
findings.push({ project: project.name, message: `${name} not declared (expected ${expected})` })
|
|
36
33
|
continue
|
|
37
34
|
}
|
|
38
35
|
if (!EXACT_SEMVER.test(declared)) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)
|
|
36
|
+
findings.push({
|
|
37
|
+
project: project.name,
|
|
38
|
+
message: `${name} ${JSON.stringify(declared)} is not an exact version — use "${expected}"`,
|
|
39
|
+
})
|
|
43
40
|
continue
|
|
44
41
|
}
|
|
45
42
|
if (declared !== expected) {
|
|
46
|
-
|
|
47
|
-
messages.push(`${project.name}: ${name} ${declared} does not match expected ${expected}`)
|
|
43
|
+
findings.push({ project: project.name, message: `${name} ${declared} does not match expected ${expected}` })
|
|
48
44
|
}
|
|
49
45
|
}
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
return { name: 'exactDependencyVersions',
|
|
48
|
+
return { name: 'exactDependencyVersions', findings }
|
|
53
49
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
1
|
+
import type { CheckResult, Finding, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
2
2
|
|
|
3
3
|
const findVersion = (pkg: PackageJson, name: string): string | undefined =>
|
|
4
4
|
pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? pkg.peerDependencies?.[name]
|
|
@@ -9,8 +9,7 @@ export const matchingDependencyVersions = (
|
|
|
9
9
|
projects: Project[],
|
|
10
10
|
options: NonNullable<NonNullable<WardenConfig['checks']>['matchingDependencyVersions']>,
|
|
11
11
|
): CheckResult => {
|
|
12
|
-
const
|
|
13
|
-
let passed = true
|
|
12
|
+
const findings: Finding[] = []
|
|
14
13
|
|
|
15
14
|
for (const packageName of options.packages ?? DEFAULT_PACKAGES) {
|
|
16
15
|
const versionToProjects = new Map<string, string[]>()
|
|
@@ -27,19 +26,26 @@ export const matchingDependencyVersions = (
|
|
|
27
26
|
versionToProjects.set(version, existing)
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
messages.push(`${packageName}: not declared in ${missing.join(', ')}`)
|
|
29
|
+
for (const project of missing) {
|
|
30
|
+
findings.push({ project, message: `${packageName}: not declared` })
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
if (versionToProjects.size > 1) {
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
const sorted = [...versionToProjects.entries()].toSorted((a, b) => b[1].length - a[1].length)
|
|
35
|
+
const [majority, ...rest] = sorted
|
|
36
|
+
if (majority) {
|
|
37
|
+
const [majorityVersion, majorityNames] = majority
|
|
38
|
+
for (const [version, names] of rest) {
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
findings.push({
|
|
41
|
+
project: name,
|
|
42
|
+
message: `${packageName} ${version} mismatches majority ${majorityVersion} (${majorityNames.join(', ')})`,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
return { name: 'matchingDependencyVersions',
|
|
50
|
+
return { name: 'matchingDependencyVersions', findings }
|
|
45
51
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Glob } from 'bun'
|
|
2
2
|
import { dirname, resolve } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
4
|
+
import type { CheckResult, Finding, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
5
5
|
|
|
6
6
|
const NEXT = 'next'
|
|
7
7
|
const CONFIG_VARIANTS = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs'] as const
|
|
@@ -77,8 +77,7 @@ export const nextConfig = async (
|
|
|
77
77
|
projects: Project[],
|
|
78
78
|
options: NonNullable<NonNullable<WardenConfig['checks']>['nextConfig']>,
|
|
79
79
|
): Promise<CheckResult> => {
|
|
80
|
-
const
|
|
81
|
-
let passed = true
|
|
80
|
+
const findings: Finding[] = []
|
|
82
81
|
|
|
83
82
|
for (const project of projects) {
|
|
84
83
|
const pkgs = await collectPackageJsons(project)
|
|
@@ -101,27 +100,26 @@ export const nextConfig = async (
|
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
if (configRel === null || configAbs === null) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`${
|
|
107
|
-
)
|
|
103
|
+
findings.push({
|
|
104
|
+
project: project.name,
|
|
105
|
+
message: `${relPath}: declares "next" but no config file found at ${dirLabel || './'} (expected one of ${CONFIG_VARIANTS.join(', ')})`,
|
|
106
|
+
})
|
|
108
107
|
} else {
|
|
109
108
|
let cfg: Record<string, unknown> | null = null
|
|
110
109
|
try {
|
|
111
110
|
cfg = await loadConfigObject(configAbs)
|
|
112
111
|
} catch (err) {
|
|
113
|
-
|
|
114
|
-
messages.push(`${project.name}/${configRel}: failed to load — ${(err as Error).message}`)
|
|
112
|
+
findings.push({ project: project.name, message: `${configRel}: failed to load — ${(err as Error).message}` })
|
|
115
113
|
}
|
|
116
114
|
|
|
117
115
|
if (cfg) {
|
|
118
116
|
for (const [key, expected] of Object.entries(options.options ?? DEFAULT_OPTIONS)) {
|
|
119
117
|
if (!equal(cfg[key], expected)) {
|
|
120
|
-
passed = false
|
|
121
118
|
const actual = key in cfg ? JSON.stringify(cfg[key]) : 'missing'
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
findings.push({
|
|
120
|
+
project: project.name,
|
|
121
|
+
message: `${configRel}: "${key}" is ${actual} — expected ${JSON.stringify(expected)}`,
|
|
122
|
+
})
|
|
125
123
|
}
|
|
126
124
|
}
|
|
127
125
|
}
|
|
@@ -131,14 +129,14 @@ export const nextConfig = async (
|
|
|
131
129
|
const declared = pkg.devDependencies?.[name]
|
|
132
130
|
if (declared !== expected) {
|
|
133
131
|
const actual = declared === undefined ? 'missing' : JSON.stringify(declared)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
`${
|
|
137
|
-
)
|
|
132
|
+
findings.push({
|
|
133
|
+
project: project.name,
|
|
134
|
+
message: `${relPath}: declares "next" but devDependencies["${name}"] is ${actual} — expected exact "${expected}" (must be declared in this package.json)`,
|
|
135
|
+
})
|
|
138
136
|
}
|
|
139
137
|
}
|
|
140
138
|
}
|
|
141
139
|
}
|
|
142
140
|
|
|
143
|
-
return { name: 'nextConfig',
|
|
141
|
+
return { name: 'nextConfig', findings }
|
|
144
142
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Glob } from 'bun'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { CheckResult, Finding, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
5
|
+
|
|
6
|
+
const EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/
|
|
7
|
+
|
|
8
|
+
const DEFAULT_PACKAGES: Record<string, string> = {
|
|
9
|
+
next: '16.2.6',
|
|
10
|
+
react: '19.2.6',
|
|
11
|
+
'react-dom': '19.2.6',
|
|
12
|
+
'@types/react': '19.2.14',
|
|
13
|
+
'@types/react-dom': '19.2.3',
|
|
14
|
+
tailwindcss: '4.3.0',
|
|
15
|
+
typescript: '6.0.3',
|
|
16
|
+
postcss: '8.5.14',
|
|
17
|
+
'@types/node': '25.7.0',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type WorkspaceField = string[] | { packages?: string[] } | undefined
|
|
21
|
+
|
|
22
|
+
const workspacePatterns = (pkg: PackageJson | null): string[] => {
|
|
23
|
+
const ws = (pkg as { workspaces?: WorkspaceField } | null)?.workspaces
|
|
24
|
+
if (Array.isArray(ws)) return ws
|
|
25
|
+
if (ws && Array.isArray(ws.packages)) return ws.packages
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const collectPackageJsons = async (project: Project): Promise<Array<{ relPath: string; pkg: PackageJson }>> => {
|
|
30
|
+
const results: Array<{ relPath: string; pkg: PackageJson }> = []
|
|
31
|
+
if (project.packageJson) {
|
|
32
|
+
results.push({ relPath: 'package.json', pkg: project.packageJson })
|
|
33
|
+
}
|
|
34
|
+
for (const pattern of workspacePatterns(project.packageJson)) {
|
|
35
|
+
const target = `${pattern}/package.json`
|
|
36
|
+
if (pattern.includes('*')) {
|
|
37
|
+
const glob = new Glob(target)
|
|
38
|
+
for await (const path of glob.scan({ cwd: project.path, onlyFiles: true })) {
|
|
39
|
+
if (path.startsWith('node_modules/')) continue
|
|
40
|
+
const pkgFile = Bun.file(resolve(project.path, path))
|
|
41
|
+
if (await pkgFile.exists()) {
|
|
42
|
+
results.push({ relPath: path, pkg: (await pkgFile.json()) as PackageJson })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
const pkgFile = Bun.file(resolve(project.path, target))
|
|
47
|
+
if (await pkgFile.exists()) {
|
|
48
|
+
results.push({ relPath: target, pkg: (await pkgFile.json()) as PackageJson })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return results
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DEP_GROUPS = ['dependencies', 'devDependencies', 'peerDependencies'] as const
|
|
56
|
+
|
|
57
|
+
export const pinnedDependencyVersions = async (
|
|
58
|
+
projects: Project[],
|
|
59
|
+
options: NonNullable<NonNullable<WardenConfig['checks']>['pinnedDependencyVersions']>,
|
|
60
|
+
): Promise<CheckResult> => {
|
|
61
|
+
const findings: Finding[] = []
|
|
62
|
+
const packages = Object.entries(options.packages ?? DEFAULT_PACKAGES)
|
|
63
|
+
if (packages.length === 0) {
|
|
64
|
+
return { name: 'pinnedDependencyVersions', findings }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const project of projects) {
|
|
68
|
+
const pkgs = await collectPackageJsons(project)
|
|
69
|
+
const excluded = new Set([
|
|
70
|
+
...(options.exclude?.[project.name] ?? []),
|
|
71
|
+
...(project.packageJson?.warden?.checks?.pinnedDependencyVersions?.exclude?.[project.name] ?? []),
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
for (const { relPath, pkg } of pkgs) {
|
|
75
|
+
for (const [name, expected] of packages) {
|
|
76
|
+
if (excluded.has(name)) continue
|
|
77
|
+
for (const group of DEP_GROUPS) {
|
|
78
|
+
const declared = pkg[group]?.[name]
|
|
79
|
+
if (declared === undefined) continue
|
|
80
|
+
if (!EXACT_SEMVER.test(declared)) {
|
|
81
|
+
findings.push({
|
|
82
|
+
project: project.name,
|
|
83
|
+
message: `${relPath}: ${group}["${name}"] is ${JSON.stringify(declared)} — must be exact "${expected}"`,
|
|
84
|
+
})
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
if (declared !== expected) {
|
|
88
|
+
findings.push({
|
|
89
|
+
project: project.name,
|
|
90
|
+
message: `${relPath}: ${group}["${name}"] is ${declared} — expected ${expected}`,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { name: 'pinnedDependencyVersions', findings }
|
|
99
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Glob } from 'bun'
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
4
|
+
import type { CheckResult, Finding, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
5
5
|
|
|
6
6
|
const NEXT_DEV_RE = /\bnext dev\b/
|
|
7
7
|
const PORTLESS_WRAP_RE = /\bportless\s+\S+\s+next dev\b/
|
|
@@ -46,8 +46,7 @@ export const portlessNextDev = async (
|
|
|
46
46
|
projects: Project[],
|
|
47
47
|
options: NonNullable<NonNullable<WardenConfig['checks']>['portlessNextDev']>,
|
|
48
48
|
): Promise<CheckResult> => {
|
|
49
|
-
const
|
|
50
|
-
let passed = true
|
|
49
|
+
const findings: Finding[] = []
|
|
51
50
|
|
|
52
51
|
for (const project of projects) {
|
|
53
52
|
const pkgs = await collectPackageJsons(project)
|
|
@@ -60,10 +59,10 @@ export const portlessNextDev = async (
|
|
|
60
59
|
if (!NEXT_DEV_RE.test(value)) continue
|
|
61
60
|
usesNextDev = true
|
|
62
61
|
if (!PORTLESS_WRAP_RE.test(value)) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`${
|
|
66
|
-
)
|
|
62
|
+
findings.push({
|
|
63
|
+
project: project.name,
|
|
64
|
+
message: `${relPath}: script "${name}" is ${JSON.stringify(value)} — must be wrapped as "portless <app-name> next dev"`,
|
|
65
|
+
})
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
68
|
|
|
@@ -72,14 +71,14 @@ export const portlessNextDev = async (
|
|
|
72
71
|
const declared = pkg.devDependencies?.portless
|
|
73
72
|
if (declared !== expectedVersion) {
|
|
74
73
|
const actual = declared === undefined ? 'missing' : JSON.stringify(declared)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`${
|
|
78
|
-
)
|
|
74
|
+
findings.push({
|
|
75
|
+
project: project.name,
|
|
76
|
+
message: `${relPath}: uses "next dev" but devDependencies.portless is ${actual} — expected exact "${expectedVersion}" (must be declared in this package.json)`,
|
|
77
|
+
})
|
|
79
78
|
}
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
return { name: 'portlessNextDev',
|
|
83
|
+
return { name: 'portlessNextDev', findings }
|
|
85
84
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckResult, Project, WardenConfig } from '../types.ts'
|
|
1
|
+
import type { CheckResult, Finding, Project, WardenConfig } from '../types.ts'
|
|
2
2
|
|
|
3
3
|
const DEFAULT_SCRIPTS: Record<string, string> = {
|
|
4
4
|
format: 'oxfmt .',
|
|
@@ -11,34 +11,34 @@ export const requiredScripts = (
|
|
|
11
11
|
projects: Project[],
|
|
12
12
|
options: NonNullable<NonNullable<WardenConfig['checks']>['requiredScripts']>,
|
|
13
13
|
): CheckResult => {
|
|
14
|
-
const
|
|
15
|
-
let passed = true
|
|
14
|
+
const findings: Finding[] = []
|
|
16
15
|
|
|
17
16
|
const required = Object.entries(options.scripts ?? DEFAULT_SCRIPTS)
|
|
18
17
|
if (required.length === 0) {
|
|
19
|
-
return { name: 'requiredScripts',
|
|
18
|
+
return { name: 'requiredScripts', findings }
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
for (const project of projects) {
|
|
23
22
|
const scripts = project.packageJson?.scripts
|
|
24
23
|
if (!scripts) {
|
|
25
|
-
|
|
26
|
-
messages.push(`${project.name}: no "scripts" in package.json`)
|
|
24
|
+
findings.push({ project: project.name, message: 'no "scripts" in package.json' })
|
|
27
25
|
continue
|
|
28
26
|
}
|
|
29
27
|
for (const [name, expected] of required) {
|
|
30
28
|
const actual = scripts[name]
|
|
31
29
|
if (actual === undefined) {
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
findings.push({
|
|
31
|
+
project: project.name,
|
|
32
|
+
message: `missing script "${name}" (expected ${JSON.stringify(expected)})`,
|
|
33
|
+
})
|
|
34
34
|
} else if (actual !== expected) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
35
|
+
findings.push({
|
|
36
|
+
project: project.name,
|
|
37
|
+
message: `script "${name}" is ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`,
|
|
38
|
+
})
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
return { name: 'requiredScripts',
|
|
43
|
+
return { name: 'requiredScripts', findings }
|
|
44
44
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
1
|
+
import type { CheckResult, Finding, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
2
2
|
|
|
3
3
|
const TAILWIND = 'tailwindcss'
|
|
4
4
|
const OXFMT_CONFIG = '.oxfmtrc.json'
|
|
@@ -13,8 +13,7 @@ export const tailwindOxfmtConfig = async (
|
|
|
13
13
|
projects: Project[],
|
|
14
14
|
_options: NonNullable<NonNullable<WardenConfig['checks']>['tailwindOxfmtConfig']>,
|
|
15
15
|
): Promise<CheckResult> => {
|
|
16
|
-
const
|
|
17
|
-
let passed = true
|
|
16
|
+
const findings: Finding[] = []
|
|
18
17
|
|
|
19
18
|
for (const project of projects) {
|
|
20
19
|
const oxfmtFile = Bun.file(`${project.path}/${OXFMT_CONFIG}`)
|
|
@@ -24,18 +23,20 @@ export const tailwindOxfmtConfig = async (
|
|
|
24
23
|
const present = REQUIRED_KEY in oxfmt
|
|
25
24
|
|
|
26
25
|
if (present && oxfmt[REQUIRED_KEY] !== true) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
)
|
|
26
|
+
findings.push({
|
|
27
|
+
project: project.name,
|
|
28
|
+
message: `${OXFMT_CONFIG} "${REQUIRED_KEY}" must be \`true\` (got ${JSON.stringify(oxfmt[REQUIRED_KEY])})`,
|
|
29
|
+
})
|
|
31
30
|
continue
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
if (!present && project.packageJson && dependsOnTailwind(project.packageJson)) {
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
findings.push({
|
|
35
|
+
project: project.name,
|
|
36
|
+
message: `depends on ${TAILWIND} but ${OXFMT_CONFIG} is missing "${REQUIRED_KEY}": true`,
|
|
37
|
+
})
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
return { name: 'tailwindOxfmtConfig',
|
|
41
|
+
return { name: 'tailwindOxfmtConfig', findings }
|
|
41
42
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Glob } from 'bun'
|
|
2
2
|
|
|
3
|
-
import type { CheckResult, Project, WardenConfig } from '../types.ts'
|
|
3
|
+
import type { CheckResult, Finding, Project, WardenConfig } from '../types.ts'
|
|
4
4
|
|
|
5
5
|
const projectHasTests = async (projectPath: string, pattern: string): Promise<boolean> => {
|
|
6
6
|
const glob = new Glob(pattern)
|
|
@@ -21,8 +21,7 @@ export const testScriptConsistency = async (
|
|
|
21
21
|
projects: Project[],
|
|
22
22
|
options: NonNullable<NonNullable<WardenConfig['checks']>['testScriptConsistency']>,
|
|
23
23
|
): Promise<CheckResult> => {
|
|
24
|
-
const
|
|
25
|
-
let passed = true
|
|
24
|
+
const findings: Finding[] = []
|
|
26
25
|
|
|
27
26
|
const testFilePattern = options.testFilePattern ?? DEFAULT_TEST_FILE_PATTERN
|
|
28
27
|
const testScript = options.testScript ?? DEFAULT_TEST_SCRIPT
|
|
@@ -35,26 +34,26 @@ export const testScriptConsistency = async (
|
|
|
35
34
|
|
|
36
35
|
if (hasTests) {
|
|
37
36
|
if (scripts.test !== testScript) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
37
|
+
findings.push({
|
|
38
|
+
project: project.name,
|
|
39
|
+
message: `has test files but "test" script is ${JSON.stringify(scripts.test)}, expected ${JSON.stringify(testScript)}`,
|
|
40
|
+
})
|
|
42
41
|
}
|
|
43
42
|
if (scripts.all !== allWithTests) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
)
|
|
43
|
+
findings.push({
|
|
44
|
+
project: project.name,
|
|
45
|
+
message: `"all" is ${JSON.stringify(scripts.all)}, expected ${JSON.stringify(allWithTests)} (project has tests)`,
|
|
46
|
+
})
|
|
48
47
|
}
|
|
49
48
|
} else {
|
|
50
49
|
if (scripts.all !== allWithoutTests) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
50
|
+
findings.push({
|
|
51
|
+
project: project.name,
|
|
52
|
+
message: `"all" is ${JSON.stringify(scripts.all)}, expected ${JSON.stringify(allWithoutTests)} (no tests detected)`,
|
|
53
|
+
})
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
return { name: 'testScriptConsistency',
|
|
58
|
+
return { name: 'testScriptConsistency', findings }
|
|
60
59
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckResult, Project, WardenConfig } from '../types.ts'
|
|
1
|
+
import type { CheckResult, Finding, Project, WardenConfig } from '../types.ts'
|
|
2
2
|
|
|
3
3
|
const SINGLE = 'tsc --noEmit'
|
|
4
4
|
const SOLUTION = 'tsc -b --noEmit'
|
|
@@ -19,14 +19,12 @@ export const typecheckScript = async (
|
|
|
19
19
|
projects: Project[],
|
|
20
20
|
options: NonNullable<NonNullable<WardenConfig['checks']>['typecheckScript']>,
|
|
21
21
|
): Promise<CheckResult> => {
|
|
22
|
-
const
|
|
23
|
-
let passed = true
|
|
22
|
+
const findings: Finding[] = []
|
|
24
23
|
|
|
25
24
|
for (const project of projects) {
|
|
26
25
|
const scripts = project.packageJson?.scripts
|
|
27
26
|
if (!scripts) {
|
|
28
|
-
|
|
29
|
-
messages.push(`${project.name}: no "scripts" in package.json`)
|
|
27
|
+
findings.push({ project: project.name, message: 'no "scripts" in package.json' })
|
|
30
28
|
continue
|
|
31
29
|
}
|
|
32
30
|
const mode = options.modes?.[project.name] ?? 'auto'
|
|
@@ -38,15 +36,17 @@ export const typecheckScript = async (
|
|
|
38
36
|
: `root tsconfig ${expected === SOLUTION ? 'has' : 'has no'} project references`
|
|
39
37
|
const actual = scripts.typecheck
|
|
40
38
|
if (actual === undefined) {
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
findings.push({
|
|
40
|
+
project: project.name,
|
|
41
|
+
message: `missing script "typecheck" (expected ${JSON.stringify(expected)})`,
|
|
42
|
+
})
|
|
43
43
|
} else if (actual !== expected) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
)
|
|
44
|
+
findings.push({
|
|
45
|
+
project: project.name,
|
|
46
|
+
message: `script "typecheck" is ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)} (${reason})`,
|
|
47
|
+
})
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
return { name: 'typecheckScript',
|
|
51
|
+
return { name: 'typecheckScript', findings }
|
|
52
52
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
1
|
+
import type { CheckResult, Finding, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
2
2
|
|
|
3
3
|
const dependsOn = (pkg: PackageJson, name: string): boolean =>
|
|
4
4
|
name in (pkg.dependencies ?? {}) || name in (pkg.devDependencies ?? {}) || name in (pkg.peerDependencies ?? {})
|
|
@@ -9,8 +9,7 @@ export const wardenScript = (
|
|
|
9
9
|
projects: Project[],
|
|
10
10
|
options: NonNullable<NonNullable<WardenConfig['checks']>['wardenScript']>,
|
|
11
11
|
): CheckResult => {
|
|
12
|
-
const
|
|
13
|
-
let passed = true
|
|
12
|
+
const findings: Finding[] = []
|
|
14
13
|
const packageName = options.package ?? DEFAULT_PACKAGE
|
|
15
14
|
|
|
16
15
|
for (const project of projects) {
|
|
@@ -19,18 +18,18 @@ export const wardenScript = (
|
|
|
19
18
|
|
|
20
19
|
const scripts = pkg.scripts ?? {}
|
|
21
20
|
if (scripts.warden !== 'warden') {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
21
|
+
findings.push({
|
|
22
|
+
project: project.name,
|
|
23
|
+
message: `depends on ${packageName} but "warden" script is ${JSON.stringify(scripts.warden)}, expected "warden"`,
|
|
24
|
+
})
|
|
26
25
|
}
|
|
27
26
|
if (!scripts.all || !scripts.all.includes('bun run warden')) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
)
|
|
27
|
+
findings.push({
|
|
28
|
+
project: project.name,
|
|
29
|
+
message: `depends on ${packageName} but "all" script ${JSON.stringify(scripts.all)} is missing "bun run warden"`,
|
|
30
|
+
})
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
return { name: 'wardenScript',
|
|
34
|
+
return { name: 'wardenScript', findings }
|
|
36
35
|
}
|
package/src/ci-status.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { loadConfig, resolveProjects } from './config.ts'
|
|
3
|
+
|
|
4
|
+
type Run = {
|
|
5
|
+
conclusion: string | null
|
|
6
|
+
status: string
|
|
7
|
+
workflowName: string
|
|
8
|
+
url: string
|
|
9
|
+
createdAt: string
|
|
10
|
+
headSha: string
|
|
11
|
+
event: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const baseDir = process.cwd()
|
|
15
|
+
|
|
16
|
+
const config = await loadConfig(baseDir)
|
|
17
|
+
const allProjects = await resolveProjects(config, baseDir)
|
|
18
|
+
|
|
19
|
+
const filterNames = process.argv.slice(2)
|
|
20
|
+
const knownNames = new Set(allProjects.map(p => p.name))
|
|
21
|
+
const unknown = filterNames.filter(n => !knownNames.has(n))
|
|
22
|
+
if (unknown.length > 0) {
|
|
23
|
+
console.error(
|
|
24
|
+
`Unknown project(s): ${unknown.join(', ')}\nConfigured projects: ${allProjects.map(p => p.name).join(', ')}`,
|
|
25
|
+
)
|
|
26
|
+
process.exit(2)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const projects = filterNames.length > 0 ? allProjects.filter(p => filterNames.includes(p.name)) : allProjects
|
|
30
|
+
|
|
31
|
+
console.log(
|
|
32
|
+
`ci-status — checking last main-branch run for ${projects.length} project(s): ${projects.map(p => p.name).join(', ')}`,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const failed: string[] = []
|
|
36
|
+
const missing: string[] = []
|
|
37
|
+
|
|
38
|
+
for (const project of projects) {
|
|
39
|
+
const repo = `verekia/${project.name}`
|
|
40
|
+
const proc = Bun.spawn(
|
|
41
|
+
[
|
|
42
|
+
'gh',
|
|
43
|
+
'run',
|
|
44
|
+
'list',
|
|
45
|
+
'--repo',
|
|
46
|
+
repo,
|
|
47
|
+
'--branch',
|
|
48
|
+
'main',
|
|
49
|
+
'--limit',
|
|
50
|
+
'1',
|
|
51
|
+
'--json',
|
|
52
|
+
'conclusion,status,workflowName,url,createdAt,headSha,event',
|
|
53
|
+
],
|
|
54
|
+
{ stdout: 'pipe', stderr: 'pipe' },
|
|
55
|
+
)
|
|
56
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
57
|
+
new Response(proc.stdout).text(),
|
|
58
|
+
new Response(proc.stderr).text(),
|
|
59
|
+
proc.exited,
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
if (exitCode !== 0) {
|
|
63
|
+
console.log(`\n[ERROR] ${project.name} (${repo})`)
|
|
64
|
+
console.log(stderr.trim() || `gh exited with code ${exitCode}`)
|
|
65
|
+
failed.push(project.name)
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const runs = JSON.parse(stdout) as Run[]
|
|
70
|
+
const run = runs[0]
|
|
71
|
+
if (!run) {
|
|
72
|
+
console.log(`\n[NONE] ${project.name} — no workflow runs on main`)
|
|
73
|
+
missing.push(project.name)
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const verdict =
|
|
78
|
+
run.status !== 'completed'
|
|
79
|
+
? `IN PROGRESS (${run.status})`
|
|
80
|
+
: run.conclusion === 'success'
|
|
81
|
+
? 'PASS'
|
|
82
|
+
: `FAIL (${run.conclusion ?? 'unknown'})`
|
|
83
|
+
const tag = verdict === 'PASS' ? '[PASS]' : verdict.startsWith('IN PROGRESS') ? '[WAIT]' : '[FAIL]'
|
|
84
|
+
|
|
85
|
+
console.log(`\n${tag} ${project.name} — ${run.workflowName}: ${verdict}`)
|
|
86
|
+
console.log(` ${run.createdAt} ${run.headSha.slice(0, 7)} ${run.event}`)
|
|
87
|
+
console.log(` ${run.url}`)
|
|
88
|
+
|
|
89
|
+
if (tag === '[FAIL]') failed.push(project.name)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const passed = projects.length - failed.length - missing.length
|
|
93
|
+
console.log(`\n${passed}/${projects.length} projects passed`)
|
|
94
|
+
if (missing.length > 0) console.log(`No runs on main: ${missing.join(', ')}`)
|
|
95
|
+
if (failed.length > 0) {
|
|
96
|
+
console.log(`Failed: ${failed.join(', ')}`)
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { configFilesPresent } from './checks/config-files-present.ts'
|
|
|
6
6
|
import { exactDependencyVersions } from './checks/exact-dependency-versions.ts'
|
|
7
7
|
import { matchingDependencyVersions } from './checks/matching-dependency-versions.ts'
|
|
8
8
|
import { nextConfig } from './checks/next-config.ts'
|
|
9
|
+
import { pinnedDependencyVersions } from './checks/pinned-dependency-versions.ts'
|
|
9
10
|
import { portlessNextDev } from './checks/portless-next-dev.ts'
|
|
10
11
|
import { requiredScripts } from './checks/required-scripts.ts'
|
|
11
12
|
import { tailwindOxfmtConfig } from './checks/tailwind-oxfmt-config.ts'
|
|
@@ -59,6 +60,9 @@ if (enabled(checks.matchingDependencyVersions)) {
|
|
|
59
60
|
if (enabled(checks.exactDependencyVersions)) {
|
|
60
61
|
results.push(exactDependencyVersions(projects, checks.exactDependencyVersions ?? {}))
|
|
61
62
|
}
|
|
63
|
+
if (enabled(checks.pinnedDependencyVersions)) {
|
|
64
|
+
results.push(await pinnedDependencyVersions(projects, checks.pinnedDependencyVersions ?? {}))
|
|
65
|
+
}
|
|
62
66
|
if (enabled(checks.requiredScripts)) {
|
|
63
67
|
results.push(requiredScripts(projects, checks.requiredScripts ?? {}))
|
|
64
68
|
}
|
|
@@ -84,14 +88,35 @@ if (enabled(checks.ciWorkflow)) {
|
|
|
84
88
|
results.push(await ciWorkflow(projects, checks.ciWorkflow ?? {}))
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
const findingsByProjectByCheck = new Map<string, Map<string, string[]>>()
|
|
87
92
|
for (const result of results) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
for (const finding of result.findings) {
|
|
94
|
+
const byCheck = findingsByProjectByCheck.get(finding.project) ?? new Map<string, string[]>()
|
|
95
|
+
const list = byCheck.get(result.name) ?? []
|
|
96
|
+
list.push(finding.message)
|
|
97
|
+
byCheck.set(result.name, list)
|
|
98
|
+
findingsByProjectByCheck.set(finding.project, byCheck)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let failedProjects = 0
|
|
103
|
+
for (const project of projects) {
|
|
104
|
+
const byCheck = findingsByProjectByCheck.get(project.name) ?? new Map<string, string[]>()
|
|
105
|
+
const projectFailed = byCheck.size > 0
|
|
106
|
+
if (projectFailed) failedProjects += 1
|
|
107
|
+
console.log(`${projectFailed ? '[FAIL]' : '[PASS]'} ${project.name}`)
|
|
108
|
+
for (const result of results) {
|
|
109
|
+
const messages = byCheck.get(result.name) ?? []
|
|
110
|
+
if (messages.length === 0) {
|
|
111
|
+
console.log(` [PASS] ${result.name}`)
|
|
112
|
+
} else {
|
|
113
|
+
console.log(` [FAIL] ${result.name}`)
|
|
114
|
+
for (const message of messages) {
|
|
115
|
+
console.log(` ${message}`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
92
118
|
}
|
|
93
119
|
}
|
|
94
120
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
process.exit(failed === 0 ? 0 : 1)
|
|
121
|
+
console.log(`\n${projects.length - failedProjects}/${projects.length} projects passed`)
|
|
122
|
+
process.exit(failedProjects === 0 ? 0 : 1)
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,11 @@ export type WardenConfig = {
|
|
|
4
4
|
configFilesPresent?: { enabled?: boolean; files?: string[] }
|
|
5
5
|
matchingDependencyVersions?: { enabled?: boolean; packages?: string[] }
|
|
6
6
|
exactDependencyVersions?: { enabled?: boolean; packages?: Record<string, string> }
|
|
7
|
+
pinnedDependencyVersions?: {
|
|
8
|
+
enabled?: boolean
|
|
9
|
+
packages?: Record<string, string>
|
|
10
|
+
exclude?: Record<string, string[]>
|
|
11
|
+
}
|
|
7
12
|
requiredScripts?: { enabled?: boolean; scripts?: Record<string, string> }
|
|
8
13
|
typecheckScript?: { enabled?: boolean; modes?: Record<string, 'auto' | 'bun-filter-all'> }
|
|
9
14
|
testScriptConsistency?: {
|
|
@@ -32,6 +37,7 @@ export type PackageJson = {
|
|
|
32
37
|
dependencies?: Record<string, string>
|
|
33
38
|
devDependencies?: Record<string, string>
|
|
34
39
|
peerDependencies?: Record<string, string>
|
|
40
|
+
warden?: WardenConfig
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export type Project = {
|
|
@@ -40,8 +46,12 @@ export type Project = {
|
|
|
40
46
|
packageJson: PackageJson | null
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
export type Finding = {
|
|
50
|
+
project: string
|
|
51
|
+
message: string
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
export type CheckResult = {
|
|
44
55
|
name: string
|
|
45
|
-
|
|
46
|
-
messages: string[]
|
|
56
|
+
findings: Finding[]
|
|
47
57
|
}
|