@verekia/warden 0.0.0
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/README.md +89 -0
- package/package.json +108 -0
- package/src/checks/config-files-present.ts +21 -0
- package/src/checks/exact-dependency-versions.ts +43 -0
- package/src/checks/matching-dependency-versions.ts +43 -0
- package/src/checks/next-config.ts +135 -0
- package/src/checks/portless-next-dev.ts +83 -0
- package/src/checks/required-scripts.ts +32 -0
- package/src/checks/tailwind-oxfmt-config.ts +41 -0
- package/src/checks/test-script-consistency.ts +49 -0
- package/src/checks/typecheck-script.ts +52 -0
- package/src/checks/warden-script.ts +33 -0
- package/src/config.ts +28 -0
- package/src/index.ts +91 -0
- package/src/types.ts +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @verekia/warden
|
|
2
|
+
|
|
3
|
+
Linter that checks repositories share consistent configs and tool versions.
|
|
4
|
+
|
|
5
|
+
Two modes:
|
|
6
|
+
|
|
7
|
+
- **Cross-project** — one warden checkout sits next to several sibling
|
|
8
|
+
repos and lints them all in one pass.
|
|
9
|
+
- **Library** — a repo installs `@verekia/warden` as a devDep and lints
|
|
10
|
+
itself.
|
|
11
|
+
|
|
12
|
+
## Library mode
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
bun add -D @verekia/warden
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Add a `warden` script and wire it into your `all` script:
|
|
19
|
+
|
|
20
|
+
```jsonc
|
|
21
|
+
{
|
|
22
|
+
"scripts": {
|
|
23
|
+
"warden": "warden",
|
|
24
|
+
"all": "bun run format:check && bun run lint && bun run typecheck && bun run warden",
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@verekia/warden": "0.0.0",
|
|
28
|
+
},
|
|
29
|
+
"warden": {
|
|
30
|
+
"checks": {
|
|
31
|
+
"configFilesPresent": {
|
|
32
|
+
"enabled": true,
|
|
33
|
+
"files": [".oxfmtrc.json", ".oxlintrc.json"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Omit `projects` — warden defaults to checking the cwd.
|
|
41
|
+
|
|
42
|
+
## Cross-project mode
|
|
43
|
+
|
|
44
|
+
Layout: warden lives next to the repos it checks.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
<parent>/
|
|
48
|
+
warden/
|
|
49
|
+
vct7/
|
|
50
|
+
verekia.com/
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`package.json` (in the warden checkout) declares projects and check
|
|
54
|
+
options under the `"warden"` key:
|
|
55
|
+
|
|
56
|
+
```jsonc
|
|
57
|
+
{
|
|
58
|
+
"warden": {
|
|
59
|
+
"projects": ["../vct7", "../verekia.com"],
|
|
60
|
+
"checks": {
|
|
61
|
+
/* … */
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Run:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
bun install
|
|
71
|
+
bun run warden # check every project in the list
|
|
72
|
+
bun run warden vct7 verekia.com # check only the named subset
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Positional arguments scope the run — useful in cloud environments where
|
|
76
|
+
only some sibling repos are attached.
|
|
77
|
+
|
|
78
|
+
## Available checks
|
|
79
|
+
|
|
80
|
+
See [`CLAUDE.md`](./CLAUDE.md) for the full list and the failure → fix
|
|
81
|
+
mapping. Each check has its own block under `"warden".checks` in
|
|
82
|
+
`package.json` and toggles independently.
|
|
83
|
+
|
|
84
|
+
## Adding a check
|
|
85
|
+
|
|
86
|
+
1. Create `src/checks/<name>.ts` exporting a function that takes
|
|
87
|
+
`(projects, options)` and returns a `CheckResult`.
|
|
88
|
+
2. Add its options shape to `WardenConfig['checks']` in `src/types.ts`.
|
|
89
|
+
3. Wire it into `src/index.ts`.
|
package/package.json
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@verekia/warden",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Linter that checks repositories share consistent configs and tool versions.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"warden": "./src/index.ts"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"warden": "bun run src/index.ts",
|
|
14
|
+
"format": "oxfmt .",
|
|
15
|
+
"format:check": "oxfmt --check .",
|
|
16
|
+
"lint": "oxlint .",
|
|
17
|
+
"lint:fix": "oxlint --fix .",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"all": "bun run format:check && bun run lint && bun run typecheck"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/bun": "^1.3.0",
|
|
23
|
+
"oxfmt": "0.48.0",
|
|
24
|
+
"oxlint": "1.63.0",
|
|
25
|
+
"typescript": "^5.6.0"
|
|
26
|
+
},
|
|
27
|
+
"warden": {
|
|
28
|
+
"projects": [
|
|
29
|
+
"../vct7",
|
|
30
|
+
"../verekia.com",
|
|
31
|
+
"../bunext",
|
|
32
|
+
"../pixelize",
|
|
33
|
+
"../gizmo",
|
|
34
|
+
"../gputex",
|
|
35
|
+
"../gradient-normal-textures",
|
|
36
|
+
"../nanothree",
|
|
37
|
+
"../svg-to-tsl"
|
|
38
|
+
],
|
|
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
|
+
"typecheckScript": {
|
|
72
|
+
"enabled": true,
|
|
73
|
+
"modes": {
|
|
74
|
+
"nanothree": "bun-filter-all"
|
|
75
|
+
}
|
|
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
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CheckResult, Project, WardenConfig } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
export const configFilesPresent = async (
|
|
4
|
+
projects: Project[],
|
|
5
|
+
options: NonNullable<WardenConfig['checks']['configFilesPresent']>,
|
|
6
|
+
): Promise<CheckResult> => {
|
|
7
|
+
const messages: string[] = []
|
|
8
|
+
let passed = true
|
|
9
|
+
|
|
10
|
+
for (const project of projects) {
|
|
11
|
+
for (const file of options.files) {
|
|
12
|
+
const exists = await Bun.file(`${project.path}/${file}`).exists()
|
|
13
|
+
if (!exists) {
|
|
14
|
+
passed = false
|
|
15
|
+
messages.push(`${project.name}: missing ${file}`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { name: 'configFilesPresent', passed, messages }
|
|
21
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
const findVersion = (pkg: PackageJson, name: string): string | undefined =>
|
|
4
|
+
pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? pkg.peerDependencies?.[name]
|
|
5
|
+
|
|
6
|
+
const EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/
|
|
7
|
+
|
|
8
|
+
export const exactDependencyVersions = (
|
|
9
|
+
projects: Project[],
|
|
10
|
+
options: NonNullable<WardenConfig['checks']['exactDependencyVersions']>,
|
|
11
|
+
): CheckResult => {
|
|
12
|
+
const messages: string[] = []
|
|
13
|
+
let passed = true
|
|
14
|
+
|
|
15
|
+
for (const project of projects) {
|
|
16
|
+
if (!project.packageJson) {
|
|
17
|
+
passed = false
|
|
18
|
+
messages.push(`${project.name}: no package.json`)
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
for (const [name, expected] of Object.entries(options.packages)) {
|
|
22
|
+
const declared = findVersion(project.packageJson, name)
|
|
23
|
+
if (declared === undefined) {
|
|
24
|
+
passed = false
|
|
25
|
+
messages.push(`${project.name}: ${name} not declared (expected ${expected})`)
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
if (!EXACT_SEMVER.test(declared)) {
|
|
29
|
+
passed = false
|
|
30
|
+
messages.push(
|
|
31
|
+
`${project.name}: ${name} ${JSON.stringify(declared)} is not an exact version — use "${expected}"`,
|
|
32
|
+
)
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
if (declared !== expected) {
|
|
36
|
+
passed = false
|
|
37
|
+
messages.push(`${project.name}: ${name} ${declared} does not match expected ${expected}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { name: 'exactDependencyVersions', passed, messages }
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
const findVersion = (pkg: PackageJson, name: string): string | undefined =>
|
|
4
|
+
pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? pkg.peerDependencies?.[name]
|
|
5
|
+
|
|
6
|
+
export const matchingDependencyVersions = (
|
|
7
|
+
projects: Project[],
|
|
8
|
+
options: NonNullable<WardenConfig['checks']['matchingDependencyVersions']>,
|
|
9
|
+
): CheckResult => {
|
|
10
|
+
const messages: string[] = []
|
|
11
|
+
let passed = true
|
|
12
|
+
|
|
13
|
+
for (const packageName of options.packages) {
|
|
14
|
+
const versionToProjects = new Map<string, string[]>()
|
|
15
|
+
const missing: string[] = []
|
|
16
|
+
|
|
17
|
+
for (const project of projects) {
|
|
18
|
+
const version = project.packageJson ? findVersion(project.packageJson, packageName) : undefined
|
|
19
|
+
if (version === undefined) {
|
|
20
|
+
missing.push(project.name)
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
const existing = versionToProjects.get(version) ?? []
|
|
24
|
+
existing.push(project.name)
|
|
25
|
+
versionToProjects.set(version, existing)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (missing.length > 0) {
|
|
29
|
+
passed = false
|
|
30
|
+
messages.push(`${packageName}: not declared in ${missing.join(', ')}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (versionToProjects.size > 1) {
|
|
34
|
+
passed = false
|
|
35
|
+
const detail = [...versionToProjects.entries()]
|
|
36
|
+
.map(([version, names]) => `${version} (${names.join(', ')})`)
|
|
37
|
+
.join(' vs ')
|
|
38
|
+
messages.push(`${packageName}: version mismatch — ${detail}`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { name: 'matchingDependencyVersions', passed, messages }
|
|
43
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Glob } from 'bun'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
5
|
+
|
|
6
|
+
const NEXT = 'next'
|
|
7
|
+
const CONFIG_VARIANTS = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs'] as const
|
|
8
|
+
|
|
9
|
+
type WorkspaceField = string[] | { packages?: string[] } | undefined
|
|
10
|
+
|
|
11
|
+
const dependsOnNext = (pkg: PackageJson): boolean =>
|
|
12
|
+
NEXT in (pkg.dependencies ?? {}) || NEXT in (pkg.devDependencies ?? {}) || NEXT in (pkg.peerDependencies ?? {})
|
|
13
|
+
|
|
14
|
+
const workspacePatterns = (pkg: PackageJson | null): string[] => {
|
|
15
|
+
const ws = (pkg as { workspaces?: WorkspaceField } | null)?.workspaces
|
|
16
|
+
if (Array.isArray(ws)) return ws
|
|
17
|
+
if (ws && Array.isArray(ws.packages)) return ws.packages
|
|
18
|
+
return []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const collectPackageJsons = async (project: Project): Promise<Array<{ relPath: string; pkg: PackageJson }>> => {
|
|
22
|
+
const results: Array<{ relPath: string; pkg: PackageJson }> = []
|
|
23
|
+
if (project.packageJson) {
|
|
24
|
+
results.push({ relPath: 'package.json', pkg: project.packageJson })
|
|
25
|
+
}
|
|
26
|
+
for (const pattern of workspacePatterns(project.packageJson)) {
|
|
27
|
+
const target = `${pattern}/package.json`
|
|
28
|
+
if (pattern.includes('*')) {
|
|
29
|
+
const glob = new Glob(target)
|
|
30
|
+
for await (const path of glob.scan({ cwd: project.path, onlyFiles: true })) {
|
|
31
|
+
if (path.startsWith('node_modules/')) continue
|
|
32
|
+
const pkgFile = Bun.file(resolve(project.path, path))
|
|
33
|
+
if (await pkgFile.exists()) {
|
|
34
|
+
results.push({ relPath: path, pkg: (await pkgFile.json()) as PackageJson })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
const pkgFile = Bun.file(resolve(project.path, target))
|
|
39
|
+
if (await pkgFile.exists()) {
|
|
40
|
+
results.push({ relPath: target, pkg: (await pkgFile.json()) as PackageJson })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return results
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const equal = (a: unknown, b: unknown): boolean => {
|
|
48
|
+
if (a === b) return true
|
|
49
|
+
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
|
50
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const loadConfigObject = async (absPath: string): Promise<Record<string, unknown>> => {
|
|
56
|
+
const mod = (await import(absPath)) as { default?: unknown }
|
|
57
|
+
let value: unknown = mod.default ?? mod
|
|
58
|
+
if (typeof value === 'function') {
|
|
59
|
+
value = await (value as (...args: unknown[]) => unknown)('phase-development-server', { defaultConfig: {} })
|
|
60
|
+
}
|
|
61
|
+
if (!value || typeof value !== 'object') {
|
|
62
|
+
throw new Error(`default export is not an object (got ${typeof value})`)
|
|
63
|
+
}
|
|
64
|
+
return value as Record<string, unknown>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const nextConfig = async (
|
|
68
|
+
projects: Project[],
|
|
69
|
+
options: NonNullable<WardenConfig['checks']['nextConfig']>,
|
|
70
|
+
): Promise<CheckResult> => {
|
|
71
|
+
const messages: string[] = []
|
|
72
|
+
let passed = true
|
|
73
|
+
|
|
74
|
+
for (const project of projects) {
|
|
75
|
+
const pkgs = await collectPackageJsons(project)
|
|
76
|
+
|
|
77
|
+
for (const { relPath, pkg } of pkgs) {
|
|
78
|
+
if (!dependsOnNext(pkg)) continue
|
|
79
|
+
|
|
80
|
+
const pkgDir = dirname(relPath)
|
|
81
|
+
const dirLabel = pkgDir === '.' ? '' : `${pkgDir}/`
|
|
82
|
+
|
|
83
|
+
let configRel: string | null = null
|
|
84
|
+
let configAbs: string | null = null
|
|
85
|
+
for (const variant of CONFIG_VARIANTS) {
|
|
86
|
+
const abs = resolve(project.path, pkgDir, variant)
|
|
87
|
+
if (await Bun.file(abs).exists()) {
|
|
88
|
+
configRel = `${dirLabel}${variant}`
|
|
89
|
+
configAbs = abs
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (configRel === null || configAbs === null) {
|
|
95
|
+
passed = false
|
|
96
|
+
messages.push(
|
|
97
|
+
`${project.name}/${relPath}: declares "next" but no config file found at ${dirLabel || './'} (expected one of ${CONFIG_VARIANTS.join(', ')})`,
|
|
98
|
+
)
|
|
99
|
+
} else {
|
|
100
|
+
let cfg: Record<string, unknown> | null = null
|
|
101
|
+
try {
|
|
102
|
+
cfg = await loadConfigObject(configAbs)
|
|
103
|
+
} catch (err) {
|
|
104
|
+
passed = false
|
|
105
|
+
messages.push(`${project.name}/${configRel}: failed to load — ${(err as Error).message}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (cfg) {
|
|
109
|
+
for (const [key, expected] of Object.entries(options.options)) {
|
|
110
|
+
if (!equal(cfg[key], expected)) {
|
|
111
|
+
passed = false
|
|
112
|
+
const actual = key in cfg ? JSON.stringify(cfg[key]) : 'missing'
|
|
113
|
+
messages.push(
|
|
114
|
+
`${project.name}/${configRel}: "${key}" is ${actual} — expected ${JSON.stringify(expected)}`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const [name, expected] of Object.entries(options.devDependencies)) {
|
|
122
|
+
const declared = pkg.devDependencies?.[name]
|
|
123
|
+
if (declared !== expected) {
|
|
124
|
+
const actual = declared === undefined ? 'missing' : JSON.stringify(declared)
|
|
125
|
+
passed = false
|
|
126
|
+
messages.push(
|
|
127
|
+
`${project.name}/${relPath}: declares "next" but devDependencies["${name}"] is ${actual} — expected exact "${expected}" (must be declared in this package.json)`,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { name: 'nextConfig', passed, messages }
|
|
135
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Glob } from 'bun'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
5
|
+
|
|
6
|
+
const NEXT_DEV_RE = /\bnext dev\b/
|
|
7
|
+
const PORTLESS_WRAP_RE = /\bportless\s+\S+\s+next dev\b/
|
|
8
|
+
|
|
9
|
+
type WorkspaceField = string[] | { packages?: string[] } | undefined
|
|
10
|
+
|
|
11
|
+
const workspacePatterns = (pkg: PackageJson | null): string[] => {
|
|
12
|
+
const ws = (pkg as { workspaces?: WorkspaceField } | null)?.workspaces
|
|
13
|
+
if (Array.isArray(ws)) return ws
|
|
14
|
+
if (ws && Array.isArray(ws.packages)) return ws.packages
|
|
15
|
+
return []
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const collectPackageJsons = async (project: Project): Promise<Array<{ relPath: string; pkg: PackageJson }>> => {
|
|
19
|
+
const results: Array<{ relPath: string; pkg: PackageJson }> = []
|
|
20
|
+
if (project.packageJson) {
|
|
21
|
+
results.push({ relPath: 'package.json', pkg: project.packageJson })
|
|
22
|
+
}
|
|
23
|
+
for (const pattern of workspacePatterns(project.packageJson)) {
|
|
24
|
+
const target = `${pattern}/package.json`
|
|
25
|
+
if (pattern.includes('*')) {
|
|
26
|
+
const glob = new Glob(target)
|
|
27
|
+
for await (const path of glob.scan({ cwd: project.path, onlyFiles: true })) {
|
|
28
|
+
if (path.startsWith('node_modules/')) continue
|
|
29
|
+
const pkgFile = Bun.file(resolve(project.path, path))
|
|
30
|
+
if (await pkgFile.exists()) {
|
|
31
|
+
results.push({ relPath: path, pkg: (await pkgFile.json()) as PackageJson })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
const pkgFile = Bun.file(resolve(project.path, target))
|
|
36
|
+
if (await pkgFile.exists()) {
|
|
37
|
+
results.push({ relPath: target, pkg: (await pkgFile.json()) as PackageJson })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return results
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const portlessNextDev = async (
|
|
45
|
+
projects: Project[],
|
|
46
|
+
options: NonNullable<WardenConfig['checks']['portlessNextDev']>,
|
|
47
|
+
): Promise<CheckResult> => {
|
|
48
|
+
const messages: string[] = []
|
|
49
|
+
let passed = true
|
|
50
|
+
|
|
51
|
+
for (const project of projects) {
|
|
52
|
+
const pkgs = await collectPackageJsons(project)
|
|
53
|
+
|
|
54
|
+
for (const { relPath, pkg } of pkgs) {
|
|
55
|
+
const scripts = pkg.scripts ?? {}
|
|
56
|
+
let usesNextDev = false
|
|
57
|
+
|
|
58
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
59
|
+
if (!NEXT_DEV_RE.test(value)) continue
|
|
60
|
+
usesNextDev = true
|
|
61
|
+
if (!PORTLESS_WRAP_RE.test(value)) {
|
|
62
|
+
passed = false
|
|
63
|
+
messages.push(
|
|
64
|
+
`${project.name}/${relPath}: script "${name}" is ${JSON.stringify(value)} — must be wrapped as "portless <app-name> next dev"`,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (usesNextDev) {
|
|
70
|
+
const declared = pkg.devDependencies?.portless
|
|
71
|
+
if (declared !== options.version) {
|
|
72
|
+
const actual = declared === undefined ? 'missing' : JSON.stringify(declared)
|
|
73
|
+
passed = false
|
|
74
|
+
messages.push(
|
|
75
|
+
`${project.name}/${relPath}: uses "next dev" but devDependencies.portless is ${actual} — expected exact "${options.version}" (must be declared in this package.json)`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { name: 'portlessNextDev', passed, messages }
|
|
83
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CheckResult, Project, WardenConfig } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
export const requiredScripts = (
|
|
4
|
+
projects: Project[],
|
|
5
|
+
options: NonNullable<WardenConfig['checks']['requiredScripts']>,
|
|
6
|
+
): CheckResult => {
|
|
7
|
+
const messages: string[] = []
|
|
8
|
+
let passed = true
|
|
9
|
+
|
|
10
|
+
for (const project of projects) {
|
|
11
|
+
const scripts = project.packageJson?.scripts
|
|
12
|
+
if (!scripts) {
|
|
13
|
+
passed = false
|
|
14
|
+
messages.push(`${project.name}: no "scripts" in package.json`)
|
|
15
|
+
continue
|
|
16
|
+
}
|
|
17
|
+
for (const [name, expected] of Object.entries(options.scripts)) {
|
|
18
|
+
const actual = scripts[name]
|
|
19
|
+
if (actual === undefined) {
|
|
20
|
+
passed = false
|
|
21
|
+
messages.push(`${project.name}: missing script "${name}" (expected ${JSON.stringify(expected)})`)
|
|
22
|
+
} else if (actual !== expected) {
|
|
23
|
+
passed = false
|
|
24
|
+
messages.push(
|
|
25
|
+
`${project.name}: script "${name}" is ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { name: 'requiredScripts', passed, messages }
|
|
32
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
const TAILWIND = 'tailwindcss'
|
|
4
|
+
const OXFMT_CONFIG = '.oxfmtrc.json'
|
|
5
|
+
const REQUIRED_KEY = 'sortTailwindcss'
|
|
6
|
+
|
|
7
|
+
const dependsOnTailwind = (pkg: PackageJson): boolean =>
|
|
8
|
+
TAILWIND in (pkg.dependencies ?? {}) ||
|
|
9
|
+
TAILWIND in (pkg.devDependencies ?? {}) ||
|
|
10
|
+
TAILWIND in (pkg.peerDependencies ?? {})
|
|
11
|
+
|
|
12
|
+
export const tailwindOxfmtConfig = async (
|
|
13
|
+
projects: Project[],
|
|
14
|
+
_options: NonNullable<WardenConfig['checks']['tailwindOxfmtConfig']>,
|
|
15
|
+
): Promise<CheckResult> => {
|
|
16
|
+
const messages: string[] = []
|
|
17
|
+
let passed = true
|
|
18
|
+
|
|
19
|
+
for (const project of projects) {
|
|
20
|
+
const oxfmtFile = Bun.file(`${project.path}/${OXFMT_CONFIG}`)
|
|
21
|
+
if (!(await oxfmtFile.exists())) continue
|
|
22
|
+
|
|
23
|
+
const oxfmt = (await oxfmtFile.json()) as Record<string, unknown>
|
|
24
|
+
const present = REQUIRED_KEY in oxfmt
|
|
25
|
+
|
|
26
|
+
if (present && oxfmt[REQUIRED_KEY] !== true) {
|
|
27
|
+
passed = false
|
|
28
|
+
messages.push(
|
|
29
|
+
`${project.name}: ${OXFMT_CONFIG} "${REQUIRED_KEY}" must be \`true\` (got ${JSON.stringify(oxfmt[REQUIRED_KEY])})`,
|
|
30
|
+
)
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!present && project.packageJson && dependsOnTailwind(project.packageJson)) {
|
|
35
|
+
passed = false
|
|
36
|
+
messages.push(`${project.name}: depends on ${TAILWIND} but ${OXFMT_CONFIG} is missing "${REQUIRED_KEY}": true`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { name: 'tailwindOxfmtConfig', passed, messages }
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Glob } from 'bun'
|
|
2
|
+
|
|
3
|
+
import type { CheckResult, Project, WardenConfig } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
const projectHasTests = async (projectPath: string, pattern: string): Promise<boolean> => {
|
|
6
|
+
const glob = new Glob(pattern)
|
|
7
|
+
for await (const path of glob.scan({ cwd: projectPath, onlyFiles: true })) {
|
|
8
|
+
if (path.startsWith('node_modules/')) continue
|
|
9
|
+
return true
|
|
10
|
+
}
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const testScriptConsistency = async (
|
|
15
|
+
projects: Project[],
|
|
16
|
+
options: NonNullable<WardenConfig['checks']['testScriptConsistency']>,
|
|
17
|
+
): Promise<CheckResult> => {
|
|
18
|
+
const messages: string[] = []
|
|
19
|
+
let passed = true
|
|
20
|
+
|
|
21
|
+
for (const project of projects) {
|
|
22
|
+
const scripts = project.packageJson?.scripts ?? {}
|
|
23
|
+
const hasTests = await projectHasTests(project.path, options.testFilePattern)
|
|
24
|
+
|
|
25
|
+
if (hasTests) {
|
|
26
|
+
if (scripts.test !== options.testScript) {
|
|
27
|
+
passed = false
|
|
28
|
+
messages.push(
|
|
29
|
+
`${project.name}: has test files but "test" script is ${JSON.stringify(scripts.test)}, expected ${JSON.stringify(options.testScript)}`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
if (scripts.all !== options.allWithTests) {
|
|
33
|
+
passed = false
|
|
34
|
+
messages.push(
|
|
35
|
+
`${project.name}: "all" is ${JSON.stringify(scripts.all)}, expected ${JSON.stringify(options.allWithTests)} (project has tests)`,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
if (scripts.all !== options.allWithoutTests) {
|
|
40
|
+
passed = false
|
|
41
|
+
messages.push(
|
|
42
|
+
`${project.name}: "all" is ${JSON.stringify(scripts.all)}, expected ${JSON.stringify(options.allWithoutTests)} (no tests detected)`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { name: 'testScriptConsistency', passed, messages }
|
|
49
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { CheckResult, Project, WardenConfig } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
const SINGLE = 'tsc --noEmit'
|
|
4
|
+
const SOLUTION = 'tsc -b --noEmit'
|
|
5
|
+
const FILTER_ALL = "bun run --filter '*' typecheck"
|
|
6
|
+
|
|
7
|
+
const readTsconfigReferences = async (projectPath: string): Promise<boolean> => {
|
|
8
|
+
const file = Bun.file(`${projectPath}/tsconfig.json`)
|
|
9
|
+
if (!(await file.exists())) return false
|
|
10
|
+
try {
|
|
11
|
+
const tsconfig = (await file.json()) as { references?: unknown[] }
|
|
12
|
+
return Array.isArray(tsconfig.references) && tsconfig.references.length > 0
|
|
13
|
+
} catch {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const typecheckScript = async (
|
|
19
|
+
projects: Project[],
|
|
20
|
+
options: NonNullable<WardenConfig['checks']['typecheckScript']>,
|
|
21
|
+
): Promise<CheckResult> => {
|
|
22
|
+
const messages: string[] = []
|
|
23
|
+
let passed = true
|
|
24
|
+
|
|
25
|
+
for (const project of projects) {
|
|
26
|
+
const scripts = project.packageJson?.scripts
|
|
27
|
+
if (!scripts) {
|
|
28
|
+
passed = false
|
|
29
|
+
messages.push(`${project.name}: no "scripts" in package.json`)
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
const mode = options.modes?.[project.name] ?? 'auto'
|
|
33
|
+
const expected =
|
|
34
|
+
mode === 'bun-filter-all' ? FILTER_ALL : (await readTsconfigReferences(project.path)) ? SOLUTION : SINGLE
|
|
35
|
+
const reason =
|
|
36
|
+
mode === 'bun-filter-all'
|
|
37
|
+
? "mode 'bun-filter-all'"
|
|
38
|
+
: `root tsconfig ${expected === SOLUTION ? 'has' : 'has no'} project references`
|
|
39
|
+
const actual = scripts.typecheck
|
|
40
|
+
if (actual === undefined) {
|
|
41
|
+
passed = false
|
|
42
|
+
messages.push(`${project.name}: missing script "typecheck" (expected ${JSON.stringify(expected)})`)
|
|
43
|
+
} else if (actual !== expected) {
|
|
44
|
+
passed = false
|
|
45
|
+
messages.push(
|
|
46
|
+
`${project.name}: script "typecheck" is ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)} (${reason})`,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { name: 'typecheckScript', passed, messages }
|
|
52
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CheckResult, PackageJson, Project, WardenConfig } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
const dependsOn = (pkg: PackageJson, name: string): boolean =>
|
|
4
|
+
name in (pkg.dependencies ?? {}) || name in (pkg.devDependencies ?? {}) || name in (pkg.peerDependencies ?? {})
|
|
5
|
+
|
|
6
|
+
export const wardenScript = (
|
|
7
|
+
projects: Project[],
|
|
8
|
+
options: NonNullable<WardenConfig['checks']['wardenScript']>,
|
|
9
|
+
): CheckResult => {
|
|
10
|
+
const messages: string[] = []
|
|
11
|
+
let passed = true
|
|
12
|
+
|
|
13
|
+
for (const project of projects) {
|
|
14
|
+
const pkg = project.packageJson
|
|
15
|
+
if (!pkg || !dependsOn(pkg, options.package)) continue
|
|
16
|
+
|
|
17
|
+
const scripts = pkg.scripts ?? {}
|
|
18
|
+
if (scripts.warden !== 'warden') {
|
|
19
|
+
passed = false
|
|
20
|
+
messages.push(
|
|
21
|
+
`${project.name}: depends on ${options.package} but "warden" script is ${JSON.stringify(scripts.warden)}, expected "warden"`,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
if (!scripts.all || !scripts.all.includes('bun run warden')) {
|
|
25
|
+
passed = false
|
|
26
|
+
messages.push(
|
|
27
|
+
`${project.name}: depends on ${options.package} but "all" script ${JSON.stringify(scripts.all)} is missing "bun run warden"`,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { name: 'wardenScript', passed, messages }
|
|
33
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { basename, resolve } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import type { PackageJson, Project, WardenConfig } from './types.ts'
|
|
4
|
+
|
|
5
|
+
export const loadConfig = async (baseDir: string): Promise<WardenConfig> => {
|
|
6
|
+
const packageJsonPath = `${baseDir}/package.json`
|
|
7
|
+
const packageJsonFile = Bun.file(packageJsonPath)
|
|
8
|
+
if (!(await packageJsonFile.exists())) {
|
|
9
|
+
throw new Error(`No package.json found at ${packageJsonPath}`)
|
|
10
|
+
}
|
|
11
|
+
const pkg = (await packageJsonFile.json()) as PackageJson & { warden?: WardenConfig }
|
|
12
|
+
if (!pkg.warden) {
|
|
13
|
+
throw new Error(`No "warden" key in ${packageJsonPath}`)
|
|
14
|
+
}
|
|
15
|
+
return pkg.warden
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const resolveProjects = async (config: WardenConfig, baseDir: string): Promise<Project[]> => {
|
|
19
|
+
const projectPaths = config.projects && config.projects.length > 0 ? config.projects : ['.']
|
|
20
|
+
return Promise.all(
|
|
21
|
+
projectPaths.map(async projectPath => {
|
|
22
|
+
const path = resolve(baseDir, projectPath)
|
|
23
|
+
const packageJsonFile = Bun.file(`${path}/package.json`)
|
|
24
|
+
const packageJson = (await packageJsonFile.exists()) ? ((await packageJsonFile.json()) as PackageJson) : null
|
|
25
|
+
return { name: basename(path), path, packageJson }
|
|
26
|
+
}),
|
|
27
|
+
)
|
|
28
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
import { configFilesPresent } from './checks/config-files-present.ts'
|
|
5
|
+
import { exactDependencyVersions } from './checks/exact-dependency-versions.ts'
|
|
6
|
+
import { matchingDependencyVersions } from './checks/matching-dependency-versions.ts'
|
|
7
|
+
import { nextConfig } from './checks/next-config.ts'
|
|
8
|
+
import { portlessNextDev } from './checks/portless-next-dev.ts'
|
|
9
|
+
import { requiredScripts } from './checks/required-scripts.ts'
|
|
10
|
+
import { tailwindOxfmtConfig } from './checks/tailwind-oxfmt-config.ts'
|
|
11
|
+
import { testScriptConsistency } from './checks/test-script-consistency.ts'
|
|
12
|
+
import { typecheckScript } from './checks/typecheck-script.ts'
|
|
13
|
+
import { wardenScript } from './checks/warden-script.ts'
|
|
14
|
+
import { loadConfig, resolveProjects } from './config.ts'
|
|
15
|
+
|
|
16
|
+
import type { CheckResult } from './types.ts'
|
|
17
|
+
|
|
18
|
+
const baseDir = process.cwd()
|
|
19
|
+
|
|
20
|
+
const config = await loadConfig(baseDir)
|
|
21
|
+
const allProjects = await resolveProjects(config, baseDir)
|
|
22
|
+
|
|
23
|
+
const filterNames = process.argv.slice(2)
|
|
24
|
+
const knownNames = new Set(allProjects.map(p => p.name))
|
|
25
|
+
const unknown = filterNames.filter(n => !knownNames.has(n))
|
|
26
|
+
if (unknown.length > 0) {
|
|
27
|
+
console.error(
|
|
28
|
+
`Unknown project(s): ${unknown.join(', ')}\nConfigured projects: ${allProjects.map(p => p.name).join(', ')}`,
|
|
29
|
+
)
|
|
30
|
+
process.exit(2)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const projects = filterNames.length > 0 ? allProjects.filter(p => filterNames.includes(p.name)) : allProjects
|
|
34
|
+
|
|
35
|
+
const missing = projects.filter(p => !existsSync(p.path))
|
|
36
|
+
if (missing.length > 0) {
|
|
37
|
+
const detail = missing.map(p => ` - ${p.name} (expected at ${p.path})`).join('\n')
|
|
38
|
+
const noun = missing.length === 1 ? 'directory is' : 'directories are'
|
|
39
|
+
console.error(
|
|
40
|
+
`Cannot run warden — the following project ${noun} not on disk:\n${detail}\n\nWarden resolves project paths relative to ${baseDir}. In a cloud env, ensure each attached repo is checked out at the expected path and pass only those names as arguments.`,
|
|
41
|
+
)
|
|
42
|
+
process.exit(2)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const scopeNote = filterNames.length > 0 ? ` (filtered from ${allProjects.length} configured)` : ''
|
|
46
|
+
console.log(`warden — checking ${projects.length} project(s)${scopeNote}: ${projects.map(p => p.name).join(', ')}\n`)
|
|
47
|
+
|
|
48
|
+
const results: CheckResult[] = []
|
|
49
|
+
|
|
50
|
+
if (config.checks.configFilesPresent?.enabled) {
|
|
51
|
+
results.push(await configFilesPresent(projects, config.checks.configFilesPresent))
|
|
52
|
+
}
|
|
53
|
+
if (config.checks.matchingDependencyVersions?.enabled) {
|
|
54
|
+
results.push(matchingDependencyVersions(projects, config.checks.matchingDependencyVersions))
|
|
55
|
+
}
|
|
56
|
+
if (config.checks.exactDependencyVersions?.enabled) {
|
|
57
|
+
results.push(exactDependencyVersions(projects, config.checks.exactDependencyVersions))
|
|
58
|
+
}
|
|
59
|
+
if (config.checks.requiredScripts?.enabled) {
|
|
60
|
+
results.push(requiredScripts(projects, config.checks.requiredScripts))
|
|
61
|
+
}
|
|
62
|
+
if (config.checks.typecheckScript?.enabled) {
|
|
63
|
+
results.push(await typecheckScript(projects, config.checks.typecheckScript))
|
|
64
|
+
}
|
|
65
|
+
if (config.checks.testScriptConsistency?.enabled) {
|
|
66
|
+
results.push(await testScriptConsistency(projects, config.checks.testScriptConsistency))
|
|
67
|
+
}
|
|
68
|
+
if (config.checks.tailwindOxfmtConfig?.enabled) {
|
|
69
|
+
results.push(await tailwindOxfmtConfig(projects, config.checks.tailwindOxfmtConfig))
|
|
70
|
+
}
|
|
71
|
+
if (config.checks.portlessNextDev?.enabled) {
|
|
72
|
+
results.push(await portlessNextDev(projects, config.checks.portlessNextDev))
|
|
73
|
+
}
|
|
74
|
+
if (config.checks.nextConfig?.enabled) {
|
|
75
|
+
results.push(await nextConfig(projects, config.checks.nextConfig))
|
|
76
|
+
}
|
|
77
|
+
if (config.checks.wardenScript?.enabled) {
|
|
78
|
+
results.push(wardenScript(projects, config.checks.wardenScript))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const result of results) {
|
|
82
|
+
const tag = result.passed ? '[PASS]' : '[FAIL]'
|
|
83
|
+
console.log(`${tag} ${result.name}`)
|
|
84
|
+
for (const message of result.messages) {
|
|
85
|
+
console.log(` ${message}`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const failed = results.filter(r => !r.passed).length
|
|
90
|
+
console.log(`\n${results.length - failed}/${results.length} checks passed`)
|
|
91
|
+
process.exit(failed === 0 ? 0 : 1)
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type WardenConfig = {
|
|
2
|
+
projects?: string[]
|
|
3
|
+
checks: {
|
|
4
|
+
configFilesPresent?: { enabled: boolean; files: string[] }
|
|
5
|
+
matchingDependencyVersions?: { enabled: boolean; packages: string[] }
|
|
6
|
+
exactDependencyVersions?: { enabled: boolean; packages: Record<string, string> }
|
|
7
|
+
requiredScripts?: { enabled: boolean; scripts: Record<string, string> }
|
|
8
|
+
typecheckScript?: { enabled: boolean; modes?: Record<string, 'auto' | 'bun-filter-all'> }
|
|
9
|
+
testScriptConsistency?: {
|
|
10
|
+
enabled: boolean
|
|
11
|
+
testFilePattern: string
|
|
12
|
+
testScript: string
|
|
13
|
+
allWithoutTests: string
|
|
14
|
+
allWithTests: string
|
|
15
|
+
}
|
|
16
|
+
tailwindOxfmtConfig?: { enabled: boolean }
|
|
17
|
+
portlessNextDev?: { enabled: boolean; version: string }
|
|
18
|
+
nextConfig?: {
|
|
19
|
+
enabled: boolean
|
|
20
|
+
options: Record<string, unknown>
|
|
21
|
+
devDependencies: Record<string, string>
|
|
22
|
+
}
|
|
23
|
+
wardenScript?: { enabled: boolean; package: string }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type PackageJson = {
|
|
28
|
+
name?: string
|
|
29
|
+
version?: string
|
|
30
|
+
scripts?: Record<string, string>
|
|
31
|
+
dependencies?: Record<string, string>
|
|
32
|
+
devDependencies?: Record<string, string>
|
|
33
|
+
peerDependencies?: Record<string, string>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type Project = {
|
|
37
|
+
name: string
|
|
38
|
+
path: string
|
|
39
|
+
packageJson: PackageJson | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type CheckResult = {
|
|
43
|
+
name: string
|
|
44
|
+
passed: boolean
|
|
45
|
+
messages: string[]
|
|
46
|
+
}
|