@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 +45 -0
- package/dist/index.d.mts +24 -0
- package/dist/index.mjs +195 -0
- package/package.json +52 -0
- package/src/index.ts +195 -0
- package/src/tool-versions.ts +6 -0
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}`.
|
package/dist/index.d.mts
ADDED
|
@@ -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;
|