@prb/devkit 1.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/.prettierrc.js ADDED
@@ -0,0 +1,13 @@
1
+ /** @type {import("prettier").Config} */
2
+ export default {
3
+ printWidth: 120,
4
+ trailingComma: "all",
5
+ overrides: [
6
+ {
7
+ files: "*.md",
8
+ options: {
9
+ proseWrap: "always",
10
+ },
11
+ },
12
+ ],
13
+ };
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/prettierrc",
3
+ "printWidth": 120,
4
+ "trailingComma": "all",
5
+ "overrides": [
6
+ {
7
+ "files": "*.md",
8
+ "options": {
9
+ "proseWrap": "always"
10
+ }
11
+ }
12
+ ]
13
+ }
package/LICENSE.md ADDED
@@ -0,0 +1,16 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Paul Razvan Berg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ persons to whom the Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # 🛠️ Devkit
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
4
+ [![npm](https://img.shields.io/npm/v/@prb/devkit)](https://www.npmjs.com/package/@prb/devkit)
5
+
6
+ Personal configuration files and reusable scripts. Designed to be extended and customized as needed.
7
+
8
+ ## 📦 Installation
9
+
10
+ ```bash
11
+ npm install @prb/devkit
12
+ ```
13
+
14
+ Or with other package managers:
15
+
16
+ ```bash
17
+ pnpm add @prb/devkit
18
+ bun add @prb/devkit
19
+ ```
20
+
21
+ ## 🚀 Usage
22
+
23
+ ### Biome
24
+
25
+ Extend the base Biome configuration in your `biome.jsonc`:
26
+
27
+ ```jsonc
28
+ {
29
+ "$schema": "https://biomejs.dev/schemas/latest/schema.json",
30
+ "extends": ["@prb/devkit/biome"],
31
+ }
32
+ ```
33
+
34
+ For UI projects, use the UI variant:
35
+
36
+ ```jsonc
37
+ {
38
+ "extends": ["@prb/devkit/biome/ui"],
39
+ }
40
+ ```
41
+
42
+ ### Prettier
43
+
44
+ Reference the Prettier config in your `package.json`:
45
+
46
+ ```json
47
+ {
48
+ "prettier": "@prb/devkit/prettier"
49
+ }
50
+ ```
51
+
52
+ ### TypeScript
53
+
54
+ Extend TSConfig presets in your `tsconfig.json`:
55
+
56
+ ```json
57
+ {
58
+ "extends": "@prb/devkit/tsconfig/base"
59
+ }
60
+ ```
61
+
62
+ Available presets:
63
+
64
+ - `@prb/devkit/tsconfig/base` — Base TypeScript configuration
65
+ - `@prb/devkit/tsconfig/build` — Build-optimized configuration
66
+ - `@prb/devkit/tsconfig/next` — Next.js configuration
67
+
68
+ ### Vitest
69
+
70
+ Use the devkit vitest config factory in your `vitest.config.ts`:
71
+
72
+ ```typescript
73
+ import { defineDevkitConfig } from "@prb/devkit/vitest";
74
+
75
+ export default defineDevkitConfig({
76
+ environment: "jsdom", // or "node" (default), "happy-dom"
77
+ setupFiles: ["./tests/setup.ts"],
78
+ coverage: true,
79
+ });
80
+ ```
81
+
82
+ The config provides CI-aware defaults:
83
+
84
+ - `globals: true`
85
+ - `retry: 2` in CI, `0` locally
86
+ - `testTimeout: 30s` in CI, `10s` locally
87
+ - `reporters: ["basic"]` in CI, `["verbose"]` locally
88
+
89
+ For merging with existing Vite configs:
90
+
91
+ ```typescript
92
+ import { defineDevkitConfig, mergeConfig } from "@prb/devkit/vitest";
93
+ import { defineConfig } from "vitest/config";
94
+
95
+ export default mergeConfig(
96
+ defineDevkitConfig({ environment: "jsdom" }),
97
+ defineConfig({
98
+ test: {
99
+ alias: { "@": "./src" },
100
+ },
101
+ }),
102
+ );
103
+ ```
104
+
105
+ ### Just
106
+
107
+ Import Just recipes in your `justfile`:
108
+
109
+ ```just
110
+ import "@prb/devkit/just/base.just"
111
+ import "@prb/devkit/just/npm.just"
112
+ ```
113
+
114
+ Available modules:
115
+
116
+ | Module | Description |
117
+ | --------------- | ------------------------------- |
118
+ | `base.just` | Common development recipes |
119
+ | `csv.just` | CSV/TSV validation with qsv |
120
+ | `npm.just` | NPM package management |
121
+ | `settings.just` | Just settings and configuration |
122
+ | `vercel.just` | Vercel build and deploy |
123
+
124
+ ## ⚙️ Available Configs
125
+
126
+ | Tool | Config File/Directory |
127
+ | ----------- | ---------------------------------------- |
128
+ | 🔍 Biome | [`biome/`](./biome/) |
129
+ | 🛠 Just | [`just/`](./just/) |
130
+ | ✨ Prettier | [`.prettierrc.json`](./.prettierrc.json) |
131
+ | 📦 TSConfig | [`tsconfig/`](./tsconfig/) |
132
+ | 🧪 Vitest | [`vitest/`](./vitest/) |
133
+ | 💻 VSCode | [`vscode/`](./vscode/) |
134
+
135
+ ## 🐈‍⬛ GitHub Actions
136
+
137
+ Reusable composite actions for GitHub CI workflows.
138
+
139
+ | Action | Description |
140
+ | --------------------------------------------- | ------------------------------------------ |
141
+ | [`actions/setup`](./actions/setup/) | Install dependencies (Node.js, Just, etc.) |
142
+ | [`actions/node-cache`](./actions/node-cache/) | Cache Node.js dependencies |
143
+
144
+ ```yaml
145
+ - uses: PaulRBerg/devkit/actions/setup@v1
146
+ ```
147
+
148
+ Dependency caching stores only the package-manager cache, not `node_modules`. Set `save-cache: true` in at most one
149
+ parallel job to publish a refreshed cache.
150
+
151
+ ## 🤝 Contributing
152
+
153
+ Contributions are welcome! Please feel free to submit a Pull Request.
154
+
155
+ ## 📄 License
156
+
157
+ This project is licensed under MIT — see the [LICENSE](LICENSE.md) file for details.
@@ -0,0 +1,123 @@
1
+ // Devkit configuration for Biome v2: https://next.biomejs.dev
2
+ //
3
+ // This is a shared base configuration meant to be extended in consuming projects.
4
+ //
5
+ // IMPORTANT: Consumers must add their own `files.includes` patterns to specify
6
+ // which files to lint and format. For example:
7
+ //
8
+ // {
9
+ // "extends": ["@prb/devkit/biome/base"],
10
+ // "files": {
11
+ // "includes": ["**/*.{css,js,jsx,json,ts,tsx}", "!node_modules"]
12
+ // }
13
+ // }
14
+ //
15
+ // You may also add override blocks with custom `includes` patterns for
16
+ // granular rule customization.
17
+ {
18
+ "$schema": "https://biomejs.dev/schemas/latest/schema.json",
19
+ "assist": {
20
+ "enabled": true,
21
+ "actions": {
22
+ "source": {
23
+ "organizeImports": "on",
24
+ "useSortedKeys": {
25
+ "level": "on",
26
+ "options": {
27
+ "groupByNesting": true
28
+ }
29
+ }
30
+ }
31
+ }
32
+ },
33
+ "files": {
34
+ "maxSize": 5242880 // 5MB
35
+ },
36
+ "formatter": {
37
+ "enabled": true,
38
+ "formatWithErrors": true,
39
+ "indentStyle": "space",
40
+ "lineWidth": 100
41
+ },
42
+ // Needed to override ultracite
43
+ "json": {
44
+ "formatter": {
45
+ "lineWidth": 100
46
+ }
47
+ },
48
+ "linter": {
49
+ "enabled": true,
50
+ "rules": {
51
+ "preset": "recommended",
52
+ "complexity": {
53
+ "noImplicitCoercions": "error", // forbid `!!foo`, allow `Boolean(foo)`
54
+ "noVoid": "off", // void is useful in some cases e.g. `useEffect` callbacks
55
+ "useSimplifiedLogicExpression": "off", // the rule is cool but it gets agents stuck
56
+ "noUselessUndefined": "off" // we want explicit undefined return values
57
+ },
58
+ "correctness": {
59
+ "noUnusedImports": "off", // allow unused imports during development
60
+ "noUnusedVariables": "error"
61
+ },
62
+ "nursery": {
63
+ "noFloatingPromises": "error" // floating promises can lead to bugs
64
+ },
65
+ "performance": {
66
+ "noBarrelFile": "off", // barrel exports lead to cleaner imports
67
+ "noDelete": "off", // disabled due to https://github.com/biomejs/biome/issues/4093
68
+ "noNamespaceImport": "off" // namespaces are cool
69
+ },
70
+ "style": {
71
+ "noEnum": "off", // enums are useful in some cases e.g. UI states
72
+ "noNamespace": "off", // namespaces are cool
73
+ "useBlockStatements": "off", // turned off because it gets AIs stuck
74
+ "useDefaultSwitchClause": "off", // already handled by TypeScript
75
+ "useTemplate": "off", // allow string concatenation using `+`
76
+ "useConsistentTypeDefinitions": {
77
+ "level": "warn",
78
+ "options": {
79
+ "style": "type"
80
+ }
81
+ },
82
+ "useFilenamingConvention": {
83
+ "level": "error",
84
+ "options": {
85
+ "filenameCases": ["kebab-case", "camelCase", "PascalCase", "export"]
86
+ }
87
+ },
88
+ // disabled due to https://github.com/biomejs/biome/issues/8450
89
+ "useImportType": {
90
+ "level": "warn",
91
+ "options": {
92
+ "style": "separatedType"
93
+ }
94
+ }
95
+ }
96
+ }
97
+ },
98
+ "overrides": [
99
+ {
100
+ // Disabling sorted keys because ordering is often important in JSON files
101
+ "includes": ["**/*.{json,json5,jsonc}"],
102
+ "assist": {
103
+ "actions": {
104
+ "source": {
105
+ "useSortedKeys": "off"
106
+ }
107
+ }
108
+ },
109
+ // Needed to override ultracite
110
+ "json": {
111
+ "parser": {
112
+ "allowComments": true,
113
+ "allowTrailingCommas": true
114
+ }
115
+ }
116
+ }
117
+ ],
118
+ "vcs": {
119
+ "clientKind": "git",
120
+ "enabled": true,
121
+ "useIgnoreFile": true
122
+ }
123
+ }
package/biome/ui.jsonc ADDED
@@ -0,0 +1,80 @@
1
+ // Devkit UI configuration for Biome v2
2
+ //
3
+ // Extends base configuration with UI-specific rules for accessibility,
4
+ // Tailwind CSS class sorting, and CSS modules support.
5
+ //
6
+ // IMPORTANT: Consumers must extend BOTH configs and add their own
7
+ // `files.includes` patterns. For example:
8
+ //
9
+ // {
10
+ // "extends": ["@prb/devkit/biome/base", "@prb/devkit/biome/ui"],
11
+ // "files": {
12
+ // "includes": ["**/*.{css,js,jsx,json,ts,tsx}", "!node_modules"]
13
+ // }
14
+ // }
15
+ {
16
+ "$schema": "https://biomejs.dev/schemas/latest/schema.json",
17
+ "assist": {
18
+ "enabled": true,
19
+ "actions": {
20
+ "source": {
21
+ "noDuplicateClasses": "on",
22
+ "useSortedAttributes": "on"
23
+ }
24
+ }
25
+ },
26
+ "css": {
27
+ "formatter": {
28
+ "lineWidth": 100
29
+ },
30
+ "parser": {
31
+ "cssModules": true,
32
+ "tailwindDirectives": true
33
+ }
34
+ },
35
+ "linter": {
36
+ "rules": {
37
+ "a11y": {
38
+ "noSvgWithoutTitle": "off",
39
+ "useKeyWithClickEvents": "off"
40
+ },
41
+ "correctness": {
42
+ "useExhaustiveDependencies": "off" // doesn't work with React Compiler: https://github.com/biomejs/biome/issues/5293
43
+ },
44
+ "nursery": {
45
+ // Tailwind class ordering is handled by ESLint (better-tailwindcss/enforce-consistent-class-order).
46
+ // Biome's useSortedClasses lacks a Tailwind v4 sort preset and sorts text-* utilities alphabetically,
47
+ // interleaving font-size (text-sm) with color (text-red-500) utilities.
48
+ // Tracked upstream: https://github.com/biomejs/biome/issues/1274
49
+ "useSortedClasses": "off"
50
+ },
51
+ "style": {
52
+ // Promoted out of nursery in Biome 2.5.0.
53
+ "useErrorCause": "error"
54
+ },
55
+ "suspicious": {
56
+ "noConsole": {
57
+ "level": "warn",
58
+ "options": {
59
+ // console.error allowed for error boundaries and critical failures
60
+ "allow": ["error"]
61
+ }
62
+ }
63
+ }
64
+ }
65
+ },
66
+ "overrides": [
67
+ // ── Styles files ─────────────────────────────────────────────────────────
68
+ // Disable alphabetical sorting in styles files, where order is important for CSS
69
+ {
70
+ "includes": ["**/*.styles.ts"],
71
+ "assist": {
72
+ "actions": {
73
+ "source": {
74
+ "useSortedKeys": "off"
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ]
80
+ }
@@ -0,0 +1,85 @@
1
+ """Shared helpers for Vercel Just recipes."""
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+
8
+
9
+ def run(*cmd):
10
+ """Run a command, exiting on failure."""
11
+ result = subprocess.run(cmd)
12
+ if result.returncode != 0:
13
+ sys.exit(result.returncode)
14
+
15
+
16
+ def run_capture_url(*cmd) -> str:
17
+ """Run a command, tee its stdout to the user, and return the last non-empty stdout line.
18
+
19
+ Used to capture the deployment URL printed by `vercel deploy` while still streaming
20
+ its output to the caller. Exits on non-zero return codes, mirroring `run`.
21
+ """
22
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True, bufsize=1)
23
+ assert proc.stdout is not None
24
+ last = ""
25
+ for line in proc.stdout:
26
+ sys.stdout.write(line)
27
+ sys.stdout.flush()
28
+ stripped = line.strip()
29
+ if stripped:
30
+ last = stripped
31
+ proc.wait()
32
+ if proc.returncode != 0:
33
+ sys.exit(proc.returncode)
34
+ return last
35
+
36
+
37
+ def emit_deployment_url(url: str) -> None:
38
+ """Publish the deployment URL to GitHub Actions and/or a caller-specified file.
39
+
40
+ - If `GITHUB_OUTPUT` is set, appends `deployment_url=<url>` so workflow steps can read it
41
+ via `steps.<id>.outputs.deployment_url`.
42
+ - If `VERCEL_DEPLOYMENT_URL_FILE` is set, writes the URL to that path (useful for local
43
+ scripts and non-GitHub CI systems).
44
+ - Always prints a human-readable `Deployment URL: <url>` line to stdout.
45
+ """
46
+ github_output = os.environ.get("GITHUB_OUTPUT")
47
+ if github_output:
48
+ with open(github_output, "a", encoding="utf-8") as fh:
49
+ fh.write(f"deployment_url={url}\n")
50
+
51
+ url_file = os.environ.get("VERCEL_DEPLOYMENT_URL_FILE")
52
+ if url_file:
53
+ with open(url_file, "w", encoding="utf-8") as fh:
54
+ fh.write(url)
55
+
56
+ print(f"\nDeployment URL: {url}")
57
+
58
+
59
+ def resolve_vercel_env(app, project_ids_json):
60
+ """Resolve VERCEL_PROJECT_ID and VERCEL_TOKEN, setting them in os.environ.
61
+
62
+ Precedence for project ID:
63
+ 1. VERCEL_PROJECT_ID already set in the environment
64
+ 2. Lookup `app` in the VERCEL_PROJECT_IDS JSON map
65
+
66
+ Returns (project_id, token).
67
+ """
68
+ project_id = os.environ.get("VERCEL_PROJECT_ID", "")
69
+ if not project_id:
70
+ project_ids = json.loads(project_ids_json)
71
+ if app not in project_ids:
72
+ print(
73
+ f"Error: unknown app '{app}'. Known apps: {', '.join(project_ids) or '(none)'}",
74
+ file=sys.stderr,
75
+ )
76
+ sys.exit(1)
77
+ project_id = project_ids[app]
78
+ os.environ["VERCEL_PROJECT_ID"] = project_id
79
+
80
+ token = os.environ.get("VERCEL_TOKEN", "")
81
+ if not token:
82
+ print("Error: VERCEL_TOKEN is not set.", file=sys.stderr)
83
+ sys.exit(1)
84
+
85
+ return project_id, token
package/just/base.just ADDED
@@ -0,0 +1,146 @@
1
+ import "./settings.just"
2
+ import "./utils.just"
3
+
4
+ # ---------------------------------------------------------------------------- #
5
+ # DEPENDENCIES #
6
+ # ---------------------------------------------------------------------------- #
7
+
8
+ # Ni: https://github.com/antfu-collective/ni
9
+ na := require("na")
10
+ ni := require("ni")
11
+ nlx := require("nlx")
12
+
13
+ # ---------------------------------------------------------------------------- #
14
+ # CONSTANTS #
15
+ # ---------------------------------------------------------------------------- #
16
+
17
+ GLOBS_PRETTIER := "\"**/*.{md,mdx,yaml,yml}\""
18
+
19
+ # ---------------------------------------------------------------------------- #
20
+ # RECIPES #
21
+ # ---------------------------------------------------------------------------- #
22
+
23
+ # Clean files
24
+ [no-cd]
25
+ clean:
26
+ just _clean ".DS_Store"
27
+
28
+ [no-cd]
29
+ _clean +globs:
30
+ nlx del-cli "{{ globs }}"
31
+
32
+ # Clear node_modules recursively
33
+ [confirm("Are you sure you want to delete all node_modules, including in subdirectories? [y/N]"), no-cd]
34
+ @clean-modules +globs="**/node_modules":
35
+ echo "🧹 Deleting node_modules recursively..."
36
+ nlx del-cli --verbose {{ globs }}
37
+
38
+ # Install the Node.js dependencies; run with --frozen to install the dependencies with the frozen lockfile
39
+ [no-cd]
40
+ @install *args:
41
+ ni {{ args }}
42
+
43
+ # Build with TypeScript
44
+ [no-cd]
45
+ [arg("project", long, short="p", help="Path to tsconfig.json")]
46
+ @tsc-build project="tsconfig.json":
47
+ na tsc -p {{ project }}
48
+ alias tb := tsc-build
49
+
50
+ # ---------------------------------------------------------------------------- #
51
+ # CHECKS #
52
+ # ---------------------------------------------------------------------------- #
53
+
54
+ # Check code with Biome - runs both the checker and the linter
55
+ [group("checks"), no-cd]
56
+ @biome-check +globs=".":
57
+ na biome check {{ globs }}
58
+ alias bc := biome-check
59
+
60
+ # Lint code with Biome
61
+ [group("checks"), no-cd]
62
+ @biome-lint +globs=".":
63
+ na biome lint {{ globs }}
64
+ alias bl := biome-lint
65
+
66
+ # Fix code with Biome
67
+ # The `noUnusedImports` rule is disabled by default to allow unused imports during development
68
+ [group("checks"), no-cd]
69
+ @biome-write +globs=".":
70
+ na biome check --write {{ globs }}
71
+ na biome lint --unsafe --write --only correctness/noUnusedImports {{ globs }}
72
+ alias bw := biome-write
73
+
74
+ # Run all code checks
75
+ [group("checks"), no-cd]
76
+ @_full-check:
77
+ just _run-with-status biome-check
78
+ just _run-with-status prettier-check
79
+ just _run-with-status type-check
80
+
81
+ # Run all code checks
82
+ [group("checks"), no-cd]
83
+ @full-check: _full-check
84
+ echo ""
85
+ echo '{{ GREEN }}All code checks passed!{{ NORMAL }}'
86
+ alias fc := full-check
87
+
88
+ # Run all code fixes
89
+ [group("checks"), no-cd]
90
+ @_full-write:
91
+ just _run-with-status biome-write
92
+ just _run-with-status prettier-write
93
+
94
+ # Run all code fixes
95
+ [group("checks"), no-cd]
96
+ @full-write: _full-write
97
+ echo ""
98
+ echo '{{ GREEN }}All code fixes applied!{{ NORMAL }}'
99
+ alias fw := full-write
100
+
101
+ # Run knip checks
102
+ [group("checks"), no-cd]
103
+ @knip-check:
104
+ na knip
105
+ alias kc := knip-check
106
+
107
+ # Run knip fix
108
+ [group("checks"), no-cd]
109
+ @knip-write:
110
+ na knip --fix
111
+ alias kw := knip-write
112
+
113
+ # Check Prettier formatting
114
+ [group("checks"), no-cd]
115
+ @prettier-check +globs=GLOBS_PRETTIER:
116
+ na prettier \
117
+ --check \
118
+ --cache \
119
+ --log-level warn \
120
+ --no-error-on-unmatched-pattern \
121
+ {{ globs }}
122
+ alias pc := prettier-check
123
+
124
+ # Format using Prettier
125
+ [group("checks"), no-cd]
126
+ @prettier-write +globs=GLOBS_PRETTIER:
127
+ na prettier \
128
+ --write \
129
+ --cache \
130
+ --log-level warn \
131
+ --no-error-on-unmatched-pattern \
132
+ {{ globs }}
133
+ alias pw := prettier-write
134
+
135
+ # Type check with TypeScript (tsgo default, falls back to tsc if unavailable)
136
+ [group("checks"), no-cd, script("bash")]
137
+ [arg("compiler", long, short="c", help="TypeScript compiler (tsgo or tsc)")]
138
+ [arg("project", long, short="p", help="Path to tsconfig.json")]
139
+ type-check compiler="tsgo" project="tsconfig.json":
140
+ cmd="{{ compiler }}"
141
+ if [[ "$cmd" == "tsgo" ]] && [[ ! -x "node_modules/.bin/tsgo" ]]; then
142
+ cmd="tsc"
143
+ fi
144
+ na "$cmd" --noEmit --project {{ project }}
145
+ alias tc := type-check
146
+ alias tsc-check := type-check
package/just/csv.just ADDED
@@ -0,0 +1,126 @@
1
+ import "./settings.just"
2
+
3
+ # ---------------------------------------------------------------------------- #
4
+ # SCRIPTS #
5
+ # ---------------------------------------------------------------------------- #
6
+
7
+ # Check CSV/TSV files using qsv: https://github.com/dathere/qsv
8
+ [group("checks"), script("python3")]
9
+ [arg("glob", long, short="g", help="Glob pattern for CSV/TSV files")]
10
+ [arg("schema", long, short="s", help="JSON schema file for validation")]
11
+ [arg("ignore", long, short="x", help="Space-separated ignore patterns")]
12
+ _csv-check glob="data/*/*.{csv,tsv}" schema="" ignore="*.invalid *.valid *validation-errors.*":
13
+ import fnmatch
14
+ import glob as globmod
15
+ import re
16
+ import subprocess
17
+ import sys
18
+
19
+ def expand_braces(pattern):
20
+ """Expand brace patterns like {a,b} into multiple patterns."""
21
+ match = re.search(r'\{([^}]+)\}', pattern)
22
+ if not match:
23
+ return [pattern]
24
+ prefix, suffix = pattern[:match.start()], pattern[match.end():]
25
+ return [p for opt in match.group(1).split(',') for p in expand_braces(prefix + opt + suffix)]
26
+
27
+ # Check qsv is available
28
+ if subprocess.run(["which", "qsv"], capture_output=True).returncode != 0:
29
+ print("✗ qsv CLI not found")
30
+ print("Install it: https://github.com/dathere/qsv")
31
+ sys.exit(1)
32
+
33
+ ignore_patterns = "{{ ignore }}".split() if "{{ ignore }}" else []
34
+ schema = "{{ schema }}" or None
35
+ globs = "{{ glob }}"
36
+
37
+ # Infer extension label from glob pattern
38
+ ext_match = re.search(r'\.(\w+|\{[^}]+\})$', globs)
39
+ if ext_match:
40
+ ext = ext_match.group(1)
41
+ if ext.startswith('{') and ext.endswith('}'):
42
+ # Handle brace expansion like {csv,tsv}
43
+ ext_label = '.' + '/'.join(f'.{e}' for e in ext[1:-1].split(','))[1:]
44
+ else:
45
+ ext_label = f'.{ext}'
46
+ else:
47
+ ext_label = 'CSV/TSV'
48
+
49
+ print(f"Validating {ext_label} files...")
50
+ files = [f for pattern in expand_braces(globs) for f in globmod.glob(pattern, recursive=True)]
51
+
52
+ # Filter ignored files
53
+ files = [f for f in files if not any(fnmatch.fnmatch(f, p) for p in ignore_patterns)]
54
+
55
+ if not files:
56
+ print(f"ℹ️ No {ext_label} files found to validate")
57
+ sys.exit(0)
58
+
59
+ for file in files:
60
+ cmd = ["qsv", "validate", file]
61
+ if schema:
62
+ cmd.append(schema)
63
+ result = subprocess.run(cmd, capture_output=True)
64
+ if result.returncode != 0:
65
+ print(f"❌ Validation failed for: {file}")
66
+ subprocess.run(["just", "_csv-show-errors", file])
67
+ sys.exit(1)
68
+
69
+ print(f"✅ All {ext_label} files are valid")
70
+
71
+ # Show validation errors for a CSV/TSV file
72
+ [group("checks"), script("python3")]
73
+ _csv-show-errors file:
74
+ import os
75
+ import subprocess
76
+ import sys
77
+
78
+ file = "{{ file }}"
79
+ # qsv always produces .validation-errors.tsv regardless of input format
80
+ error_file = f"{file}.validation-errors.tsv"
81
+
82
+ if not os.path.exists(error_file):
83
+ print(f"Error file not found: {error_file}")
84
+ sys.exit(0)
85
+
86
+ # Count errors (qsv auto-detects delimiter based on file extension)
87
+ result = subprocess.run(["qsv", "count", error_file], capture_output=True, text=True)
88
+ try:
89
+ total = int(result.stdout.strip()) if result.returncode == 0 and result.stdout.strip() else 0
90
+ except ValueError:
91
+ total = 0
92
+
93
+ print()
94
+ if total > 0:
95
+ print(f"First 20 validation errors ({total} total):")
96
+ else:
97
+ print("Validation errors:")
98
+ print()
99
+
100
+ # Try qsv table for nice formatting, fall back to reading file directly
101
+ slice_result = subprocess.run(
102
+ ["qsv", "slice", "--start", "0", "--len", "21", error_file],
103
+ capture_output=True, text=True
104
+ )
105
+ if slice_result.returncode == 0:
106
+ table_result = subprocess.run(
107
+ ["qsv", "table"],
108
+ input=slice_result.stdout,
109
+ capture_output=True, text=True
110
+ )
111
+ if table_result.returncode == 0:
112
+ print(table_result.stdout)
113
+ else:
114
+ print(slice_result.stdout)
115
+ else:
116
+ with open(error_file) as f:
117
+ for i, line in enumerate(f):
118
+ if i >= 21:
119
+ break
120
+ print(line, end="")
121
+
122
+ if total > 20:
123
+ print()
124
+ print(f"... and {total - 20} more errors")
125
+ print()
126
+ print(f"Full details: {error_file}")
package/just/npm.just ADDED
@@ -0,0 +1,54 @@
1
+ import "./settings.just"
2
+
3
+ # ---------------------------------------------------------------------------- #
4
+ # DEPENDENCIES #
5
+ # ---------------------------------------------------------------------------- #
6
+
7
+ # Jq
8
+ jq := require("jq")
9
+
10
+ # Npm
11
+ npm := require("npm")
12
+
13
+ # ---------------------------------------------------------------------------- #
14
+ # SCRIPTS #
15
+ # ---------------------------------------------------------------------------- #
16
+
17
+ # Publish the npm package, e.g. v1.0.0
18
+ [group("publish")]
19
+ [arg("tag", long, value="true")]
20
+ publish tag="false" *args:
21
+ npm publish {{ args }}
22
+ @{{ if tag == "true" { "just tag $(jq -r '.version' package.json)" } else { "true" } }}
23
+
24
+ # Publish using the `beta` tag, e.g. v1.0.0-beta.1
25
+ [group("publish")]
26
+ publish-beta *args:
27
+ @just check-beta-version
28
+ npm publish --tag beta {{ args }}
29
+
30
+ # Tag the new version
31
+ [group("publish")]
32
+ tag *version:
33
+ git tag -am "{{ version }}" {{ version }}
34
+ git push origin --tags
35
+
36
+ # ---------------------------------------------------------------------------- #
37
+ # PRIVATE HELPERS #
38
+ # ---------------------------------------------------------------------------- #
39
+
40
+
41
+ # Check that package.json version includes -beta.x suffix
42
+ [private, script("bash")]
43
+ @check-beta-version:
44
+ # Extract version from package.json using jq for reliable JSON parsing
45
+ version=$(jq -r '.version' package.json)
46
+
47
+ # Check if version contains -beta suffix
48
+ if [[ "$version" =~ -beta\.[0-9]+$ ]]; then
49
+ echo "✓ Version $version includes beta suffix"
50
+ else
51
+ echo "✗ Error: Version $version does not include -beta.x suffix"
52
+ echo "Please update package.json version to include -beta.x (e.g., 1.0.0-beta.1)"
53
+ exit 1
54
+ fi
@@ -0,0 +1,6 @@
1
+ # See https://just.systems/man/en/settings.html
2
+ set allow-duplicate-recipes
3
+ set allow-duplicate-variables
4
+ set lists
5
+ set shell := ["bash", "-euo", "pipefail", "-c"]
6
+ set unstable
@@ -0,0 +1,15 @@
1
+ import "./settings.just"
2
+
3
+ # ---------------------------------------------------------------------------- #
4
+ # UTILITIES #
5
+ # ---------------------------------------------------------------------------- #
6
+
7
+ # Private recipe to run a check with formatted output
8
+ [no-cd]
9
+ @_run-with-status recipe *args:
10
+ echo ""
11
+ echo '{{ CYAN }}→ Running {{ recipe }}...{{ NORMAL }}'
12
+ just {{ recipe }} {{ args }}
13
+ echo '{{ GREEN }}✓ {{ recipe }} completed{{ NORMAL }}'
14
+ alias rws := _run-with-status
15
+ alias _rws := _run-with-status
@@ -0,0 +1,86 @@
1
+ import "./settings.just"
2
+
3
+ # ---------------------------------------------------------------------------- #
4
+ # DEPENDENCIES #
5
+ # ---------------------------------------------------------------------------- #
6
+
7
+ # Ni: https://github.com/antfu-collective/ni
8
+ na := require("na")
9
+
10
+ # ---------------------------------------------------------------------------- #
11
+ # CONSTANTS #
12
+ # ---------------------------------------------------------------------------- #
13
+
14
+ # Consumer MUST override with a JSON map of app name → Vercel project ID.
15
+ # Example: '{"portal": "prj_xxx", "landing": "prj_yyy"}'
16
+ # Note: if VERCEL_PROJECT_ID is already set in the environment, this map is ignored.
17
+ VERCEL_PROJECT_IDS := '{}'
18
+
19
+ # ---------------------------------------------------------------------------- #
20
+ # RECIPES #
21
+ # ---------------------------------------------------------------------------- #
22
+
23
+ # Build website for Vercel deployment
24
+ [arg("app", long)]
25
+ [arg("env", long)]
26
+ [group("vercel")]
27
+ build app env="staging":
28
+ @just _vercel-build "{{ app }}" "{{ env }}"
29
+ alias vb := build
30
+
31
+ # Deploy website to Vercel. Trailing args are forwarded to `vercel deploy`.
32
+ [arg("app", long)]
33
+ [arg("env", long)]
34
+ [arg("skip_build", long="skip-build", value="true")]
35
+ [confirm("Are you sure you want to deploy? [y/N]")]
36
+ [group("vercel")]
37
+ deploy app env="staging" skip_build="false" *args="":
38
+ @just _vercel-deploy "{{ app }}" "{{ env }}" "{{ skip_build }}" {{ args }}
39
+ alias vd := deploy
40
+
41
+ # ---------------------------------------------------------------------------- #
42
+ # PRIVATE HELPERS #
43
+ # ---------------------------------------------------------------------------- #
44
+
45
+ # Core build: vercel pull + vercel build
46
+ [private, script("python3")]
47
+ _vercel-build app env:
48
+ import sys; sys.path.insert(0, "{{ source_directory() }}")
49
+ import os
50
+ from _vercel_helpers import resolve_vercel_env, run
51
+
52
+ # Workaround for https://github.com/vercel/vercel/issues/14666
53
+ os.environ["VERCEL_TARGET_ENV"] = "{{ env }}"
54
+ os.environ["NEXT_PUBLIC_VERCEL_TARGET_ENV"] = "{{ env }}"
55
+
56
+ project_id, token = resolve_vercel_env("{{ app }}", '{{ VERCEL_PROJECT_IDS }}')
57
+
58
+ # Pull the environment from the Vercel project
59
+ run("na", "vercel", "pull", "--environment={{ env }}", f"--token={token}", "--yes")
60
+
61
+ # Build the project
62
+ run("na", "vercel", "build", "--target={{ env }}", f"--token={token}")
63
+
64
+ # Core deploy: optionally build, then deploy prebuilt artifacts
65
+ [private, script("python3")]
66
+ _vercel-deploy app env skip_build="false" *args="":
67
+ import shlex, sys; sys.path.insert(0, "{{ source_directory() }}")
68
+ import os
69
+ from _vercel_helpers import emit_deployment_url, resolve_vercel_env, run, run_capture_url
70
+
71
+ # Build the project if not skipped (calls the PUBLIC recipe so consumer overrides apply)
72
+ if "{{ skip_build }}" != "true":
73
+ run("just", "build", "--app={{ app }}", "--env={{ env }}")
74
+
75
+ # Workaround for https://github.com/vercel/vercel/issues/14666
76
+ os.environ["VERCEL_TARGET_ENV"] = "{{ env }}"
77
+ os.environ["NEXT_PUBLIC_VERCEL_TARGET_ENV"] = "{{ env }}"
78
+
79
+ project_id, token = resolve_vercel_env("{{ app }}", '{{ VERCEL_PROJECT_IDS }}')
80
+
81
+ # Deploy the project to Vercel, capturing the deployment URL from stdout
82
+ extra = shlex.split("""{{ args }}""")
83
+ url = run_capture_url(
84
+ "na", "vercel", "deploy", "--prebuilt", "--target={{ env }}", f"--token={token}", *extra
85
+ )
86
+ emit_deployment_url(url)
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@prb/devkit",
3
+ "description": "Personal configuration files and reusable scripts",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "version": "1.0.0",
7
+ "author": {
8
+ "name": "Paul Razvan Berg",
9
+ "url": "https://github.com/PaulRBerg"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/PaulRBerg/devkit/issues"
13
+ },
14
+ "devDependencies": {
15
+ "@biomejs/biome": "^2.5.0",
16
+ "prettier": "^3.8.3",
17
+ "vitest": "^3.2.4"
18
+ },
19
+ "peerDependencies": {
20
+ "vitest": ">=2.0.0"
21
+ },
22
+ "peerDependenciesMeta": {
23
+ "vitest": {
24
+ "optional": true
25
+ }
26
+ },
27
+ "exports": {
28
+ "./biome": "./biome/base.jsonc",
29
+ "./biome/base": "./biome/base.jsonc",
30
+ "./biome/ui": "./biome/ui.jsonc",
31
+ "./prettier": "./.prettierrc.js",
32
+ "./tsconfig/base": "./tsconfig/base.json",
33
+ "./tsconfig/build": "./tsconfig/build.json",
34
+ "./tsconfig/next": "./tsconfig/next.json",
35
+ "./vitest": "./vitest/base.js"
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "files": [
41
+ "biome/",
42
+ "just/",
43
+ "tsconfig/",
44
+ "vitest/",
45
+ ".prettierrc.js",
46
+ ".prettierrc.json"
47
+ ],
48
+ "keywords": [
49
+ "biome",
50
+ "config",
51
+ "devkit",
52
+ "prettier",
53
+ "tsconfig",
54
+ "typescript",
55
+ "vitest"
56
+ ],
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/PaulRBerg/devkit.git"
63
+ }
64
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "esModuleInterop": true,
5
+ "forceConsistentCasingInFileNames": true,
6
+ "lib": ["ESNext"],
7
+ "module": "NodeNext",
8
+ "moduleResolution": "NodeNext",
9
+ "noEmit": true,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "noImplicitReturns": true,
12
+ "resolveJsonModule": true,
13
+ "skipLibCheck": true,
14
+ "sourceMap": true,
15
+ "strict": true,
16
+ "target": "ESNext"
17
+ }
18
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "./base.json",
4
+ "compilerOptions": {
5
+ "declaration": true,
6
+ "inlineSources": true,
7
+ "declarationMap": true,
8
+ "emitDecoratorMetadata": true,
9
+ "experimentalDecorators": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "noEmit": false,
12
+ "removeComments": true,
13
+ "skipLibCheck": true,
14
+ "stripInternal": true
15
+ }
16
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "./base.json",
4
+ "compilerOptions": {
5
+ "allowJs": true,
6
+ "isolatedModules": true,
7
+ "incremental": true,
8
+ "jsx": "preserve",
9
+ "lib": ["dom", "dom.iterable", "ESNext"],
10
+ "module": "ESNext",
11
+ "moduleResolution": "bundler",
12
+ "plugins": [
13
+ {
14
+ "name": "next"
15
+ }
16
+ ]
17
+ }
18
+ }
package/vitest/base.js ADDED
@@ -0,0 +1,32 @@
1
+ import { defineConfig, mergeConfig } from "vitest/config";
2
+
3
+ /**
4
+ * @typedef {Object} DevkitVitestOptions
5
+ * @property {"node" | "jsdom" | "happy-dom"} [environment]
6
+ * @property {string[]} [setupFiles]
7
+ * @property {boolean} [coverage]
8
+ */
9
+
10
+ /**
11
+ * @param {DevkitVitestOptions} [options]
12
+ */
13
+ export function defineDevkitConfig(options = {}) {
14
+ const isCI = Boolean(process.env.CI);
15
+
16
+ const baseConfig = {
17
+ test: {
18
+ coverage: options.coverage ? { provider: "v8" } : undefined,
19
+ environment: options.environment ?? "node",
20
+ globals: true,
21
+ reporters: isCI ? ["basic"] : ["verbose"],
22
+ retry: isCI ? 2 : 0,
23
+ setupFiles: options.setupFiles,
24
+ testTimeout: isCI ? 30_000 : 10_000,
25
+ },
26
+ };
27
+
28
+ return defineConfig(baseConfig);
29
+ }
30
+
31
+ // Re-export for merging with existing vite configs
32
+ export { mergeConfig };