@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verekia/warden",
3
- "version": "0.0.1",
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 && options.version !== undefined) {
70
+ if (usesNextDev) {
71
+ const expectedVersion = options.version ?? DEFAULT_VERSION
70
72
  const declared = pkg.devDependencies?.portless
71
- if (declared !== options.version) {
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 "${options.version}" (must be declared in this package.json)`,
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 (options.allWithTests !== undefined && scripts.all !== options.allWithTests) {
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(options.allWithTests)} (project has tests)`,
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 (options.allWithoutTests !== undefined && scripts.all !== options.allWithoutTests) {
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(options.allWithoutTests)} (no tests detected)`,
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]'
package/src/types.ts CHANGED
@@ -21,6 +21,7 @@ export type WardenConfig = {
21
21
  devDependencies?: Record<string, string>
22
22
  }
23
23
  wardenScript?: { enabled?: boolean; package?: string }
24
+ ciWorkflow?: { enabled?: boolean; file?: string; runs?: string }
24
25
  }
25
26
  }
26
27