@rrlab/biome-plugin 0.0.1-git-06ed46c.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 ADDED
@@ -0,0 +1,40 @@
1
+ # @rrlab/biome-plugin
2
+
3
+ Biome plugin for [`@rrlab/cli`](https://npmjs.com/package/@rrlab/cli). Provides `lint`, `format`, and `jsc` capabilities backed by [Biome](https://biomejs.dev).
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ rr plugins add biome
9
+ ```
10
+
11
+ `rr plugins add` installs `@rrlab/biome-plugin`, adds `@biomejs/biome` as a `devDependency`, and (with your confirmation) scaffolds a `biome.json` extending [`@rrlab/biome-config`](https://npmjs.com/package/@rrlab/biome-config). When `biome.json` already exists you can choose to patch it, leave it alone, or overwrite it.
12
+
13
+ ## What it provides
14
+
15
+ | Capability | Surface |
16
+ |---|---|
17
+ | `lint` | `rr lint`, `rr lint doctor` |
18
+ | `format` | `rr format`, `rr format doctor` |
19
+ | `jsc` | `rr jsc` (lint + format together), `rr jsc doctor` |
20
+
21
+ ## Configuration
22
+
23
+ The scaffolded `biome.json` is a thin wrapper:
24
+
25
+ ```json
26
+ {
27
+ "$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
28
+ "extends": ["@rrlab/biome-config"]
29
+ }
30
+ ```
31
+
32
+ Override any biome setting by adding it to your local `biome.json`. The preset lives in [`@rrlab/biome-config`](https://npmjs.com/package/@rrlab/biome-config) — bump the package to get updated defaults.
33
+
34
+ ## Removal
35
+
36
+ ```sh
37
+ rr plugins remove biome
38
+ ```
39
+
40
+ Removes `@biomejs/biome` + `@rrlab/biome-config` from `package.json`, edits or deletes `biome.json` (depending on whether you have other settings in it), and drops the `biome()` entry from `run-run.config.{ts,mts}`.
@@ -0,0 +1,23 @@
1
+ import { FormatOptions, Formatter, InstallContext, InstallResult, LintOptions, Linter, StaticChecker, StaticCheckerOptions, ToolService, UninstallContext, UninstallResult } from "@rrlab/cli/plugin";
2
+ import { ShellService } from "@vlandoss/clibuddy";
3
+
4
+ //#region src/tool-versions.d.ts
5
+ declare const TOOL_VERSIONS: {
6
+ readonly "@biomejs/biome": {
7
+ readonly install: "^2.0.0";
8
+ readonly peer: ">=2.0.0";
9
+ };
10
+ };
11
+ //#endregion
12
+ //#region src/index.d.ts
13
+ declare class BiomeService extends ToolService implements Formatter, Linter, StaticChecker {
14
+ constructor(shellService: ShellService);
15
+ format(options: FormatOptions): Promise<void>;
16
+ lint(options: LintOptions): Promise<void>;
17
+ check(options: StaticCheckerOptions): Promise<void>;
18
+ }
19
+ declare function install(ctx: InstallContext): Promise<InstallResult>;
20
+ declare function uninstall(ctx: UninstallContext): Promise<UninstallResult>;
21
+ declare const biome: (options: void) => import("@rrlab/cli/plugin").Plugin;
22
+ //#endregion
23
+ export { BiomeService, TOOL_VERSIONS, biome as default, install, uninstall };
package/dist/index.mjs ADDED
@@ -0,0 +1,190 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ToolService, definePlugin } from "@rrlab/cli/plugin";
4
+ import { colorize, isCI } from "@vlandoss/clibuddy";
5
+ import { parse } from "comment-json";
6
+ //#region src/tool-versions.ts
7
+ const TOOL_VERSIONS = { "@biomejs/biome": {
8
+ install: "^2.0.0",
9
+ peer: ">=2.0.0"
10
+ } };
11
+ //#endregion
12
+ //#region src/index.ts
13
+ const FROM = import.meta.url;
14
+ const UI = colorize("#61A5FA")("biome");
15
+ const COMMON_FLAGS = ["--colors=force", "--no-errors-on-unmatched"];
16
+ const BIOME_JSON = "biome.json";
17
+ const BIOME_CONFIG_PKG = "@rrlab/biome-config";
18
+ const BIOME_SCHEMA = "https://biomejs.dev/schemas/2.4.4/schema.json";
19
+ var BiomeService = class extends ToolService {
20
+ constructor(shellService) {
21
+ super({
22
+ pkg: "@biomejs/biome",
23
+ bin: "biome",
24
+ ui: UI,
25
+ shellService,
26
+ from: FROM
27
+ });
28
+ }
29
+ async format(options) {
30
+ const args = ["format", ...COMMON_FLAGS];
31
+ if (options.fix) args.push("--fix");
32
+ await this.exec(args);
33
+ }
34
+ async lint(options) {
35
+ const args = [
36
+ "check",
37
+ ...COMMON_FLAGS,
38
+ "--formatter-enabled=false"
39
+ ];
40
+ if (options.fix) args.push("--fix", "--unsafe");
41
+ await this.exec(args);
42
+ }
43
+ async check(options) {
44
+ if (options.fix) await this.exec([
45
+ "check",
46
+ ...COMMON_FLAGS,
47
+ "--fix"
48
+ ]);
49
+ else if (options.fixStaged) await this.exec([
50
+ "check",
51
+ ...COMMON_FLAGS,
52
+ "--fix",
53
+ "--staged"
54
+ ]);
55
+ else await this.exec([isCI ? "ci" : "check", ...COMMON_FLAGS]);
56
+ }
57
+ };
58
+ async function install(ctx) {
59
+ const scaffoldDecision = await decideScaffoldAction(ctx, await pathExists(path.join(ctx.appPkg.dirPath, BIOME_JSON)));
60
+ if (scaffoldDecision === "skip") return { devDependencies: { "@biomejs/biome": TOOL_VERSIONS["@biomejs/biome"].install } };
61
+ return {
62
+ devDependencies: {
63
+ "@biomejs/biome": TOOL_VERSIONS["@biomejs/biome"].install,
64
+ [BIOME_CONFIG_PKG]: "^0.1.0"
65
+ },
66
+ files: [scaffoldDecision === "create" || scaffoldDecision === "overwrite" ? {
67
+ kind: "create",
68
+ path: BIOME_JSON,
69
+ content: `${JSON.stringify({
70
+ $schema: BIOME_SCHEMA,
71
+ extends: [BIOME_CONFIG_PKG]
72
+ }, null, 2)}\n`,
73
+ overwrite: scaffoldDecision === "overwrite" || ctx.flags.force
74
+ } : {
75
+ kind: "edit-json",
76
+ path: BIOME_JSON,
77
+ edits: [{
78
+ op: "set",
79
+ path: "/$schema",
80
+ value: BIOME_SCHEMA,
81
+ mode: "if-missing"
82
+ }, {
83
+ op: "include",
84
+ path: "/extends",
85
+ value: BIOME_CONFIG_PKG,
86
+ position: "start"
87
+ }]
88
+ }]
89
+ };
90
+ }
91
+ async function uninstall(ctx) {
92
+ const biomeJsonPath = path.join(ctx.appPkg.dirPath, BIOME_JSON);
93
+ const removeDependencies = ["@biomejs/biome", BIOME_CONFIG_PKG];
94
+ if (!await pathExists(biomeJsonPath)) return { removeDependencies };
95
+ let existing;
96
+ try {
97
+ existing = parse(await fs.readFile(biomeJsonPath, "utf8"));
98
+ } catch {}
99
+ const files = [];
100
+ if (existing) {
101
+ const otherExtends = (Array.isArray(existing.extends) ? existing.extends : []).filter((e) => e !== BIOME_CONFIG_PKG);
102
+ const { $schema: _schema, extends: _extends, ...rest } = existing;
103
+ const semanticKeys = Object.keys(rest);
104
+ if (otherExtends.length === 0 && semanticKeys.length === 0) files.push({
105
+ kind: "delete",
106
+ path: BIOME_JSON
107
+ });
108
+ else {
109
+ const edits = [{
110
+ op: "exclude",
111
+ path: "/extends",
112
+ value: BIOME_CONFIG_PKG
113
+ }];
114
+ if (otherExtends.length === 0) edits.push({
115
+ op: "unset",
116
+ path: "/extends"
117
+ });
118
+ files.push({
119
+ kind: "edit-json",
120
+ path: BIOME_JSON,
121
+ edits
122
+ });
123
+ }
124
+ }
125
+ return {
126
+ removeDependencies,
127
+ files
128
+ };
129
+ }
130
+ async function decideScaffoldAction(ctx, fileExists) {
131
+ if (!fileExists) {
132
+ if (ctx.flags.yes || ctx.flags.nonInteractive) return "create";
133
+ const choice = await ctx.prompts.confirm({
134
+ message: `Scaffold ${BIOME_JSON} with the @rrlab/biome-config preset?`,
135
+ initialValue: true
136
+ });
137
+ if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
138
+ return choice ? "create" : "skip";
139
+ }
140
+ if (ctx.flags.yes || ctx.flags.nonInteractive) return "patch";
141
+ const choice = await ctx.prompts.select({
142
+ message: `${BIOME_JSON} already exists. What do you want to do?`,
143
+ options: [
144
+ {
145
+ value: "patch",
146
+ label: "Patch — add @rrlab/biome-config to extends, keep my other settings"
147
+ },
148
+ {
149
+ value: "skip",
150
+ label: "Skip — leave it alone"
151
+ },
152
+ {
153
+ value: "overwrite",
154
+ label: "Overwrite — replace with a fresh scaffold"
155
+ }
156
+ ],
157
+ initialValue: "patch"
158
+ });
159
+ if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
160
+ return choice;
161
+ }
162
+ async function pathExists(p) {
163
+ try {
164
+ await fs.access(p);
165
+ return true;
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+ const biome = definePlugin(() => ({
171
+ name: "biome",
172
+ apiVersion: 1,
173
+ install,
174
+ uninstall,
175
+ async setup({ shell }) {
176
+ const svc = new BiomeService(shell);
177
+ try {
178
+ await svc.getBinDir();
179
+ } catch (_err) {
180
+ throw new Error("@rrlab/biome-plugin requires @biomejs/biome to be installed in the host project. Run: rr plugins add biome (or: pnpm add -D @biomejs/biome)");
181
+ }
182
+ return {
183
+ lint: svc,
184
+ format: svc,
185
+ jsc: svc
186
+ };
187
+ }
188
+ }));
189
+ //#endregion
190
+ export { BiomeService, TOOL_VERSIONS, biome as default, install, uninstall };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@rrlab/biome-plugin",
3
+ "version": "0.0.1-git-06ed46c.0",
4
+ "description": "Biome plugin for @rrlab/cli — provides lint, format, and jsc capabilities.",
5
+ "homepage": "https://github.com/variableland/dx/tree/main/run-run/biome-plugin#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/variableland/dx/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/variableland/dx.git",
12
+ "directory": "run-run/biome-plugin"
13
+ },
14
+ "license": "MIT",
15
+ "author": "rcrd <rcrd@variable.land>",
16
+ "type": "module",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.mts",
20
+ "default": "./dist/index.mjs"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src",
26
+ "!src/**/__tests__",
27
+ "!src/**/*.test.*"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "engines": {
33
+ "node": ">=20.0.0"
34
+ },
35
+ "dependencies": {
36
+ "comment-json": "4.2.5",
37
+ "@vlandoss/clibuddy": "0.6.1"
38
+ },
39
+ "peerDependencies": {
40
+ "@biomejs/biome": ">=2.0.0",
41
+ "@rrlab/cli": "0.0.2-git-06ed46c.0"
42
+ },
43
+ "devDependencies": {
44
+ "@biomejs/biome": "2.4.4",
45
+ "@rrlab/cli": "0.0.2-git-06ed46c.0",
46
+ "@rrlab/tsdown-config": "^0.0.1-git-06ed46c.0"
47
+ },
48
+ "scripts": {
49
+ "build": "tsdown",
50
+ "test": "vitest run"
51
+ }
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,192 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ definePlugin,
5
+ type FileOp,
6
+ type FormatOptions,
7
+ type Formatter,
8
+ type InstallContext,
9
+ type InstallResult,
10
+ type Linter,
11
+ type LintOptions,
12
+ type StaticChecker,
13
+ type StaticCheckerOptions,
14
+ ToolService,
15
+ type UninstallContext,
16
+ type UninstallResult,
17
+ } from "@rrlab/cli/plugin";
18
+ import { colorize, isCI, type ShellService } from "@vlandoss/clibuddy";
19
+ import { parse as parseJsonc } from "comment-json";
20
+ import { TOOL_VERSIONS } from "./tool-versions.ts";
21
+
22
+ const FROM = import.meta.url;
23
+ const UI = colorize("#61A5FA")("biome");
24
+ const COMMON_FLAGS = ["--colors=force", "--no-errors-on-unmatched"];
25
+ const BIOME_JSON = "biome.json";
26
+ const BIOME_CONFIG_PKG = "@rrlab/biome-config";
27
+ const BIOME_SCHEMA = "https://biomejs.dev/schemas/2.4.4/schema.json";
28
+
29
+ export { TOOL_VERSIONS } from "./tool-versions.ts";
30
+
31
+ export class BiomeService extends ToolService implements Formatter, Linter, StaticChecker {
32
+ constructor(shellService: ShellService) {
33
+ super({ pkg: "@biomejs/biome", bin: "biome", ui: UI, shellService, from: FROM });
34
+ }
35
+
36
+ async format(options: FormatOptions) {
37
+ const args = ["format", ...COMMON_FLAGS];
38
+ if (options.fix) args.push("--fix");
39
+ await this.exec(args);
40
+ }
41
+
42
+ async lint(options: LintOptions) {
43
+ const args = ["check", ...COMMON_FLAGS, "--formatter-enabled=false"];
44
+ if (options.fix) args.push("--fix", "--unsafe");
45
+ await this.exec(args);
46
+ }
47
+
48
+ async check(options: StaticCheckerOptions): Promise<void> {
49
+ if (options.fix) {
50
+ await this.exec(["check", ...COMMON_FLAGS, "--fix"]);
51
+ } else if (options.fixStaged) {
52
+ await this.exec(["check", ...COMMON_FLAGS, "--fix", "--staged"]);
53
+ } else {
54
+ await this.exec([isCI ? "ci" : "check", ...COMMON_FLAGS]);
55
+ }
56
+ }
57
+ }
58
+
59
+ export async function install(ctx: InstallContext): Promise<InstallResult> {
60
+ const biomeJsonPath = path.join(ctx.appPkg.dirPath, BIOME_JSON);
61
+ const fileExists = await pathExists(biomeJsonPath);
62
+ const scaffoldDecision = await decideScaffoldAction(ctx, fileExists);
63
+
64
+ if (scaffoldDecision === "skip") {
65
+ return { devDependencies: { "@biomejs/biome": TOOL_VERSIONS["@biomejs/biome"].install } };
66
+ }
67
+
68
+ const devDependencies: Record<string, string> = {
69
+ "@biomejs/biome": TOOL_VERSIONS["@biomejs/biome"].install,
70
+ [BIOME_CONFIG_PKG]: "^0.1.0",
71
+ };
72
+
73
+ const file: FileOp =
74
+ scaffoldDecision === "create" || scaffoldDecision === "overwrite"
75
+ ? {
76
+ kind: "create",
77
+ path: BIOME_JSON,
78
+ content: `${JSON.stringify({ $schema: BIOME_SCHEMA, extends: [BIOME_CONFIG_PKG] }, null, 2)}\n`,
79
+ overwrite: scaffoldDecision === "overwrite" || ctx.flags.force,
80
+ }
81
+ : {
82
+ kind: "edit-json",
83
+ path: BIOME_JSON,
84
+ edits: [
85
+ { op: "set", path: "/$schema", value: BIOME_SCHEMA, mode: "if-missing" },
86
+ { op: "include", path: "/extends", value: BIOME_CONFIG_PKG, position: "start" },
87
+ ],
88
+ };
89
+
90
+ return { devDependencies, files: [file] };
91
+ }
92
+
93
+ export async function uninstall(ctx: UninstallContext): Promise<UninstallResult> {
94
+ const biomeJsonPath = path.join(ctx.appPkg.dirPath, BIOME_JSON);
95
+ const removeDependencies = ["@biomejs/biome", BIOME_CONFIG_PKG];
96
+
97
+ if (!(await pathExists(biomeJsonPath))) {
98
+ return { removeDependencies };
99
+ }
100
+
101
+ let existing: Record<string, unknown> | undefined;
102
+ try {
103
+ const text = await fs.readFile(biomeJsonPath, "utf8");
104
+ existing = parseJsonc(text) as Record<string, unknown>;
105
+ } catch {
106
+ /* malformed — skip surgical edits */
107
+ }
108
+
109
+ const files: FileOp[] = [];
110
+ if (existing) {
111
+ const extendsArr = Array.isArray(existing.extends) ? existing.extends : [];
112
+ const otherExtends = extendsArr.filter((e) => e !== BIOME_CONFIG_PKG);
113
+ const { $schema: _schema, extends: _extends, ...rest } = existing;
114
+ const semanticKeys = Object.keys(rest);
115
+ if (otherExtends.length === 0 && semanticKeys.length === 0) {
116
+ files.push({ kind: "delete", path: BIOME_JSON });
117
+ } else {
118
+ const edits = [{ op: "exclude" as const, path: "/extends", value: BIOME_CONFIG_PKG }];
119
+ if (otherExtends.length === 0) {
120
+ edits.push({ op: "unset" as const, path: "/extends" } as never);
121
+ }
122
+ files.push({ kind: "edit-json", path: BIOME_JSON, edits });
123
+ }
124
+ }
125
+
126
+ return { removeDependencies, files };
127
+ }
128
+
129
+ type ExistingFileAction = "skip" | "patch" | "overwrite";
130
+
131
+ async function decideScaffoldAction(
132
+ ctx: InstallContext,
133
+ fileExists: boolean,
134
+ ): Promise<"create" | "patch" | "overwrite" | "skip"> {
135
+ if (!fileExists) {
136
+ if (ctx.flags.yes || ctx.flags.nonInteractive) return "create";
137
+ const choice = await ctx.prompts.confirm({
138
+ message: `Scaffold ${BIOME_JSON} with the @rrlab/biome-config preset?`,
139
+ initialValue: true,
140
+ });
141
+ if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
142
+ return choice ? "create" : "skip";
143
+ }
144
+
145
+ if (ctx.flags.yes || ctx.flags.nonInteractive) return "patch";
146
+
147
+ const choice = await ctx.prompts.select<ExistingFileAction>({
148
+ message: `${BIOME_JSON} already exists. What do you want to do?`,
149
+ options: [
150
+ { value: "patch", label: "Patch — add @rrlab/biome-config to extends, keep my other settings" },
151
+ { value: "skip", label: "Skip — leave it alone" },
152
+ { value: "overwrite", label: "Overwrite — replace with a fresh scaffold" },
153
+ ],
154
+ initialValue: "patch",
155
+ });
156
+ if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
157
+ return choice;
158
+ }
159
+
160
+ async function pathExists(p: string): Promise<boolean> {
161
+ try {
162
+ await fs.access(p);
163
+ return true;
164
+ } catch {
165
+ return false;
166
+ }
167
+ }
168
+
169
+ const biome = definePlugin<void>(() => ({
170
+ name: "biome",
171
+ apiVersion: 1,
172
+ install,
173
+ uninstall,
174
+ async setup({ shell }) {
175
+ const svc = new BiomeService(shell);
176
+ try {
177
+ await svc.getBinDir();
178
+ } catch (_err) {
179
+ throw new Error(
180
+ "@rrlab/biome-plugin requires @biomejs/biome to be installed in the host project. " +
181
+ "Run: rr plugins add biome (or: pnpm add -D @biomejs/biome)",
182
+ );
183
+ }
184
+ return {
185
+ lint: svc,
186
+ format: svc,
187
+ jsc: svc,
188
+ };
189
+ },
190
+ }));
191
+
192
+ export default biome;
@@ -0,0 +1,3 @@
1
+ export const TOOL_VERSIONS = {
2
+ "@biomejs/biome": { install: "^2.0.0", peer: ">=2.0.0" },
3
+ } as const;