@rrlab/cli 1.0.1-git-908d2c0.0 → 1.1.1-git-4903a88.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/bin +3 -5
- package/dist/cli.usage.kdl +26 -25
- package/dist/config.d.mts +1 -1
- package/dist/magic-string.es-BgIV5Mu3.mjs +1011 -0
- package/dist/plugin/__tests__/bin-probe.test.d.mts +1 -0
- package/dist/plugin/__tests__/bin-probe.test.mjs +64 -0
- package/dist/plugin/__tests__/decide-scaffold.test.d.mts +1 -0
- package/dist/plugin/__tests__/decide-scaffold.test.mjs +103 -0
- package/dist/plugin/__tests__/define-plugin.test.d.mts +1 -0
- package/dist/plugin/__tests__/define-plugin.test.mjs +130 -0
- package/dist/plugin/__tests__/pick-preset.test.d.mts +1 -0
- package/dist/plugin/__tests__/pick-preset.test.mjs +72 -0
- package/dist/plugin/__tests__/registry.test.d.mts +1 -0
- package/dist/plugin/__tests__/registry.test.mjs +104 -0
- package/dist/plugin/bin-probe.d.mts +4 -0
- package/dist/plugin/bin-probe.mjs +22 -0
- package/dist/plugin/decide-scaffold.d.mts +18 -0
- package/dist/plugin/decide-scaffold.mjs +36 -0
- package/dist/plugin/define-plugin.d.mts +17 -0
- package/dist/plugin/define-plugin.mjs +25 -0
- package/dist/plugin/directory.d.mts +47 -0
- package/dist/plugin/directory.mjs +45 -0
- package/dist/plugin/errors.d.mts +11 -0
- package/dist/plugin/errors.mjs +15 -0
- package/dist/plugin/index.d.mts +7 -0
- package/dist/plugin/index.mjs +50 -0
- package/dist/plugin/pick-preset.d.mts +13 -0
- package/dist/plugin/pick-preset.mjs +17 -0
- package/dist/plugin/registry.d.mts +19 -0
- package/dist/plugin/registry.mjs +2 -0
- package/dist/plugin/tool-service.d.mts +45 -0
- package/dist/plugin/tool-service.mjs +64 -0
- package/dist/plugin/types.d.mts +3 -0
- package/dist/plugin/types.mjs +1 -0
- package/dist/registry-BgqfKK5L.mjs +55 -0
- package/dist/run.mjs +969 -585
- package/dist/test.DNmyFkvJ-09ScyH13.mjs +13617 -0
- package/dist/tool-DKL6TauZ.d.mts +43 -0
- package/dist/{types-snfbujDH.d.mts → types-Iu4IyWof.d.mts} +11 -75
- package/package.json +7 -6
- package/src/actions/clean.ts +36 -0
- package/src/actions/config.ts +46 -0
- package/src/actions/doctor.ts +47 -0
- package/src/actions/format.ts +13 -0
- package/src/actions/jsc.ts +13 -0
- package/src/actions/lint.ts +13 -0
- package/src/actions/pack.ts +12 -0
- package/src/actions/plugins/add.ts +143 -0
- package/src/actions/plugins/list.ts +27 -0
- package/src/actions/plugins/remove.ts +110 -0
- package/src/actions/plugins/shared.ts +58 -0
- package/src/actions/run-tool.ts +23 -0
- package/src/actions/tsc.ts +65 -0
- package/src/errors/invalid-plugin-module.ts +6 -0
- package/src/errors/missing-plugin.ts +17 -0
- package/src/errors/plugin-api-version.ts +6 -0
- package/src/errors/unknown-plugin.ts +7 -0
- package/src/lib/plugin/define-plugin.ts +56 -0
- package/src/lib/plugin/directory.ts +30 -0
- package/src/lib/plugin/errors.ts +15 -0
- package/src/lib/{plugin.ts → plugin/index.ts} +8 -9
- package/src/lib/plugin/registry.ts +82 -0
- package/src/{plugin → lib/plugin}/tool-service.ts +10 -14
- package/src/{plugin → lib/plugin}/types.ts +10 -33
- package/src/program/base.ts +75 -0
- package/src/program/commands/check.ts +31 -62
- package/src/program/commands/clean.ts +12 -43
- package/src/program/commands/completion.ts +6 -4
- package/src/program/commands/config.ts +6 -11
- package/src/program/commands/doctor.ts +5 -54
- package/src/program/commands/format.ts +18 -25
- package/src/program/commands/jscheck.ts +18 -31
- package/src/program/commands/lint.ts +18 -26
- package/src/program/commands/pack.ts +18 -22
- package/src/program/commands/plugins.ts +17 -364
- package/src/program/commands/tscheck.ts +19 -77
- package/src/program/index.ts +20 -27
- package/src/program/root.ts +62 -0
- package/src/render/banner.ts +25 -0
- package/src/render/board.ts +41 -0
- package/src/render/footer.ts +31 -0
- package/src/render/labels.ts +28 -0
- package/src/render/lines.ts +100 -0
- package/src/render/plugin-view.ts +68 -0
- package/src/render/steps.ts +20 -0
- package/src/run.ts +2 -8
- package/src/services/config.ts +4 -0
- package/src/services/context.ts +84 -0
- package/src/services/file-ops.ts +79 -0
- package/src/services/json-edit.ts +1 -1
- package/src/services/plugin-meta.ts +63 -0
- package/src/services/plugin-services.ts +41 -0
- package/src/services/prompts.ts +1 -1
- package/src/services/static-checker.ts +46 -0
- package/src/types/config.ts +2 -1
- package/src/types/tool.ts +13 -26
- package/src/ui/theme.ts +5 -0
- package/dist/plugin.d.mts +0 -87
- package/dist/plugin.mjs +0 -214
- package/src/plugin/define-plugin.ts +0 -54
- package/src/plugin/registry.ts +0 -48
- package/src/program/board.ts +0 -86
- package/src/program/composed-jsc.ts +0 -43
- package/src/program/missing-plugin.ts +0 -18
- package/src/program/ui.ts +0 -59
- package/src/services/ctx.ts +0 -71
- package/src/services/plugins-registry.ts +0 -22
- /package/src/{plugin → lib/plugin}/bin-probe.ts +0 -0
- /package/src/{plugin → lib/plugin}/decide-scaffold.ts +0 -0
- /package/src/{plugin → lib/plugin}/pick-preset.ts +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ToolService } from "../tool-service.mjs";
|
|
2
|
+
import { probeBins } from "../bin-probe.mjs";
|
|
3
|
+
import { i as it, r as describe, t as globalExpect } from "../../test.DNmyFkvJ-09ScyH13.mjs";
|
|
4
|
+
//#region src/lib/plugin/__tests__/bin-probe.test.ts
|
|
5
|
+
const FROM = import.meta.url;
|
|
6
|
+
var ResolvableService = class extends ToolService {
|
|
7
|
+
constructor() {
|
|
8
|
+
super({
|
|
9
|
+
pkg: "typescript",
|
|
10
|
+
bin: "tsc",
|
|
11
|
+
color: (s) => s,
|
|
12
|
+
shellService: {},
|
|
13
|
+
from: FROM
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var MissingService = class extends ToolService {
|
|
18
|
+
constructor() {
|
|
19
|
+
super({
|
|
20
|
+
bin: "ghostly-bin-that-does-not-exist",
|
|
21
|
+
color: (s) => s,
|
|
22
|
+
shellService: {},
|
|
23
|
+
from: FROM
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var AlsoMissing = class extends ToolService {
|
|
28
|
+
constructor() {
|
|
29
|
+
super({
|
|
30
|
+
bin: "another-ghost-bin",
|
|
31
|
+
color: (s) => s,
|
|
32
|
+
shellService: {},
|
|
33
|
+
from: FROM
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
describe("probeBins", () => {
|
|
38
|
+
it("is a no-op when given an empty list", async () => {
|
|
39
|
+
await globalExpect(probeBins([], "noop")).resolves.toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
it("ignores non-ToolService values silently", async () => {
|
|
42
|
+
await globalExpect(probeBins([
|
|
43
|
+
{ random: "object" },
|
|
44
|
+
42,
|
|
45
|
+
null
|
|
46
|
+
], "noop")).resolves.toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
it("succeeds when every distinct pkg resolves", async () => {
|
|
49
|
+
await globalExpect(probeBins([new ResolvableService(), new ResolvableService()], "ts")).resolves.toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
it("throws with the canonical message listing the missing pkg", async () => {
|
|
52
|
+
await globalExpect(probeBins([new MissingService()], "ghostly")).rejects.toThrow(/@rrlab\/ghostly-plugin requires ghostly-bin-that-does-not-exist to be installed in the host project\. Run: rr plugins add ghostly {2}\(or: pnpm add -D ghostly-bin-that-does-not-exist\)/);
|
|
53
|
+
});
|
|
54
|
+
it("lists multiple distinct missing pkgs in the same error", async () => {
|
|
55
|
+
await globalExpect(probeBins([new MissingService(), new AlsoMissing()], "ghostly")).rejects.toThrow(/requires .*(ghostly-bin-that-does-not-exist|another-ghost-bin).*(another-ghost-bin|ghostly-bin-that-does-not-exist)/);
|
|
56
|
+
});
|
|
57
|
+
it("deduplicates services sharing a pkg into a single probe", async () => {
|
|
58
|
+
const err = await probeBins([new MissingService(), new MissingService()], "ghostly").catch((e) => e);
|
|
59
|
+
globalExpect(err).toBeInstanceOf(Error);
|
|
60
|
+
globalExpect(err.message.match(/ghostly-bin-that-does-not-exist/g)?.length).toBe(2);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
//#endregion
|
|
64
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { decideScaffold } from "../decide-scaffold.mjs";
|
|
2
|
+
import { i as it, n as vi, r as describe, t as globalExpect } from "../../test.DNmyFkvJ-09ScyH13.mjs";
|
|
3
|
+
//#region src/lib/plugin/__tests__/decide-scaffold.test.ts
|
|
4
|
+
function makeCtx(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
shell: {},
|
|
7
|
+
logger: {},
|
|
8
|
+
appPkg: { dirPath: process.cwd() },
|
|
9
|
+
prompts: overrides.prompts ?? {},
|
|
10
|
+
flags: {
|
|
11
|
+
force: false,
|
|
12
|
+
yes: false,
|
|
13
|
+
nonInteractive: false,
|
|
14
|
+
...overrides.flags
|
|
15
|
+
},
|
|
16
|
+
release: {}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
describe("decideScaffold", () => {
|
|
20
|
+
describe("unattended (--yes or non-interactive)", () => {
|
|
21
|
+
it("returns 'create' when file does not exist", async () => {
|
|
22
|
+
globalExpect(await decideScaffold(makeCtx({ flags: { yes: true } }), {
|
|
23
|
+
label: "biome.json",
|
|
24
|
+
fileExists: false,
|
|
25
|
+
patchHint: "x"
|
|
26
|
+
})).toBe("create");
|
|
27
|
+
});
|
|
28
|
+
it("returns 'patch' when file exists and default unattendedExistingAction", async () => {
|
|
29
|
+
globalExpect(await decideScaffold(makeCtx({ flags: { yes: true } }), {
|
|
30
|
+
label: "biome.json",
|
|
31
|
+
fileExists: true,
|
|
32
|
+
patchHint: "x"
|
|
33
|
+
})).toBe("patch");
|
|
34
|
+
});
|
|
35
|
+
it("returns 'skip' when file exists and unattendedExistingAction: 'skip'", async () => {
|
|
36
|
+
globalExpect(await decideScaffold(makeCtx({ flags: { nonInteractive: true } }), {
|
|
37
|
+
label: "tsdown.config.ts",
|
|
38
|
+
fileExists: true,
|
|
39
|
+
patchHint: "x",
|
|
40
|
+
unattendedExistingAction: "skip"
|
|
41
|
+
})).toBe("skip");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("interactive", () => {
|
|
45
|
+
it("confirms creation when file does not exist", async () => {
|
|
46
|
+
const confirm = vi.fn().mockResolvedValue(true);
|
|
47
|
+
globalExpect(await decideScaffold(makeCtx({ prompts: {
|
|
48
|
+
confirm,
|
|
49
|
+
select: vi.fn(),
|
|
50
|
+
isCancel: () => false
|
|
51
|
+
} }), {
|
|
52
|
+
label: "biome.json",
|
|
53
|
+
fileExists: false,
|
|
54
|
+
patchHint: "x"
|
|
55
|
+
})).toBe("create");
|
|
56
|
+
globalExpect(confirm).toHaveBeenCalledWith(globalExpect.objectContaining({ message: globalExpect.stringContaining("biome.json") }));
|
|
57
|
+
});
|
|
58
|
+
it("returns 'skip' when user declines the create confirm", async () => {
|
|
59
|
+
globalExpect(await decideScaffold(makeCtx({ prompts: {
|
|
60
|
+
confirm: vi.fn().mockResolvedValue(false),
|
|
61
|
+
select: vi.fn(),
|
|
62
|
+
isCancel: () => false
|
|
63
|
+
} }), {
|
|
64
|
+
label: "biome.json",
|
|
65
|
+
fileExists: false,
|
|
66
|
+
patchHint: "x"
|
|
67
|
+
})).toBe("skip");
|
|
68
|
+
});
|
|
69
|
+
it("shows the patch/skip/overwrite select when file exists", async () => {
|
|
70
|
+
const select = vi.fn().mockResolvedValue("overwrite");
|
|
71
|
+
globalExpect(await decideScaffold(makeCtx({ prompts: {
|
|
72
|
+
confirm: vi.fn(),
|
|
73
|
+
select,
|
|
74
|
+
isCancel: () => false
|
|
75
|
+
} }), {
|
|
76
|
+
label: "biome.json",
|
|
77
|
+
fileExists: true,
|
|
78
|
+
patchHint: "add @rrlab/biome-config to extends"
|
|
79
|
+
})).toBe("overwrite");
|
|
80
|
+
const call = select.mock.calls[0]?.[0];
|
|
81
|
+
globalExpect(call.options.map((o) => o.value).sort()).toEqual([
|
|
82
|
+
"overwrite",
|
|
83
|
+
"patch",
|
|
84
|
+
"skip"
|
|
85
|
+
]);
|
|
86
|
+
globalExpect(call.options.find((o) => o.value === "patch")?.label).toMatch(/add @rrlab\/biome-config to extends/);
|
|
87
|
+
});
|
|
88
|
+
it("throws 'Cancelled by user.' when the user cancels", async () => {
|
|
89
|
+
const cancelSymbol = Symbol("cancel");
|
|
90
|
+
await globalExpect(decideScaffold(makeCtx({ prompts: {
|
|
91
|
+
confirm: vi.fn().mockResolvedValue(cancelSymbol),
|
|
92
|
+
select: vi.fn(),
|
|
93
|
+
isCancel: (v) => v === cancelSymbol
|
|
94
|
+
} }), {
|
|
95
|
+
label: "x",
|
|
96
|
+
fileExists: false,
|
|
97
|
+
patchHint: "y"
|
|
98
|
+
})).rejects.toThrow("Cancelled by user.");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
//#endregion
|
|
103
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { ToolService } from "../tool-service.mjs";
|
|
2
|
+
import { definePlugin } from "../define-plugin.mjs";
|
|
3
|
+
import { i as it, r as describe, t as globalExpect } from "../../test.DNmyFkvJ-09ScyH13.mjs";
|
|
4
|
+
//#region src/lib/plugin/__tests__/define-plugin.test.ts
|
|
5
|
+
function ctx() {
|
|
6
|
+
return {
|
|
7
|
+
shell: {},
|
|
8
|
+
logger: {},
|
|
9
|
+
appPkg: { dirPath: process.cwd() },
|
|
10
|
+
binPkg: { dirPath: process.cwd() },
|
|
11
|
+
cwd: process.cwd()
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const FROM = import.meta.url;
|
|
15
|
+
const emptyReport = {
|
|
16
|
+
ok: true,
|
|
17
|
+
output: ""
|
|
18
|
+
};
|
|
19
|
+
var FakeBiome = class extends ToolService {
|
|
20
|
+
constructor() {
|
|
21
|
+
super({
|
|
22
|
+
pkg: "@biomejs/biome",
|
|
23
|
+
bin: "biome",
|
|
24
|
+
color: (s) => s,
|
|
25
|
+
shellService: {},
|
|
26
|
+
from: FROM
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async lint() {
|
|
30
|
+
return emptyReport;
|
|
31
|
+
}
|
|
32
|
+
async format() {
|
|
33
|
+
return emptyReport;
|
|
34
|
+
}
|
|
35
|
+
async check() {
|
|
36
|
+
return emptyReport;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var FakeTsc = class extends ToolService {
|
|
40
|
+
constructor() {
|
|
41
|
+
super({
|
|
42
|
+
pkg: "typescript",
|
|
43
|
+
bin: "tsc",
|
|
44
|
+
color: (s) => s,
|
|
45
|
+
shellService: {},
|
|
46
|
+
from: FROM
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async check() {
|
|
50
|
+
return emptyReport;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var MissingService = class extends ToolService {
|
|
54
|
+
constructor() {
|
|
55
|
+
super({
|
|
56
|
+
bin: "ghostly-bin-that-does-not-exist",
|
|
57
|
+
color: (s) => s,
|
|
58
|
+
shellService: {},
|
|
59
|
+
from: FROM
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async lint() {
|
|
63
|
+
return emptyReport;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
describe("definePlugin", () => {
|
|
67
|
+
it("returns a Plugin shape the registry expects", async () => {
|
|
68
|
+
const plugin = definePlugin({
|
|
69
|
+
name: "fake-linter",
|
|
70
|
+
apiVersion: 1,
|
|
71
|
+
color: (s) => s,
|
|
72
|
+
services: () => ({ lint: new FakeBiome() })
|
|
73
|
+
})();
|
|
74
|
+
globalExpect(plugin.name).toBe("fake-linter");
|
|
75
|
+
globalExpect(plugin.apiVersion).toBe(1);
|
|
76
|
+
globalExpect(typeof plugin.services).toBe("function");
|
|
77
|
+
const caps = await plugin.services(ctx());
|
|
78
|
+
globalExpect(Object.keys(caps)).toEqual(["lint"]);
|
|
79
|
+
});
|
|
80
|
+
it("filters capabilities by 'only'", async () => {
|
|
81
|
+
const caps = await definePlugin({
|
|
82
|
+
name: "biome",
|
|
83
|
+
apiVersion: 1,
|
|
84
|
+
color: (s) => s,
|
|
85
|
+
services: () => {
|
|
86
|
+
const svc = new FakeBiome();
|
|
87
|
+
return {
|
|
88
|
+
lint: svc,
|
|
89
|
+
format: svc,
|
|
90
|
+
jscheck: svc
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
})({ only: ["lint", "format"] }).services(ctx());
|
|
94
|
+
globalExpect(Object.keys(caps).sort()).toEqual(["format", "lint"]);
|
|
95
|
+
});
|
|
96
|
+
it("throws on unknown kind in 'only' with the canonical message", async () => {
|
|
97
|
+
await globalExpect(definePlugin({
|
|
98
|
+
name: "biome",
|
|
99
|
+
apiVersion: 1,
|
|
100
|
+
color: (s) => s,
|
|
101
|
+
services: () => {
|
|
102
|
+
const svc = new FakeBiome();
|
|
103
|
+
return {
|
|
104
|
+
lint: svc,
|
|
105
|
+
format: svc,
|
|
106
|
+
jscheck: svc
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
})({ only: ["tsc"] }).services(ctx())).rejects.toThrow(/@rrlab\/biome-plugin: unknown capability 'tsc' in 'only'\. Available: /);
|
|
110
|
+
});
|
|
111
|
+
it("throws when a required pkg is missing in the host", async () => {
|
|
112
|
+
await globalExpect(definePlugin({
|
|
113
|
+
name: "ghostly",
|
|
114
|
+
apiVersion: 1,
|
|
115
|
+
color: (s) => s,
|
|
116
|
+
services: () => ({ lint: new MissingService() })
|
|
117
|
+
})().services(ctx())).rejects.toThrow(/@rrlab\/ghostly-plugin requires ghostly-bin-that-does-not-exist to be installed/);
|
|
118
|
+
});
|
|
119
|
+
it("succeeds when distinct services share a pkg (deduplicated probe)", async () => {
|
|
120
|
+
const caps = await definePlugin({
|
|
121
|
+
name: "ts",
|
|
122
|
+
apiVersion: 1,
|
|
123
|
+
color: (s) => s,
|
|
124
|
+
services: () => ({ typecheck: new FakeTsc() })
|
|
125
|
+
})().services(ctx());
|
|
126
|
+
globalExpect(Object.keys(caps)).toEqual(["typecheck"]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
//#endregion
|
|
130
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { pickPreset } from "../pick-preset.mjs";
|
|
2
|
+
import { i as it, n as vi, r as describe, t as globalExpect } from "../../test.DNmyFkvJ-09ScyH13.mjs";
|
|
3
|
+
//#region src/lib/plugin/__tests__/pick-preset.test.ts
|
|
4
|
+
function makeCtx(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
shell: {},
|
|
7
|
+
logger: {},
|
|
8
|
+
appPkg: { dirPath: process.cwd() },
|
|
9
|
+
prompts: overrides.prompts ?? {},
|
|
10
|
+
flags: {
|
|
11
|
+
force: false,
|
|
12
|
+
yes: false,
|
|
13
|
+
nonInteractive: false,
|
|
14
|
+
...overrides.flags
|
|
15
|
+
},
|
|
16
|
+
release: {}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const PRESETS = {
|
|
20
|
+
lib: { label: "Library" },
|
|
21
|
+
bin: { label: "CLI / Node binary" }
|
|
22
|
+
};
|
|
23
|
+
describe("pickPreset", () => {
|
|
24
|
+
it("returns the defaultPreset under --yes", async () => {
|
|
25
|
+
globalExpect(await pickPreset(makeCtx({ flags: { yes: true } }), {
|
|
26
|
+
message: "Which kind of build?",
|
|
27
|
+
presets: PRESETS,
|
|
28
|
+
defaultPreset: "lib"
|
|
29
|
+
})).toBe("lib");
|
|
30
|
+
});
|
|
31
|
+
it("returns the defaultPreset under non-interactive", async () => {
|
|
32
|
+
globalExpect(await pickPreset(makeCtx({ flags: { nonInteractive: true } }), {
|
|
33
|
+
message: "x",
|
|
34
|
+
presets: PRESETS,
|
|
35
|
+
defaultPreset: "bin"
|
|
36
|
+
})).toBe("bin");
|
|
37
|
+
});
|
|
38
|
+
it("returns the user's interactive choice", async () => {
|
|
39
|
+
const select = vi.fn().mockResolvedValue("bin");
|
|
40
|
+
globalExpect(await pickPreset(makeCtx({ prompts: {
|
|
41
|
+
confirm: vi.fn(),
|
|
42
|
+
select,
|
|
43
|
+
isCancel: () => false
|
|
44
|
+
} }), {
|
|
45
|
+
message: "Which kind of build?",
|
|
46
|
+
presets: PRESETS,
|
|
47
|
+
defaultPreset: "lib"
|
|
48
|
+
})).toBe("bin");
|
|
49
|
+
const call = select.mock.calls[0]?.[0];
|
|
50
|
+
globalExpect(call.options).toEqual([{
|
|
51
|
+
value: "lib",
|
|
52
|
+
label: "Library"
|
|
53
|
+
}, {
|
|
54
|
+
value: "bin",
|
|
55
|
+
label: "CLI / Node binary"
|
|
56
|
+
}]);
|
|
57
|
+
});
|
|
58
|
+
it("throws 'Cancelled by user.' when the user cancels", async () => {
|
|
59
|
+
const cancelSymbol = Symbol("cancel");
|
|
60
|
+
await globalExpect(pickPreset(makeCtx({ prompts: {
|
|
61
|
+
confirm: vi.fn(),
|
|
62
|
+
select: vi.fn().mockResolvedValue(cancelSymbol),
|
|
63
|
+
isCancel: (v) => v === cancelSymbol
|
|
64
|
+
} }), {
|
|
65
|
+
message: "x",
|
|
66
|
+
presets: PRESETS,
|
|
67
|
+
defaultPreset: "lib"
|
|
68
|
+
})).rejects.toThrow("Cancelled by user.");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
//#endregion
|
|
72
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { t as PluginRegistry } from "../../registry-BgqfKK5L.mjs";
|
|
2
|
+
import { i as it, r as describe, t as globalExpect } from "../../test.DNmyFkvJ-09ScyH13.mjs";
|
|
3
|
+
//#region src/lib/plugin/__tests__/registry.test.ts
|
|
4
|
+
function plugin(name) {
|
|
5
|
+
return {
|
|
6
|
+
name,
|
|
7
|
+
ui: name,
|
|
8
|
+
color: (str) => str,
|
|
9
|
+
apiVersion: 1,
|
|
10
|
+
services: async () => ({})
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async function okReport() {
|
|
14
|
+
return {
|
|
15
|
+
ok: true,
|
|
16
|
+
output: ""
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function fakeLinter() {
|
|
20
|
+
return {
|
|
21
|
+
ui: "fake",
|
|
22
|
+
lint: okReport,
|
|
23
|
+
doctor: okReport
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function fakeFormatter() {
|
|
27
|
+
return {
|
|
28
|
+
ui: "fake",
|
|
29
|
+
format: okReport,
|
|
30
|
+
doctor: okReport
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function fakeTypeChecker() {
|
|
34
|
+
return {
|
|
35
|
+
ui: "fake",
|
|
36
|
+
check: okReport,
|
|
37
|
+
doctor: okReport
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function fakePacker() {
|
|
41
|
+
return {
|
|
42
|
+
ui: "fake",
|
|
43
|
+
pack: okReport,
|
|
44
|
+
doctor: okReport
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
describe("PluginRegistry", () => {
|
|
48
|
+
describe("getService(capability)", () => {
|
|
49
|
+
it("returns undefined when no plugin provides the capability", () => {
|
|
50
|
+
globalExpect(new PluginRegistry().getService("lint")).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
it("returns the impl when exactly one plugin provides the capability", () => {
|
|
53
|
+
const registry = new PluginRegistry();
|
|
54
|
+
const linter = fakeLinter();
|
|
55
|
+
registry.register(plugin("biome"), { lint: linter });
|
|
56
|
+
globalExpect(registry.getService("lint")).toBe(linter);
|
|
57
|
+
});
|
|
58
|
+
it("does not collide across kinds", () => {
|
|
59
|
+
const registry = new PluginRegistry();
|
|
60
|
+
const linter = fakeLinter();
|
|
61
|
+
const formatter = fakeFormatter();
|
|
62
|
+
registry.register(plugin("oxc"), {
|
|
63
|
+
lint: linter,
|
|
64
|
+
format: formatter
|
|
65
|
+
});
|
|
66
|
+
globalExpect(registry.getService("lint")).toBe(linter);
|
|
67
|
+
globalExpect(registry.getService("format")).toBe(formatter);
|
|
68
|
+
globalExpect(registry.getService("jscheck")).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
it("throws when two plugins provide the same capability, listing both", () => {
|
|
71
|
+
const registry = new PluginRegistry();
|
|
72
|
+
registry.register(plugin("biome"), { lint: fakeLinter() });
|
|
73
|
+
registry.register(plugin("oxc"), { lint: fakeLinter() });
|
|
74
|
+
globalExpect(() => registry.getService("lint")).toThrowError(/biome.*oxc|oxc.*biome/);
|
|
75
|
+
});
|
|
76
|
+
it("suggests the 'only' option when reporting multi-provider conflicts", () => {
|
|
77
|
+
const registry = new PluginRegistry();
|
|
78
|
+
registry.register(plugin("biome"), { lint: fakeLinter() });
|
|
79
|
+
registry.register(plugin("oxc"), { lint: fakeLinter() });
|
|
80
|
+
globalExpect(() => registry.getService("lint")).toThrowError(/only:\s*\['lint'\]/);
|
|
81
|
+
});
|
|
82
|
+
it("supports typecheck and pack kinds", () => {
|
|
83
|
+
const registry = new PluginRegistry();
|
|
84
|
+
const tc = fakeTypeChecker();
|
|
85
|
+
const packer = fakePacker();
|
|
86
|
+
registry.register(plugin("ts"), { typecheck: tc });
|
|
87
|
+
registry.register(plugin("tsdown"), { pack: packer });
|
|
88
|
+
globalExpect(registry.getService("typecheck")).toBe(tc);
|
|
89
|
+
globalExpect(registry.getService("pack")).toBe(packer);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe("providersOf(capability)", () => {
|
|
93
|
+
it("returns every plugin that provides the capability", () => {
|
|
94
|
+
const registry = new PluginRegistry();
|
|
95
|
+
registry.register(plugin("biome"), { lint: fakeLinter() });
|
|
96
|
+
registry.register(plugin("oxc"), { lint: fakeLinter() });
|
|
97
|
+
registry.register(plugin("tsdown"), { pack: fakePacker() });
|
|
98
|
+
globalExpect(registry.providersOf("lint").map((p) => p.plugin.name).sort()).toEqual(["biome", "oxc"]);
|
|
99
|
+
globalExpect(registry.providersOf("format")).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
//#endregion
|
|
104
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ToolService } from "./tool-service.mjs";
|
|
2
|
+
//#region src/lib/plugin/bin-probe.ts
|
|
3
|
+
async function probeBins(services, pluginName) {
|
|
4
|
+
const toolServices = services.filter((s) => s instanceof ToolService);
|
|
5
|
+
const distinct = /* @__PURE__ */ new Map();
|
|
6
|
+
for (const svc of toolServices) if (!distinct.has(svc.pkg)) distinct.set(svc.pkg, svc);
|
|
7
|
+
if (distinct.size === 0) return;
|
|
8
|
+
const probes = [...distinct.values()].map(async (svc) => {
|
|
9
|
+
try {
|
|
10
|
+
await svc.getBinDir();
|
|
11
|
+
} catch {
|
|
12
|
+
return svc.pkg;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
});
|
|
16
|
+
const missing = (await Promise.all(probes)).filter((p) => p !== null);
|
|
17
|
+
if (missing.length === 0) return;
|
|
18
|
+
const pkgName = `@rrlab/${pluginName}-plugin`;
|
|
19
|
+
throw new Error(`${pkgName} requires ${missing.join(", ")} to be installed in the host project. Run: rr plugins add ${pluginName} (or: pnpm add -D ${missing.join(" ")})`);
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { probeBins };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { i as InstallContext } from "../types-Iu4IyWof.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/plugin/decide-scaffold.d.ts
|
|
4
|
+
type ScaffoldDecision = "create" | "patch" | "overwrite" | "skip";
|
|
5
|
+
type DecideScaffoldOptions = {
|
|
6
|
+
/** The config file label shown to the user (e.g. `"biome.json"`, `"tsdown.config.ts"`). */label: string; /** Whether the file currently exists in the app project. */
|
|
7
|
+
fileExists: boolean; /** Short description of what "patch" does, shown in the select option. */
|
|
8
|
+
patchHint: string;
|
|
9
|
+
/**
|
|
10
|
+
* What to return when the file exists and the run is unattended (`--yes` / non-interactive).
|
|
11
|
+
* - `"patch"` (default): assume the user wants to merge our config into theirs (safe for JSON we can edit).
|
|
12
|
+
* - `"skip"`: assume the user owns the file (right for TS modules we'd otherwise rewrite blindly).
|
|
13
|
+
*/
|
|
14
|
+
unattendedExistingAction?: "patch" | "skip";
|
|
15
|
+
};
|
|
16
|
+
declare function decideScaffold(ctx: InstallContext, opts: DecideScaffoldOptions): Promise<ScaffoldDecision>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { DecideScaffoldOptions, ScaffoldDecision, decideScaffold };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/lib/plugin/decide-scaffold.ts
|
|
2
|
+
async function decideScaffold(ctx, opts) {
|
|
3
|
+
const { label, fileExists, patchHint, unattendedExistingAction = "patch" } = opts;
|
|
4
|
+
if (!fileExists) {
|
|
5
|
+
if (ctx.flags.yes || ctx.flags.nonInteractive) return "create";
|
|
6
|
+
const choice = await ctx.prompts.confirm({
|
|
7
|
+
message: `Scaffold ${label}?`,
|
|
8
|
+
initialValue: true
|
|
9
|
+
});
|
|
10
|
+
if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
|
|
11
|
+
return choice ? "create" : "skip";
|
|
12
|
+
}
|
|
13
|
+
if (ctx.flags.yes || ctx.flags.nonInteractive) return unattendedExistingAction;
|
|
14
|
+
const choice = await ctx.prompts.select({
|
|
15
|
+
message: `${label} already exists. What do you want to do?`,
|
|
16
|
+
options: [
|
|
17
|
+
{
|
|
18
|
+
value: "patch",
|
|
19
|
+
label: `Patch — ${patchHint}`
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
value: "skip",
|
|
23
|
+
label: "Skip — leave it alone"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
value: "overwrite",
|
|
27
|
+
label: "Overwrite — replace with a fresh scaffold"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
initialValue: "patch"
|
|
31
|
+
});
|
|
32
|
+
if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
|
|
33
|
+
return choice;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { decideScaffold };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { c as Plugin, d as PluginServices, f as UninstallContext, i as InstallContext, l as PluginCapability, m as UninstallResult, o as InstallResult, u as PluginContext } from "../types-Iu4IyWof.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/plugin/define-plugin.d.ts
|
|
4
|
+
type PluginDefinition<TServices extends PluginServices> = {
|
|
5
|
+
apiVersion: 1;
|
|
6
|
+
name: string;
|
|
7
|
+
color: (value: string) => string;
|
|
8
|
+
services(ctx: PluginContext): TServices | Promise<TServices>;
|
|
9
|
+
install?(this: void, ctx: InstallContext): Promise<InstallResult>;
|
|
10
|
+
uninstall?(this: void, ctx: UninstallContext): Promise<UninstallResult>;
|
|
11
|
+
};
|
|
12
|
+
type Options<TKind extends PluginCapability> = {
|
|
13
|
+
only?: readonly TKind[];
|
|
14
|
+
};
|
|
15
|
+
declare function definePlugin<TServices extends PluginServices>(definition: PluginDefinition<TServices>): (options?: Options<keyof TServices & PluginCapability>) => Plugin;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { PluginDefinition, definePlugin };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { probeBins } from "./bin-probe.mjs";
|
|
2
|
+
//#region src/lib/plugin/define-plugin.ts
|
|
3
|
+
function definePlugin(definition) {
|
|
4
|
+
return (options) => {
|
|
5
|
+
const only = options?.only;
|
|
6
|
+
const pkgName = `@rrlab/${definition.name}-plugin`;
|
|
7
|
+
return {
|
|
8
|
+
name: definition.name,
|
|
9
|
+
color: definition.color,
|
|
10
|
+
ui: definition.color(definition.name),
|
|
11
|
+
apiVersion: definition.apiVersion,
|
|
12
|
+
install: definition.install,
|
|
13
|
+
uninstall: definition.uninstall,
|
|
14
|
+
async services(ctx) {
|
|
15
|
+
const services = await definition.services(ctx);
|
|
16
|
+
await probeBins(Object.values(services), definition.name);
|
|
17
|
+
if (!only) return services;
|
|
18
|
+
for (const k of only) if (!(k in services)) throw new Error(`${pkgName}: unknown capability '${k}' in 'only'. Available: ${Object.keys(services).join(", ")}.`);
|
|
19
|
+
return Object.fromEntries(only.map((capability) => [capability, services[capability]]));
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { definePlugin };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { l as PluginCapability } from "../types-Iu4IyWof.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/plugin/directory.d.ts
|
|
4
|
+
declare const PLUGINS_DIRECTORY: {
|
|
5
|
+
readonly ts: {
|
|
6
|
+
readonly pkg: "@rrlab/ts-plugin";
|
|
7
|
+
readonly name: "ts";
|
|
8
|
+
readonly capabilities: ["typecheck"];
|
|
9
|
+
};
|
|
10
|
+
readonly biome: {
|
|
11
|
+
readonly pkg: "@rrlab/biome-plugin";
|
|
12
|
+
readonly name: "biome";
|
|
13
|
+
readonly capabilities: ["format", "jscheck", "lint"];
|
|
14
|
+
};
|
|
15
|
+
readonly oxc: {
|
|
16
|
+
readonly pkg: "@rrlab/oxc-plugin";
|
|
17
|
+
readonly name: "oxc";
|
|
18
|
+
readonly capabilities: ["format", "lint", "jscheck", "typecheck"];
|
|
19
|
+
};
|
|
20
|
+
readonly tsdown: {
|
|
21
|
+
readonly pkg: "@rrlab/tsdown-plugin";
|
|
22
|
+
readonly name: "tsdown";
|
|
23
|
+
readonly capabilities: ["pack"];
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
type PluginName = keyof typeof PLUGINS_DIRECTORY;
|
|
27
|
+
declare function allPluginNames(): readonly PluginName[];
|
|
28
|
+
declare function isPluginName(name: string): name is PluginName;
|
|
29
|
+
declare function providersOf(capability: PluginCapability): ({
|
|
30
|
+
readonly pkg: "@rrlab/ts-plugin";
|
|
31
|
+
readonly name: "ts";
|
|
32
|
+
readonly capabilities: ["typecheck"];
|
|
33
|
+
} | {
|
|
34
|
+
readonly pkg: "@rrlab/biome-plugin";
|
|
35
|
+
readonly name: "biome";
|
|
36
|
+
readonly capabilities: ["format", "jscheck", "lint"];
|
|
37
|
+
} | {
|
|
38
|
+
readonly pkg: "@rrlab/oxc-plugin";
|
|
39
|
+
readonly name: "oxc";
|
|
40
|
+
readonly capabilities: ["format", "lint", "jscheck", "typecheck"];
|
|
41
|
+
} | {
|
|
42
|
+
readonly pkg: "@rrlab/tsdown-plugin";
|
|
43
|
+
readonly name: "tsdown";
|
|
44
|
+
readonly capabilities: ["pack"];
|
|
45
|
+
})[];
|
|
46
|
+
//#endregion
|
|
47
|
+
export { PLUGINS_DIRECTORY, PluginName, allPluginNames, isPluginName, providersOf };
|