@rrlab/ts-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,45 @@
1
+ # @rrlab/ts-plugin
2
+
3
+ TypeScript plugin for [`@rrlab/cli`](https://npmjs.com/package/@rrlab/cli). Provides the `tsc` capability backed by the TypeScript compiler.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ rr plugins add ts
9
+ ```
10
+
11
+ `rr plugins add` installs `@rrlab/ts-plugin`, adds `typescript` as a `devDependency`, and (with your confirmation) scaffolds a `tsconfig.json` extending one of the [`@rrlab/ts-config`](https://npmjs.com/package/@rrlab/ts-config) presets. When you opt into scaffolding, you're prompted to pick:
12
+
13
+ - `react` — React app.
14
+ - `dom-app` — Web app (DOM, no React).
15
+ - `dom-lib` — Browser library.
16
+ - `no-dom-app` — Node.js app / CLI (default).
17
+ - `no-dom-lib` — Node.js library.
18
+
19
+ The `no-dom-*` presets also bring `@types/node` along. If `tsconfig.json` already exists, you can choose to patch (the safe migration default), skip, or overwrite.
20
+
21
+ ## What it provides
22
+
23
+ | Capability | Surface |
24
+ |---|---|
25
+ | `tsc` | `rr tsc` (workspace-aware in monorepos), `rr tsc doctor` |
26
+
27
+ ## Configuration
28
+
29
+ The scaffolded `tsconfig.json` is a thin wrapper:
30
+
31
+ ```json
32
+ {
33
+ "extends": "@rrlab/ts-config/no-dom/app"
34
+ }
35
+ ```
36
+
37
+ Override compiler options or `include`/`exclude` by adding them to your local `tsconfig.json`. The preset lives in [`@rrlab/ts-config`](https://npmjs.com/package/@rrlab/ts-config).
38
+
39
+ ## Removal
40
+
41
+ ```sh
42
+ rr plugins remove ts
43
+ ```
44
+
45
+ Removes `typescript`, `@rrlab/ts-config`, and `@types/node` from `package.json`. If `tsconfig.json` was only the wrapper we scaffolded, deletes it; otherwise unsets the `extends` and leaves the rest of your settings untouched. Drops the `ts()` entry from `run-run.config.{ts,mts}`.
@@ -0,0 +1,24 @@
1
+ import { InstallContext, InstallResult, ToolService, TypeCheckOptions, TypeChecker, 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 typescript: {
7
+ readonly install: "^6.0.0";
8
+ readonly peer: ">=5.0.0";
9
+ };
10
+ readonly "@types/node": {
11
+ readonly install: ">=20";
12
+ };
13
+ };
14
+ //#endregion
15
+ //#region src/index.d.ts
16
+ declare class TscService extends ToolService implements TypeChecker {
17
+ constructor(shellService: ShellService);
18
+ check(options?: TypeCheckOptions): Promise<void>;
19
+ }
20
+ declare function install(ctx: InstallContext): Promise<InstallResult>;
21
+ declare function uninstall(ctx: UninstallContext): Promise<UninstallResult>;
22
+ declare const ts: (options: void) => import("@rrlab/cli/plugin").Plugin;
23
+ //#endregion
24
+ export { TOOL_VERSIONS, TscService, ts as default, install, uninstall };
package/dist/index.mjs ADDED
@@ -0,0 +1,195 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ToolService, definePlugin } from "@rrlab/cli/plugin";
4
+ import { colorize } from "@vlandoss/clibuddy";
5
+ import { parse } from "comment-json";
6
+ //#region src/tool-versions.ts
7
+ const TOOL_VERSIONS = {
8
+ typescript: {
9
+ install: "^6.0.0",
10
+ peer: ">=5.0.0"
11
+ },
12
+ "@types/node": { install: ">=20" }
13
+ };
14
+ //#endregion
15
+ //#region src/index.ts
16
+ const FROM = import.meta.url;
17
+ const UI = colorize("#3178C6")("tsc");
18
+ const TSCONFIG = "tsconfig.json";
19
+ var TscService = class extends ToolService {
20
+ constructor(shellService) {
21
+ super({
22
+ pkg: "typescript",
23
+ bin: "tsc",
24
+ ui: UI,
25
+ shellService,
26
+ from: FROM
27
+ });
28
+ }
29
+ async check(options = {}) {
30
+ await this.exec(["--noEmit"], {
31
+ cwd: options.cwd,
32
+ verbose: !options.cwd
33
+ });
34
+ }
35
+ };
36
+ const PRESETS = {
37
+ react: {
38
+ extendsPath: "@rrlab/ts-config/react",
39
+ label: "React app",
40
+ needsNode: false
41
+ },
42
+ "dom-app": {
43
+ extendsPath: "@rrlab/ts-config/dom/app",
44
+ label: "Web app (DOM, no React)",
45
+ needsNode: false
46
+ },
47
+ "dom-lib": {
48
+ extendsPath: "@rrlab/ts-config/dom/lib",
49
+ label: "Browser library",
50
+ needsNode: false
51
+ },
52
+ "no-dom-app": {
53
+ extendsPath: "@rrlab/ts-config/no-dom/app",
54
+ label: "Node.js app / CLI",
55
+ needsNode: true
56
+ },
57
+ "no-dom-lib": {
58
+ extendsPath: "@rrlab/ts-config/no-dom/lib",
59
+ label: "Node.js library",
60
+ needsNode: true
61
+ }
62
+ };
63
+ const DEFAULT_PRESET = "no-dom-app";
64
+ async function install(ctx) {
65
+ const scaffoldDecision = await decideScaffoldAction(ctx, await pathExists(path.join(ctx.appPkg.dirPath, TSCONFIG)));
66
+ if (scaffoldDecision === "skip") return { devDependencies: { typescript: TOOL_VERSIONS.typescript.install } };
67
+ const presetInfo = PRESETS[await pickPreset(ctx)];
68
+ const devDependencies = {
69
+ typescript: TOOL_VERSIONS.typescript.install,
70
+ "@rrlab/ts-config": "^0.1.0"
71
+ };
72
+ if (presetInfo.needsNode) devDependencies["@types/node"] = TOOL_VERSIONS["@types/node"].install;
73
+ const wrapper = { extends: presetInfo.extendsPath };
74
+ return {
75
+ devDependencies,
76
+ files: [scaffoldDecision === "create" || scaffoldDecision === "overwrite" ? {
77
+ kind: "create",
78
+ path: TSCONFIG,
79
+ content: `${JSON.stringify(wrapper, null, 2)}\n`,
80
+ overwrite: scaffoldDecision === "overwrite" || ctx.flags.force
81
+ } : {
82
+ kind: "edit-json",
83
+ path: TSCONFIG,
84
+ edits: [{
85
+ op: "set",
86
+ path: "/extends",
87
+ value: presetInfo.extendsPath,
88
+ mode: "replace"
89
+ }]
90
+ }]
91
+ };
92
+ }
93
+ async function uninstall(ctx) {
94
+ const tsconfigPath = path.join(ctx.appPkg.dirPath, TSCONFIG);
95
+ const removeDependencies = [
96
+ "typescript",
97
+ "@rrlab/ts-config",
98
+ "@types/node"
99
+ ];
100
+ if (!await pathExists(tsconfigPath)) return { removeDependencies };
101
+ let existing;
102
+ try {
103
+ existing = parse(await fs.readFile(tsconfigPath, "utf8"));
104
+ } catch {}
105
+ const files = [];
106
+ if (existing) {
107
+ const { extends: _drop, ...rest } = existing;
108
+ if (Object.keys(rest).filter((k) => !k.startsWith("$")).length === 0) files.push({
109
+ kind: "delete",
110
+ path: TSCONFIG
111
+ });
112
+ else files.push({
113
+ kind: "edit-json",
114
+ path: TSCONFIG,
115
+ edits: [{
116
+ op: "unset",
117
+ path: "/extends"
118
+ }]
119
+ });
120
+ }
121
+ return {
122
+ removeDependencies,
123
+ files
124
+ };
125
+ }
126
+ async function decideScaffoldAction(ctx, fileExists) {
127
+ if (!fileExists) {
128
+ if (ctx.flags.yes || ctx.flags.nonInteractive) return "create";
129
+ const choice = await ctx.prompts.confirm({
130
+ message: `Scaffold ${TSCONFIG} with an @rrlab/ts-config preset?`,
131
+ initialValue: true
132
+ });
133
+ if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
134
+ return choice ? "create" : "skip";
135
+ }
136
+ if (ctx.flags.yes || ctx.flags.nonInteractive) return "patch";
137
+ const choice = await ctx.prompts.select({
138
+ message: `${TSCONFIG} already exists. What do you want to do?`,
139
+ options: [
140
+ {
141
+ value: "patch",
142
+ label: "Patch — update extends, keep my other settings"
143
+ },
144
+ {
145
+ value: "skip",
146
+ label: "Skip — leave it alone"
147
+ },
148
+ {
149
+ value: "overwrite",
150
+ label: "Overwrite — replace with a fresh scaffold"
151
+ }
152
+ ],
153
+ initialValue: "patch"
154
+ });
155
+ if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
156
+ return choice;
157
+ }
158
+ async function pickPreset(ctx) {
159
+ if (ctx.flags.yes || ctx.flags.nonInteractive) return DEFAULT_PRESET;
160
+ const choice = await ctx.prompts.select({
161
+ message: "Which kind of TS project do you need?",
162
+ options: Object.entries(PRESETS).map(([value, meta]) => ({
163
+ value,
164
+ label: meta.label
165
+ })),
166
+ initialValue: DEFAULT_PRESET
167
+ });
168
+ if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
169
+ return choice;
170
+ }
171
+ async function pathExists(p) {
172
+ try {
173
+ await fs.access(p);
174
+ return true;
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+ const ts = definePlugin(() => ({
180
+ name: "ts",
181
+ apiVersion: 1,
182
+ install,
183
+ uninstall,
184
+ async setup({ shell }) {
185
+ const svc = new TscService(shell);
186
+ try {
187
+ await svc.getBinDir();
188
+ } catch (_err) {
189
+ throw new Error("@rrlab/ts-plugin requires typescript to be installed in the host project. Run: rr plugins add ts (or: pnpm add -D typescript)");
190
+ }
191
+ return { tsc: svc };
192
+ }
193
+ }));
194
+ //#endregion
195
+ export { TOOL_VERSIONS, TscService, ts as default, install, uninstall };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@rrlab/ts-plugin",
3
+ "version": "0.0.1-git-06ed46c.0",
4
+ "description": "TypeScript plugin for @rrlab/cli — provides the tsc capability.",
5
+ "homepage": "https://github.com/variableland/dx/tree/main/run-run/ts-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/ts-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
+ "typescript": ">=5.0.0",
41
+ "@rrlab/cli": "0.0.2-git-06ed46c.0"
42
+ },
43
+ "devDependencies": {
44
+ "typescript": "6.0.3",
45
+ "@rrlab/tsdown-config": "^0.0.1-git-06ed46c.0",
46
+ "@rrlab/cli": "0.0.2-git-06ed46c.0"
47
+ },
48
+ "scripts": {
49
+ "build": "tsdown",
50
+ "test": "vitest run"
51
+ }
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,195 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ definePlugin,
5
+ type FileOp,
6
+ type InstallContext,
7
+ type InstallResult,
8
+ ToolService,
9
+ type TypeChecker,
10
+ type TypeCheckOptions,
11
+ type UninstallContext,
12
+ type UninstallResult,
13
+ } from "@rrlab/cli/plugin";
14
+ import { colorize, type ShellService } from "@vlandoss/clibuddy";
15
+ import { parse as parseJsonc } from "comment-json";
16
+ import { TOOL_VERSIONS } from "./tool-versions.ts";
17
+
18
+ export { TOOL_VERSIONS } from "./tool-versions.ts";
19
+
20
+ const FROM = import.meta.url;
21
+ const UI = colorize("#3178C6")("tsc");
22
+ const TSCONFIG = "tsconfig.json";
23
+
24
+ export class TscService extends ToolService implements TypeChecker {
25
+ constructor(shellService: ShellService) {
26
+ super({ pkg: "typescript", bin: "tsc", ui: UI, shellService, from: FROM });
27
+ }
28
+
29
+ async check(options: TypeCheckOptions = {}): Promise<void> {
30
+ await this.exec(["--noEmit"], { cwd: options.cwd, verbose: !options.cwd });
31
+ }
32
+ }
33
+
34
+ type Preset = "react" | "dom-app" | "dom-lib" | "no-dom-app" | "no-dom-lib";
35
+
36
+ type PresetInfo = {
37
+ extendsPath: string;
38
+ label: string;
39
+ /** `@types/node` is only required by the no-dom presets. */
40
+ needsNode: boolean;
41
+ };
42
+
43
+ const PRESETS: Record<Preset, PresetInfo> = {
44
+ react: { extendsPath: "@rrlab/ts-config/react", label: "React app", needsNode: false },
45
+ "dom-app": { extendsPath: "@rrlab/ts-config/dom/app", label: "Web app (DOM, no React)", needsNode: false },
46
+ "dom-lib": { extendsPath: "@rrlab/ts-config/dom/lib", label: "Browser library", needsNode: false },
47
+ "no-dom-app": { extendsPath: "@rrlab/ts-config/no-dom/app", label: "Node.js app / CLI", needsNode: true },
48
+ "no-dom-lib": { extendsPath: "@rrlab/ts-config/no-dom/lib", label: "Node.js library", needsNode: true },
49
+ };
50
+
51
+ const DEFAULT_PRESET: Preset = "no-dom-app";
52
+
53
+ type ExistingFileAction = "skip" | "patch" | "overwrite";
54
+
55
+ export async function install(ctx: InstallContext): Promise<InstallResult> {
56
+ const tsconfigPath = path.join(ctx.appPkg.dirPath, TSCONFIG);
57
+ const fileExists = await pathExists(tsconfigPath);
58
+
59
+ const scaffoldDecision = await decideScaffoldAction(ctx, fileExists);
60
+ if (scaffoldDecision === "skip") {
61
+ return { devDependencies: { typescript: TOOL_VERSIONS.typescript.install } };
62
+ }
63
+
64
+ const preset = await pickPreset(ctx);
65
+ const presetInfo = PRESETS[preset];
66
+
67
+ const devDependencies: Record<string, string> = {
68
+ typescript: TOOL_VERSIONS.typescript.install,
69
+ "@rrlab/ts-config": "^0.1.0",
70
+ };
71
+ if (presetInfo.needsNode) devDependencies["@types/node"] = TOOL_VERSIONS["@types/node"].install;
72
+
73
+ const wrapper = { extends: presetInfo.extendsPath };
74
+ const file: FileOp =
75
+ scaffoldDecision === "create" || scaffoldDecision === "overwrite"
76
+ ? {
77
+ kind: "create",
78
+ path: TSCONFIG,
79
+ content: `${JSON.stringify(wrapper, null, 2)}\n`,
80
+ overwrite: scaffoldDecision === "overwrite" || ctx.flags.force,
81
+ }
82
+ : {
83
+ kind: "edit-json",
84
+ path: TSCONFIG,
85
+ edits: [{ op: "set", path: "/extends", value: presetInfo.extendsPath, mode: "replace" }],
86
+ };
87
+
88
+ return { devDependencies, files: [file] };
89
+ }
90
+
91
+ export async function uninstall(ctx: UninstallContext): Promise<UninstallResult> {
92
+ const tsconfigPath = path.join(ctx.appPkg.dirPath, TSCONFIG);
93
+ const removeDependencies = ["typescript", "@rrlab/ts-config", "@types/node"];
94
+
95
+ if (!(await pathExists(tsconfigPath))) {
96
+ return { removeDependencies };
97
+ }
98
+
99
+ // Read the current tsconfig to decide between full delete and surgical unset.
100
+ let existing: Record<string, unknown> | undefined;
101
+ try {
102
+ const text = await fs.readFile(tsconfigPath, "utf8");
103
+ existing = parseJsonc(text) as Record<string, unknown>;
104
+ } catch {
105
+ /* malformed — leave it alone, only edit if we can parse */
106
+ }
107
+
108
+ const files: FileOp[] = [];
109
+ if (existing) {
110
+ const { extends: _drop, ...rest } = existing;
111
+ const semanticKeys = Object.keys(rest).filter((k) => !k.startsWith("$"));
112
+ if (semanticKeys.length === 0) {
113
+ files.push({ kind: "delete", path: TSCONFIG });
114
+ } else {
115
+ files.push({ kind: "edit-json", path: TSCONFIG, edits: [{ op: "unset", path: "/extends" }] });
116
+ }
117
+ }
118
+
119
+ return { removeDependencies, files };
120
+ }
121
+
122
+ async function decideScaffoldAction(
123
+ ctx: InstallContext,
124
+ fileExists: boolean,
125
+ ): Promise<"create" | "patch" | "overwrite" | "skip"> {
126
+ if (!fileExists) {
127
+ if (ctx.flags.yes || ctx.flags.nonInteractive) return "create";
128
+ const choice = await ctx.prompts.confirm({
129
+ message: `Scaffold ${TSCONFIG} with an @rrlab/ts-config preset?`,
130
+ initialValue: true,
131
+ });
132
+ if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
133
+ return choice ? "create" : "skip";
134
+ }
135
+
136
+ // Existing file. Default to patch for migration safety.
137
+ if (ctx.flags.yes || ctx.flags.nonInteractive) return "patch";
138
+
139
+ const choice = await ctx.prompts.select<ExistingFileAction>({
140
+ message: `${TSCONFIG} already exists. What do you want to do?`,
141
+ options: [
142
+ { value: "patch", label: "Patch — update extends, keep my other settings" },
143
+ { value: "skip", label: "Skip — leave it alone" },
144
+ { value: "overwrite", label: "Overwrite — replace with a fresh scaffold" },
145
+ ],
146
+ initialValue: "patch",
147
+ });
148
+ if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
149
+ return choice;
150
+ }
151
+
152
+ async function pickPreset(ctx: InstallContext): Promise<Preset> {
153
+ if (ctx.flags.yes || ctx.flags.nonInteractive) return DEFAULT_PRESET;
154
+
155
+ const choice = await ctx.prompts.select<Preset>({
156
+ message: "Which kind of TS project do you need?",
157
+ options: (Object.entries(PRESETS) as Array<[Preset, PresetInfo]>).map(([value, meta]) => ({
158
+ value,
159
+ label: meta.label,
160
+ })),
161
+ initialValue: DEFAULT_PRESET,
162
+ });
163
+ if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
164
+ return choice;
165
+ }
166
+
167
+ async function pathExists(p: string): Promise<boolean> {
168
+ try {
169
+ await fs.access(p);
170
+ return true;
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+
176
+ const ts = definePlugin<void>(() => ({
177
+ name: "ts",
178
+ apiVersion: 1,
179
+ install,
180
+ uninstall,
181
+ async setup({ shell }) {
182
+ const svc = new TscService(shell);
183
+ try {
184
+ await svc.getBinDir();
185
+ } catch (_err) {
186
+ throw new Error(
187
+ "@rrlab/ts-plugin requires typescript to be installed in the host project. " +
188
+ "Run: rr plugins add ts (or: pnpm add -D typescript)",
189
+ );
190
+ }
191
+ return { tsc: svc };
192
+ },
193
+ }));
194
+
195
+ export default ts;
@@ -0,0 +1,6 @@
1
+ export const TOOL_VERSIONS = {
2
+ // install range > peer range on purpose: pin latest stable for fresh installs,
3
+ // accept TS 5+ if the host already has it.
4
+ typescript: { install: "^6.0.0", peer: ">=5.0.0" },
5
+ "@types/node": { install: ">=20" },
6
+ } as const;