@rrlab/tsdown-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 +27 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.mjs +263 -0
- package/package.json +52 -0
- package/src/index.ts +324 -0
- package/src/tool-versions.ts +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @rrlab/tsdown-plugin
|
|
2
|
+
|
|
3
|
+
[tsdown](https://tsdown.dev) plugin for [`@rrlab/cli`](https://npmjs.com/package/@rrlab/cli). Provides the `pack` capability for packaging TypeScript libraries for distribution.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
rr plugins add tsdown
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Installs `@rrlab/tsdown-plugin` and adds `tsdown` as a `devDependency`. No config file is scaffolded — tsdown reads `tsdown.config.ts` if present, otherwise uses sensible defaults. Add your own when you need to customise.
|
|
12
|
+
|
|
13
|
+
## What it provides
|
|
14
|
+
|
|
15
|
+
| Capability | Surface |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `pack` | `rr pack`, `rr pack doctor` |
|
|
18
|
+
|
|
19
|
+
`rr pack` builds your library: emits ESM JavaScript + `.d.ts` declarations to `dist/` so consumers can `import` from the published package.
|
|
20
|
+
|
|
21
|
+
## Removal
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
rr plugins remove tsdown
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Removes `tsdown` from `package.json` and drops the `tsdown()` entry from `run-run.config.{ts,mts}`. `tsdown.config.ts` — if you have one — is left alone.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { InstallContext, InstallResult, 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 tsdown: {
|
|
7
|
+
readonly install: "^0.22.0";
|
|
8
|
+
readonly peer: ">=0.22.0";
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/index.d.ts
|
|
13
|
+
declare class TsdownService extends ToolService {
|
|
14
|
+
constructor(shellService: ShellService);
|
|
15
|
+
pack(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
declare function install(ctx: InstallContext): Promise<InstallResult>;
|
|
18
|
+
declare function uninstall(ctx: UninstallContext): Promise<UninstallResult>;
|
|
19
|
+
declare const tsdown: (options: void) => import("@rrlab/cli/plugin").Plugin;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { TOOL_VERSIONS, TsdownService, tsdown as default, install, uninstall };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
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 { generateCode, parseModule } from "magicast";
|
|
6
|
+
//#region src/tool-versions.ts
|
|
7
|
+
const TOOL_VERSIONS = { tsdown: {
|
|
8
|
+
install: "^0.22.0",
|
|
9
|
+
peer: ">=0.22.0"
|
|
10
|
+
} };
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/index.ts
|
|
13
|
+
const FROM = import.meta.url;
|
|
14
|
+
const UI = colorize("#FF7E18")("tsdown");
|
|
15
|
+
const CONFIG_PKG = "@rrlab/tsdown-config";
|
|
16
|
+
const DEFAULT_CONFIG_FILENAME = "tsdown.config.ts";
|
|
17
|
+
const CONFIG_FILENAMES = [
|
|
18
|
+
"tsdown.config.ts",
|
|
19
|
+
"tsdown.config.mts",
|
|
20
|
+
"tsdown.config.cts",
|
|
21
|
+
"tsdown.config.js",
|
|
22
|
+
"tsdown.config.mjs",
|
|
23
|
+
"tsdown.config.cjs"
|
|
24
|
+
];
|
|
25
|
+
const PRESETS = {
|
|
26
|
+
lib: {
|
|
27
|
+
factory: "defineLibConfig",
|
|
28
|
+
label: "Library (dts on, entry src/index.ts)"
|
|
29
|
+
},
|
|
30
|
+
bin: {
|
|
31
|
+
factory: "defineBinConfig",
|
|
32
|
+
label: "CLI / Node binary (entry src/run.ts)"
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const DEFAULT_PRESET = "lib";
|
|
36
|
+
var TsdownService = class extends ToolService {
|
|
37
|
+
constructor(shellService) {
|
|
38
|
+
super({
|
|
39
|
+
pkg: "tsdown",
|
|
40
|
+
ui: UI,
|
|
41
|
+
shellService,
|
|
42
|
+
from: FROM
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async pack() {
|
|
46
|
+
await this.exec();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
async function install(ctx) {
|
|
50
|
+
const existingPath = await findExistingConfig(ctx.appPkg.dirPath);
|
|
51
|
+
const action = await decideScaffoldAction(ctx, existingPath);
|
|
52
|
+
if (action === "skip") return { devDependencies: { tsdown: TOOL_VERSIONS.tsdown.install } };
|
|
53
|
+
const { factory } = PRESETS[await pickPreset(ctx)];
|
|
54
|
+
const devDependencies = {
|
|
55
|
+
tsdown: TOOL_VERSIONS.tsdown.install,
|
|
56
|
+
[CONFIG_PKG]: "^0.1.0"
|
|
57
|
+
};
|
|
58
|
+
if (action === "create" || action === "overwrite") return {
|
|
59
|
+
devDependencies,
|
|
60
|
+
files: [{
|
|
61
|
+
kind: "create",
|
|
62
|
+
path: existingPath ? path.relative(ctx.appPkg.dirPath, existingPath) : DEFAULT_CONFIG_FILENAME,
|
|
63
|
+
content: renderScaffold(factory),
|
|
64
|
+
overwrite: action === "overwrite" || ctx.flags.force
|
|
65
|
+
}]
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
devDependencies,
|
|
69
|
+
files: [{
|
|
70
|
+
kind: "edit-text",
|
|
71
|
+
path: path.relative(ctx.appPkg.dirPath, existingPath),
|
|
72
|
+
edit: (src) => patchToFactory(src, factory)
|
|
73
|
+
}]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function uninstall(ctx) {
|
|
77
|
+
const removeDependencies = ["tsdown", CONFIG_PKG];
|
|
78
|
+
const existingPath = await findExistingConfig(ctx.appPkg.dirPath);
|
|
79
|
+
if (!existingPath) return { removeDependencies };
|
|
80
|
+
let source;
|
|
81
|
+
try {
|
|
82
|
+
source = await fs.readFile(existingPath, "utf8");
|
|
83
|
+
} catch {
|
|
84
|
+
return { removeDependencies };
|
|
85
|
+
}
|
|
86
|
+
let mod;
|
|
87
|
+
try {
|
|
88
|
+
mod = parseModule(source);
|
|
89
|
+
} catch {
|
|
90
|
+
return { removeDependencies };
|
|
91
|
+
}
|
|
92
|
+
const scaffold = readScaffoldFactory(mod);
|
|
93
|
+
if (!scaffold) return { removeDependencies };
|
|
94
|
+
const relPath = path.relative(ctx.appPkg.dirPath, existingPath);
|
|
95
|
+
if (!scaffold.hasArgs) return {
|
|
96
|
+
removeDependencies,
|
|
97
|
+
files: [{
|
|
98
|
+
kind: "delete",
|
|
99
|
+
path: relPath
|
|
100
|
+
}]
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
removeDependencies,
|
|
104
|
+
files: [{
|
|
105
|
+
kind: "edit-text",
|
|
106
|
+
path: relPath,
|
|
107
|
+
edit: (src) => patchBackToDefineConfig(src)
|
|
108
|
+
}]
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function findExistingConfig(cwd) {
|
|
112
|
+
for (const name of CONFIG_FILENAMES) {
|
|
113
|
+
const candidate = path.join(cwd, name);
|
|
114
|
+
try {
|
|
115
|
+
await fs.access(candidate);
|
|
116
|
+
return candidate;
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
async function decideScaffoldAction(ctx, existingPath) {
|
|
122
|
+
if (!existingPath) {
|
|
123
|
+
if (ctx.flags.yes || ctx.flags.nonInteractive) return "create";
|
|
124
|
+
const choice = await ctx.prompts.confirm({
|
|
125
|
+
message: `Scaffold ${DEFAULT_CONFIG_FILENAME} from ${CONFIG_PKG}?`,
|
|
126
|
+
initialValue: true
|
|
127
|
+
});
|
|
128
|
+
if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
|
|
129
|
+
return choice ? "create" : "skip";
|
|
130
|
+
}
|
|
131
|
+
if (ctx.flags.yes || ctx.flags.nonInteractive) return "skip";
|
|
132
|
+
const relPath = path.relative(ctx.appPkg.dirPath, existingPath);
|
|
133
|
+
const choice = await ctx.prompts.select({
|
|
134
|
+
message: `${relPath} already exists. What do you want to do?`,
|
|
135
|
+
options: [
|
|
136
|
+
{
|
|
137
|
+
value: "patch",
|
|
138
|
+
label: `Patch — rewrite to use ${CONFIG_PKG}, keep my options`
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
value: "skip",
|
|
142
|
+
label: "Skip — leave it alone"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
value: "overwrite",
|
|
146
|
+
label: "Overwrite — replace with a fresh scaffold"
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
initialValue: "patch"
|
|
150
|
+
});
|
|
151
|
+
if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
|
|
152
|
+
return choice;
|
|
153
|
+
}
|
|
154
|
+
async function pickPreset(ctx) {
|
|
155
|
+
if (ctx.flags.yes || ctx.flags.nonInteractive) return DEFAULT_PRESET;
|
|
156
|
+
const choice = await ctx.prompts.select({
|
|
157
|
+
message: "Which kind of build?",
|
|
158
|
+
options: Object.entries(PRESETS).map(([value, meta]) => ({
|
|
159
|
+
value,
|
|
160
|
+
label: meta.label
|
|
161
|
+
})),
|
|
162
|
+
initialValue: DEFAULT_PRESET
|
|
163
|
+
});
|
|
164
|
+
if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
|
|
165
|
+
return choice;
|
|
166
|
+
}
|
|
167
|
+
function renderScaffold(factory) {
|
|
168
|
+
return `import { ${factory} } from "${CONFIG_PKG}";\n\nexport default ${factory}();\n`;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Rewrites a user-owned `tsdown.config.*` to use one of our factories,
|
|
172
|
+
* preserving the call's arguments. Throws when the default export is not a
|
|
173
|
+
* direct call to `defineConfig` from `tsdown` or one of our factories — we
|
|
174
|
+
* refuse to mutate shapes we don't recognise.
|
|
175
|
+
*/
|
|
176
|
+
function patchToFactory(source, factory) {
|
|
177
|
+
const mod = parseModule(source);
|
|
178
|
+
const def = readDefaultCall(mod);
|
|
179
|
+
if (!def) throw new Error(`Cannot patch tsdown config: default export is not a direct function call. Expected \`defineConfig(...)\` from "tsdown" or one of: ${ourFactoryList()}.`);
|
|
180
|
+
if (def.callee !== "defineConfig" && !isOurFactory(def.callee)) throw new Error(`Cannot patch tsdown config: default export calls \`${def.callee}()\`. Expected \`defineConfig()\` from "tsdown" or one of: ${ourFactoryList()}.`);
|
|
181
|
+
removeImport(mod, def.callee);
|
|
182
|
+
addImport(mod, factory, CONFIG_PKG);
|
|
183
|
+
setCalleeName(mod, factory);
|
|
184
|
+
return generateCode(mod).code;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Reverses `patchToFactory`. Swaps our factory call + import back to
|
|
188
|
+
* `defineConfig` from "tsdown", preserving the user's arguments. Used by
|
|
189
|
+
* uninstall when the scaffolded file has been customised.
|
|
190
|
+
*/
|
|
191
|
+
function patchBackToDefineConfig(source) {
|
|
192
|
+
const mod = parseModule(source);
|
|
193
|
+
const def = readDefaultCall(mod);
|
|
194
|
+
if (!def || !isOurFactory(def.callee)) return source;
|
|
195
|
+
removeImport(mod, def.callee);
|
|
196
|
+
addImport(mod, "defineConfig", "tsdown");
|
|
197
|
+
setCalleeName(mod, "defineConfig");
|
|
198
|
+
return generateCode(mod).code;
|
|
199
|
+
}
|
|
200
|
+
function readScaffoldFactory(mod) {
|
|
201
|
+
const def = readDefaultCall(mod);
|
|
202
|
+
if (!def || !isOurFactory(def.callee)) return null;
|
|
203
|
+
const imp = mod.imports[def.callee];
|
|
204
|
+
if (!imp || imp.from !== CONFIG_PKG) return null;
|
|
205
|
+
return {
|
|
206
|
+
factory: def.callee,
|
|
207
|
+
hasArgs: def.hasArgs
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function readDefaultCall(mod) {
|
|
211
|
+
const def = mod.exports.default;
|
|
212
|
+
if (!def || def.$type !== "function-call") return null;
|
|
213
|
+
const callee = def.$callee;
|
|
214
|
+
if (typeof callee !== "string") return null;
|
|
215
|
+
const args = def.$args;
|
|
216
|
+
return {
|
|
217
|
+
callee,
|
|
218
|
+
hasArgs: !!args && args.length > 0
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function isOurFactory(name) {
|
|
222
|
+
return name === "defineLibConfig" || name === "defineBinConfig";
|
|
223
|
+
}
|
|
224
|
+
function ourFactoryList() {
|
|
225
|
+
return Object.values(PRESETS).map((p) => `\`${p.factory}\``).join(", ");
|
|
226
|
+
}
|
|
227
|
+
function removeImport(mod, local) {
|
|
228
|
+
if (mod.imports[local]) delete mod.imports[local];
|
|
229
|
+
}
|
|
230
|
+
function addImport(mod, local, from) {
|
|
231
|
+
if (!mod.imports[local]) mod.imports.$add({
|
|
232
|
+
from,
|
|
233
|
+
imported: local,
|
|
234
|
+
local
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Renames the default export's callee identifier in-place. magicast's
|
|
239
|
+
* `$callee` is a string snapshot taken at proxy creation, so we mutate the
|
|
240
|
+
* underlying AST node directly — `generateCode` reads from the AST.
|
|
241
|
+
*/
|
|
242
|
+
function setCalleeName(mod, newName) {
|
|
243
|
+
const ast = mod.exports.default?.$ast;
|
|
244
|
+
if (!ast || ast.callee?.type !== "Identifier") throw new Error("Cannot rename callee: default export is not a simple identifier call.");
|
|
245
|
+
ast.callee.name = newName;
|
|
246
|
+
}
|
|
247
|
+
const tsdown = definePlugin(() => ({
|
|
248
|
+
name: "tsdown",
|
|
249
|
+
apiVersion: 1,
|
|
250
|
+
install,
|
|
251
|
+
uninstall,
|
|
252
|
+
async setup({ shell }) {
|
|
253
|
+
const svc = new TsdownService(shell);
|
|
254
|
+
try {
|
|
255
|
+
await svc.getBinDir();
|
|
256
|
+
} catch (_err) {
|
|
257
|
+
throw new Error("@rrlab/tsdown-plugin requires tsdown to be installed in the host project. Run: rr plugins add tsdown (or: pnpm add -D tsdown)");
|
|
258
|
+
}
|
|
259
|
+
return { pack: svc };
|
|
260
|
+
}
|
|
261
|
+
}));
|
|
262
|
+
//#endregion
|
|
263
|
+
export { TOOL_VERSIONS, TsdownService, tsdown as default, install, uninstall };
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rrlab/tsdown-plugin",
|
|
3
|
+
"version": "0.0.1-git-06ed46c.0",
|
|
4
|
+
"description": "tsdown plugin for @rrlab/cli — provides the pack capability for building TS libraries.",
|
|
5
|
+
"homepage": "https://github.com/variableland/dx/tree/main/run-run/tsdown-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/tsdown-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
|
+
"magicast": "0.3.5",
|
|
37
|
+
"@vlandoss/clibuddy": "0.6.1"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"tsdown": ">=0.22.0",
|
|
41
|
+
"@rrlab/cli": "0.0.2-git-06ed46c.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"tsdown": "0.22.0",
|
|
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,324 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
definePlugin,
|
|
5
|
+
type InstallContext,
|
|
6
|
+
type InstallResult,
|
|
7
|
+
ToolService,
|
|
8
|
+
type UninstallContext,
|
|
9
|
+
type UninstallResult,
|
|
10
|
+
} from "@rrlab/cli/plugin";
|
|
11
|
+
import { colorize, type ShellService } from "@vlandoss/clibuddy";
|
|
12
|
+
import { generateCode, type ProxifiedModule, parseModule } from "magicast";
|
|
13
|
+
import { TOOL_VERSIONS } from "./tool-versions.ts";
|
|
14
|
+
|
|
15
|
+
export { TOOL_VERSIONS } from "./tool-versions.ts";
|
|
16
|
+
|
|
17
|
+
const FROM = import.meta.url;
|
|
18
|
+
const UI = colorize("#FF7E18")("tsdown");
|
|
19
|
+
const CONFIG_PKG = "@rrlab/tsdown-config";
|
|
20
|
+
const DEFAULT_CONFIG_FILENAME = "tsdown.config.ts";
|
|
21
|
+
const CONFIG_FILENAMES = [
|
|
22
|
+
"tsdown.config.ts",
|
|
23
|
+
"tsdown.config.mts",
|
|
24
|
+
"tsdown.config.cts",
|
|
25
|
+
"tsdown.config.js",
|
|
26
|
+
"tsdown.config.mjs",
|
|
27
|
+
"tsdown.config.cjs",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
type Preset = "lib" | "bin";
|
|
31
|
+
type FactoryName = "defineLibConfig" | "defineBinConfig";
|
|
32
|
+
|
|
33
|
+
type PresetInfo = {
|
|
34
|
+
factory: FactoryName;
|
|
35
|
+
label: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const PRESETS: Record<Preset, PresetInfo> = {
|
|
39
|
+
lib: { factory: "defineLibConfig", label: "Library (dts on, entry src/index.ts)" },
|
|
40
|
+
bin: { factory: "defineBinConfig", label: "CLI / Node binary (entry src/run.ts)" },
|
|
41
|
+
};
|
|
42
|
+
const DEFAULT_PRESET: Preset = "lib";
|
|
43
|
+
|
|
44
|
+
type ExistingFileAction = "patch" | "skip" | "overwrite";
|
|
45
|
+
|
|
46
|
+
export class TsdownService extends ToolService {
|
|
47
|
+
constructor(shellService: ShellService) {
|
|
48
|
+
super({ pkg: "tsdown", ui: UI, shellService, from: FROM });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async pack() {
|
|
52
|
+
await this.exec();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function install(ctx: InstallContext): Promise<InstallResult> {
|
|
57
|
+
const existingPath = await findExistingConfig(ctx.appPkg.dirPath);
|
|
58
|
+
const action = await decideScaffoldAction(ctx, existingPath);
|
|
59
|
+
if (action === "skip") {
|
|
60
|
+
return { devDependencies: { tsdown: TOOL_VERSIONS.tsdown.install } };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const preset = await pickPreset(ctx);
|
|
64
|
+
const { factory } = PRESETS[preset];
|
|
65
|
+
|
|
66
|
+
const devDependencies: Record<string, string> = {
|
|
67
|
+
tsdown: TOOL_VERSIONS.tsdown.install,
|
|
68
|
+
[CONFIG_PKG]: "^0.1.0",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (action === "create" || action === "overwrite") {
|
|
72
|
+
const relPath = existingPath ? path.relative(ctx.appPkg.dirPath, existingPath) : DEFAULT_CONFIG_FILENAME;
|
|
73
|
+
return {
|
|
74
|
+
devDependencies,
|
|
75
|
+
files: [
|
|
76
|
+
{
|
|
77
|
+
kind: "create",
|
|
78
|
+
path: relPath,
|
|
79
|
+
content: renderScaffold(factory),
|
|
80
|
+
overwrite: action === "overwrite" || ctx.flags.force,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// action === "patch": rewrite the existing config to use our factory.
|
|
87
|
+
const relPath = path.relative(ctx.appPkg.dirPath, existingPath as string);
|
|
88
|
+
return {
|
|
89
|
+
devDependencies,
|
|
90
|
+
files: [
|
|
91
|
+
{
|
|
92
|
+
kind: "edit-text",
|
|
93
|
+
path: relPath,
|
|
94
|
+
edit: (src) => patchToFactory(src, factory),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function uninstall(ctx: UninstallContext): Promise<UninstallResult> {
|
|
101
|
+
const removeDependencies = ["tsdown", CONFIG_PKG];
|
|
102
|
+
const existingPath = await findExistingConfig(ctx.appPkg.dirPath);
|
|
103
|
+
if (!existingPath) return { removeDependencies };
|
|
104
|
+
|
|
105
|
+
let source: string;
|
|
106
|
+
try {
|
|
107
|
+
source = await fs.readFile(existingPath, "utf8");
|
|
108
|
+
} catch {
|
|
109
|
+
return { removeDependencies };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let mod: ProxifiedModule;
|
|
113
|
+
try {
|
|
114
|
+
mod = parseModule(source);
|
|
115
|
+
} catch {
|
|
116
|
+
return { removeDependencies };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const scaffold = readScaffoldFactory(mod);
|
|
120
|
+
if (!scaffold) {
|
|
121
|
+
// Not a file we wrote — leave it alone, only drop deps.
|
|
122
|
+
return { removeDependencies };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const relPath = path.relative(ctx.appPkg.dirPath, existingPath);
|
|
126
|
+
if (!scaffold.hasArgs) {
|
|
127
|
+
// Pure scaffold (no user options) → safe to remove the file.
|
|
128
|
+
return { removeDependencies, files: [{ kind: "delete", path: relPath }] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The user added options after we scaffolded. Rewrite the file back to a
|
|
132
|
+
// bare `defineConfig` so they keep their options but the file no longer
|
|
133
|
+
// depends on the package we're removing.
|
|
134
|
+
return {
|
|
135
|
+
removeDependencies,
|
|
136
|
+
files: [{ kind: "edit-text", path: relPath, edit: (src) => patchBackToDefineConfig(src) }],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function findExistingConfig(cwd: string): Promise<string | null> {
|
|
141
|
+
for (const name of CONFIG_FILENAMES) {
|
|
142
|
+
const candidate = path.join(cwd, name);
|
|
143
|
+
try {
|
|
144
|
+
await fs.access(candidate);
|
|
145
|
+
return candidate;
|
|
146
|
+
} catch {
|
|
147
|
+
/* try next */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function decideScaffoldAction(ctx: InstallContext, existingPath: string | null): Promise<"create" | ExistingFileAction> {
|
|
154
|
+
if (!existingPath) {
|
|
155
|
+
if (ctx.flags.yes || ctx.flags.nonInteractive) return "create";
|
|
156
|
+
const choice = await ctx.prompts.confirm({
|
|
157
|
+
message: `Scaffold ${DEFAULT_CONFIG_FILENAME} from ${CONFIG_PKG}?`,
|
|
158
|
+
initialValue: true,
|
|
159
|
+
});
|
|
160
|
+
if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
|
|
161
|
+
return choice ? "create" : "skip";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Existing file: don't silently rewrite under --yes — that's user code.
|
|
165
|
+
if (ctx.flags.yes || ctx.flags.nonInteractive) return "skip";
|
|
166
|
+
|
|
167
|
+
const relPath = path.relative(ctx.appPkg.dirPath, existingPath);
|
|
168
|
+
const choice = await ctx.prompts.select<ExistingFileAction>({
|
|
169
|
+
message: `${relPath} already exists. What do you want to do?`,
|
|
170
|
+
options: [
|
|
171
|
+
{ value: "patch", label: `Patch — rewrite to use ${CONFIG_PKG}, keep my options` },
|
|
172
|
+
{ value: "skip", label: "Skip — leave it alone" },
|
|
173
|
+
{ value: "overwrite", label: "Overwrite — replace with a fresh scaffold" },
|
|
174
|
+
],
|
|
175
|
+
initialValue: "patch",
|
|
176
|
+
});
|
|
177
|
+
if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
|
|
178
|
+
return choice;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function pickPreset(ctx: InstallContext): Promise<Preset> {
|
|
182
|
+
if (ctx.flags.yes || ctx.flags.nonInteractive) return DEFAULT_PRESET;
|
|
183
|
+
|
|
184
|
+
const choice = await ctx.prompts.select<Preset>({
|
|
185
|
+
message: "Which kind of build?",
|
|
186
|
+
options: (Object.entries(PRESETS) as Array<[Preset, PresetInfo]>).map(([value, meta]) => ({
|
|
187
|
+
value,
|
|
188
|
+
label: meta.label,
|
|
189
|
+
})),
|
|
190
|
+
initialValue: DEFAULT_PRESET,
|
|
191
|
+
});
|
|
192
|
+
if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
|
|
193
|
+
return choice;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderScaffold(factory: FactoryName): string {
|
|
197
|
+
return `import { ${factory} } from "${CONFIG_PKG}";\n\nexport default ${factory}();\n`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Rewrites a user-owned `tsdown.config.*` to use one of our factories,
|
|
202
|
+
* preserving the call's arguments. Throws when the default export is not a
|
|
203
|
+
* direct call to `defineConfig` from `tsdown` or one of our factories — we
|
|
204
|
+
* refuse to mutate shapes we don't recognise.
|
|
205
|
+
*/
|
|
206
|
+
function patchToFactory(source: string, factory: FactoryName): string {
|
|
207
|
+
const mod = parseModule(source);
|
|
208
|
+
const def = readDefaultCall(mod);
|
|
209
|
+
if (!def) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Cannot patch tsdown config: default export is not a direct function call. Expected \`defineConfig(...)\` from "tsdown" or one of: ${ourFactoryList()}.`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (def.callee !== "defineConfig" && !isOurFactory(def.callee)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Cannot patch tsdown config: default export calls \`${def.callee}()\`. Expected \`defineConfig()\` from "tsdown" or one of: ${ourFactoryList()}.`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Swap import + callee. `defineConfig` came from "tsdown"; our factories
|
|
221
|
+
// come from "@rrlab/tsdown-config". Removing first guards against ending up
|
|
222
|
+
// with both bindings in scope when the user is switching presets.
|
|
223
|
+
removeImport(mod, def.callee);
|
|
224
|
+
addImport(mod, factory, CONFIG_PKG);
|
|
225
|
+
setCalleeName(mod, factory);
|
|
226
|
+
|
|
227
|
+
return generateCode(mod).code;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Reverses `patchToFactory`. Swaps our factory call + import back to
|
|
232
|
+
* `defineConfig` from "tsdown", preserving the user's arguments. Used by
|
|
233
|
+
* uninstall when the scaffolded file has been customised.
|
|
234
|
+
*/
|
|
235
|
+
function patchBackToDefineConfig(source: string): string {
|
|
236
|
+
const mod = parseModule(source);
|
|
237
|
+
const def = readDefaultCall(mod);
|
|
238
|
+
if (!def || !isOurFactory(def.callee)) {
|
|
239
|
+
return source;
|
|
240
|
+
}
|
|
241
|
+
removeImport(mod, def.callee);
|
|
242
|
+
addImport(mod, "defineConfig", "tsdown");
|
|
243
|
+
setCalleeName(mod, "defineConfig");
|
|
244
|
+
return generateCode(mod).code;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function readScaffoldFactory(mod: ProxifiedModule): { factory: FactoryName; hasArgs: boolean } | null {
|
|
248
|
+
const def = readDefaultCall(mod);
|
|
249
|
+
if (!def || !isOurFactory(def.callee)) return null;
|
|
250
|
+
const imp = mod.imports[def.callee];
|
|
251
|
+
if (!imp || imp.from !== CONFIG_PKG) return null;
|
|
252
|
+
return { factory: def.callee, hasArgs: def.hasArgs };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
type DefaultCallInfo = { callee: string; hasArgs: boolean };
|
|
256
|
+
|
|
257
|
+
function readDefaultCall(mod: ProxifiedModule): DefaultCallInfo | null {
|
|
258
|
+
// biome-ignore lint/suspicious/noExplicitAny: magicast proxies are opaque
|
|
259
|
+
const def = (mod.exports as any).default;
|
|
260
|
+
if (!def || def.$type !== "function-call") return null;
|
|
261
|
+
const callee = def.$callee;
|
|
262
|
+
if (typeof callee !== "string") return null;
|
|
263
|
+
// `$args` is a ProxifiedArray (not a real Array) — guard on `.length` not `Array.isArray`.
|
|
264
|
+
const args = def.$args;
|
|
265
|
+
return { callee, hasArgs: !!args && args.length > 0 };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isOurFactory(name: string): name is FactoryName {
|
|
269
|
+
return name === "defineLibConfig" || name === "defineBinConfig";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function ourFactoryList(): string {
|
|
273
|
+
return Object.values(PRESETS)
|
|
274
|
+
.map((p) => `\`${p.factory}\``)
|
|
275
|
+
.join(", ");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function removeImport(mod: ProxifiedModule, local: string): void {
|
|
279
|
+
if (mod.imports[local]) {
|
|
280
|
+
delete mod.imports[local];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function addImport(mod: ProxifiedModule, local: string, from: string): void {
|
|
285
|
+
if (!mod.imports[local]) {
|
|
286
|
+
mod.imports.$add({ from, imported: local, local });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Renames the default export's callee identifier in-place. magicast's
|
|
292
|
+
* `$callee` is a string snapshot taken at proxy creation, so we mutate the
|
|
293
|
+
* underlying AST node directly — `generateCode` reads from the AST.
|
|
294
|
+
*/
|
|
295
|
+
function setCalleeName(mod: ProxifiedModule, newName: string): void {
|
|
296
|
+
// biome-ignore lint/suspicious/noExplicitAny: magicast proxies expose $ast
|
|
297
|
+
const def = (mod.exports as any).default;
|
|
298
|
+
const ast = def?.$ast;
|
|
299
|
+
if (!ast || ast.callee?.type !== "Identifier") {
|
|
300
|
+
throw new Error("Cannot rename callee: default export is not a simple identifier call.");
|
|
301
|
+
}
|
|
302
|
+
ast.callee.name = newName;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const tsdown = definePlugin<void>(() => ({
|
|
306
|
+
name: "tsdown",
|
|
307
|
+
apiVersion: 1,
|
|
308
|
+
install,
|
|
309
|
+
uninstall,
|
|
310
|
+
async setup({ shell }) {
|
|
311
|
+
const svc = new TsdownService(shell);
|
|
312
|
+
try {
|
|
313
|
+
await svc.getBinDir();
|
|
314
|
+
} catch (_err) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
"@rrlab/tsdown-plugin requires tsdown to be installed in the host project. " +
|
|
317
|
+
"Run: rr plugins add tsdown (or: pnpm add -D tsdown)",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return { pack: svc };
|
|
321
|
+
},
|
|
322
|
+
}));
|
|
323
|
+
|
|
324
|
+
export default tsdown;
|