ts-shove 1.0.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/LICENSE +21 -0
- package/README.md +224 -0
- package/dist/chunk-ASGBQAOU.js +680 -0
- package/dist/chunk-MRSPJM6J.js +65 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +116 -0
- package/dist/mover.d.ts +17 -0
- package/dist/mover.js +7 -0
- package/dist/resolver.d.ts +7 -0
- package/dist/resolver.js +14 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alexander Fuerst
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# ts-shove
|
|
2
|
+
|
|
3
|
+
Move TypeScript files and directories at scale — all import paths are rewritten automatically via ts-morph AST analysis.
|
|
4
|
+
|
|
5
|
+
> "We restructured a 300+ file TypeScript codebase — 15 batch moves, 860 tests passing throughout, zero data loss. The tool paid for itself on the first task."
|
|
6
|
+
>
|
|
7
|
+
> — Claude, Senior Autonomous Refactoring Engineer at Anthropic
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add -D ts-shove
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or run directly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx ts-shove <source> <destination>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
### Move a single file
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ts-shove src/components/Button.tsx src/ui/Button.tsx
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Move a directory
|
|
30
|
+
|
|
31
|
+
Trailing slashes indicate a directory move. All files (TS and non-TS) are moved.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
ts-shove src/components/ src/ui/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Batch move from manifest
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
ts-shove moves.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Where `moves.json` contains:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"projectRoot": "/absolute/path/to/project",
|
|
48
|
+
"moves": {
|
|
49
|
+
"src/components/Button.tsx": "src/ui/Button.tsx",
|
|
50
|
+
"src/helpers/": "src/utils/"
|
|
51
|
+
},
|
|
52
|
+
"dryRun": false,
|
|
53
|
+
"useAliases": "preserve"
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| Field | Type | Default | Description |
|
|
58
|
+
|-------|------|---------|-------------|
|
|
59
|
+
| `projectRoot` | `string` | git root or cwd | Absolute path to project root |
|
|
60
|
+
| `moves` | `Record<string, string>` | (required) | Source-to-destination mapping. Trailing `/` = directory move |
|
|
61
|
+
| `dryRun` | `boolean` | `false` | Preview without writing changes; detects destination conflicts |
|
|
62
|
+
| `useAliases` | `"always" \| "never" \| "preserve"` | `"preserve"` | How to handle path alias imports |
|
|
63
|
+
|
|
64
|
+
## CLI options
|
|
65
|
+
|
|
66
|
+
| Flag | Short | Description |
|
|
67
|
+
|------|-------|-------------|
|
|
68
|
+
| `--dry-run` | `-n` | Preview changes without modifying files; detects destination conflicts |
|
|
69
|
+
| `--root <dir>` | `-r` | Project root (default: git root or cwd) |
|
|
70
|
+
| `--use-aliases <mode>` | `-a` | Alias handling: `always`, `never`, `preserve` (default: `preserve`) |
|
|
71
|
+
| `--help` | `-h` | Show usage information |
|
|
72
|
+
|
|
73
|
+
## Features
|
|
74
|
+
|
|
75
|
+
- Rewrites static imports (`import { x } from "./path"`)
|
|
76
|
+
- Rewrites dynamic imports (`import("./path")`, including inside `React.lazy()` callbacks)
|
|
77
|
+
- Rewrites `require()` calls -- `require("./foo")` with relative paths are rewritten, same as dynamic `import()`
|
|
78
|
+
- Rewrites re-exports (`export { x } from "./path"`)
|
|
79
|
+
- Rewrites side-effect imports (`import "./styles"`)
|
|
80
|
+
- Detects and preserves `.js`/`.jsx` extension convention in import specifiers
|
|
81
|
+
- Resolves and rewrites tsconfig path aliases (`@/components/...`)
|
|
82
|
+
- Supports both wildcard (`@/*`) and exact-match aliases
|
|
83
|
+
- Moves non-TS files (CSS, JSON, SVG, etc.) during directory moves
|
|
84
|
+
- Stages moves as git renames for clean history
|
|
85
|
+
- Falls back to plain file operations in non-git projects
|
|
86
|
+
- Protects against overwriting existing destination files
|
|
87
|
+
- Cleans up empty directories after moves
|
|
88
|
+
- Handles case-sensitive renames on case-insensitive filesystems (macOS)
|
|
89
|
+
- Supports batch moves with correct ordering for overlapping source/destination paths
|
|
90
|
+
|
|
91
|
+
## Alias handling
|
|
92
|
+
|
|
93
|
+
The `--use-aliases` flag controls how tsconfig path aliases are treated in import specifiers.
|
|
94
|
+
|
|
95
|
+
### `preserve` (default)
|
|
96
|
+
|
|
97
|
+
Alias imports stay as aliases. If the target file moved, the alias path is updated. If the new location can't be expressed as an alias, falls back to a relative path.
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
# Before: import { Button } from "@/components/Button"
|
|
101
|
+
# After: import { Button } from "@/ui/Button"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `always`
|
|
105
|
+
|
|
106
|
+
All relative imports in affected files are converted to aliases where possible.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
# Before: import { Button } from "../../ui/Button"
|
|
110
|
+
# After: import { Button } from "@/ui/Button"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `never`
|
|
114
|
+
|
|
115
|
+
All alias imports in affected files are converted to relative paths.
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
# Before: import { Button } from "@/ui/Button"
|
|
119
|
+
# After: import { Button } from "../../ui/Button"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Dry-run conflict detection
|
|
123
|
+
|
|
124
|
+
When `--dry-run` (or `dryRun: true` in the manifest) is used, ts-shove checks whether any destination path already exists on disk (and is not itself being moved away). Detected conflicts are reported in the console output and returned in the `MoveResult.conflicts` array. This lets you catch overwrites before any files are touched.
|
|
125
|
+
|
|
126
|
+
## Programmatic usage
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { executeMoves, type MoveManifest, type MoveResult } from "ts-shove";
|
|
130
|
+
|
|
131
|
+
const result: MoveResult = executeMoves({
|
|
132
|
+
projectRoot: "/path/to/project",
|
|
133
|
+
moves: {
|
|
134
|
+
"src/old.ts": "src/new.ts",
|
|
135
|
+
"src/old-dir/": "src/new-dir/",
|
|
136
|
+
},
|
|
137
|
+
useAliases: "preserve",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(`Moved ${result.filesMoved} files, rewrote ${result.importsRewritten} imports`);
|
|
141
|
+
|
|
142
|
+
if (result.conflicts?.length) {
|
|
143
|
+
console.warn("Destination conflicts:", result.conflicts);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## What it does NOT handle
|
|
148
|
+
|
|
149
|
+
- **CSS/SCSS import paths** -- `@import` in stylesheets is not touched.
|
|
150
|
+
- **Template literal imports** -- `` import(`./locale/${lang}`) `` is skipped (non-string-literal argument).
|
|
151
|
+
- **Runtime string paths** -- Dynamically constructed import paths cannot be statically analyzed.
|
|
152
|
+
- **Non-project files** -- Only files included in `tsconfig.json` are analyzed for import rewriting.
|
|
153
|
+
- **Git rename tracking** -- Git detects renames by content similarity. When a move rewrites many import lines, the file content may change enough that git no longer recognizes it as a rename, and `git log --follow` won't track the history. This is a fundamental git limitation. For critical files, consider committing the rename and import rewrites as separate steps.
|
|
154
|
+
|
|
155
|
+
## tsconfig and test files
|
|
156
|
+
|
|
157
|
+
ts-shove uses ts-morph, which loads the project via `tsconfig.json`. **Only files included by tsconfig are analyzed for import rewriting.** If your tsconfig excludes test files (e.g., `"exclude": ["**/*.test.ts"]`), those files' imports will NOT be updated when you move their dependencies.
|
|
158
|
+
|
|
159
|
+
If you try to move a file that is excluded from tsconfig, ts-shove will throw an early error:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
Error: Source file is not included in tsconfig: src/utils.test.ts
|
|
163
|
+
ts-morph cannot rewrite imports for files outside the project.
|
|
164
|
+
Either add it to tsconfig "include" or use --tsconfig to specify a broader config.
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Recommended approach:** Use a tsconfig that includes your test files. Many projects already have a `tsconfig.json` that includes everything and a separate `tsconfig.build.json` for compilation. Point ts-shove at the broader one:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
ts-shove --tsconfig tsconfig.json src/old.ts src/new.ts
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Or if your main `tsconfig.json` excludes tests, create a `tsconfig.check.json`:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"extends": "./tsconfig.json",
|
|
178
|
+
"exclude": []
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
After a move, ts-shove reports stale-path warnings for string literals (like `vi.mock("./old/path")`) that match moved files but couldn't be automatically rewritten. Review these manually.
|
|
183
|
+
|
|
184
|
+
## How it works
|
|
185
|
+
|
|
186
|
+
1. **Expand moves** -- Directory moves are expanded to individual file mappings. Source paths are validated.
|
|
187
|
+
2. **Move TS files in-memory** -- ts-morph moves each source file to its destination, automatically rewriting relative imports across the entire project.
|
|
188
|
+
3. **Fix .js extensions** -- If the project convention uses `.js` extensions in import specifiers, they are restored (ts-morph strips them).
|
|
189
|
+
4. **Fix dynamic imports, `require()` calls, and side-effect imports** -- ts-morph does not handle `import()` expressions, `require()` calls, or imports with no bindings. A separate AST pass rewrites these.
|
|
190
|
+
5. **Handle aliases** -- Alias imports pointing to moved files are updated according to the chosen mode.
|
|
191
|
+
6. **Write and clean up** -- Changes are saved to disk. Non-TS files are copied. Original files are deleted (via `git rm` when possible). Empty directories are removed.
|
|
192
|
+
|
|
193
|
+
## Development
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Install dependencies
|
|
197
|
+
pnpm install
|
|
198
|
+
|
|
199
|
+
# Run tests
|
|
200
|
+
pnpm test
|
|
201
|
+
|
|
202
|
+
# Run tests in watch mode
|
|
203
|
+
pnpm test:watch
|
|
204
|
+
|
|
205
|
+
# Build
|
|
206
|
+
pnpm build
|
|
207
|
+
|
|
208
|
+
# Run in development (without building)
|
|
209
|
+
pnpm dev -- <source> <destination>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Requires Node.js 20+ and a `tsconfig.json` at the project root.
|
|
213
|
+
|
|
214
|
+
## Platform support
|
|
215
|
+
|
|
216
|
+
Tested on macOS and Linux (via CI). Windows is expected to work (all path handling uses Node.js `path` module) but has not been tested. If you encounter Windows-specific issues, please [open an issue](https://github.com/fuerst-one/ts-shove/issues).
|
|
217
|
+
|
|
218
|
+
## Credits
|
|
219
|
+
|
|
220
|
+
Built on [ts-morph](https://github.com/dsherret/ts-morph) by David Sherret — the TypeScript AST library that makes all the import rewriting possible.
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT
|
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findAllFiles,
|
|
3
|
+
findTsFiles,
|
|
4
|
+
resolveCandidates
|
|
5
|
+
} from "./chunk-MRSPJM6J.js";
|
|
6
|
+
|
|
7
|
+
// src/mover.ts
|
|
8
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import { execFileSync } from "child_process";
|
|
12
|
+
var CASE_RENAME_TEMP_SUFFIX = ".__ts_mv_tmp__";
|
|
13
|
+
function expandMoves(moves, projectRoot) {
|
|
14
|
+
const expanded = /* @__PURE__ */ new Map();
|
|
15
|
+
for (const [src, dest] of Object.entries(moves)) {
|
|
16
|
+
const absSrc = path.resolve(projectRoot, src);
|
|
17
|
+
const absDest = path.resolve(projectRoot, dest);
|
|
18
|
+
if (!absSrc.startsWith(projectRoot + path.sep) && absSrc !== projectRoot) {
|
|
19
|
+
throw new Error(`Source path is outside project root: ${src}`);
|
|
20
|
+
}
|
|
21
|
+
if (!absDest.startsWith(projectRoot + path.sep) && absDest !== projectRoot) {
|
|
22
|
+
throw new Error(`Destination path is outside project root: ${dest}`);
|
|
23
|
+
}
|
|
24
|
+
if (src.endsWith("/")) {
|
|
25
|
+
if (!fs.existsSync(absSrc)) {
|
|
26
|
+
throw new Error(`Source directory does not exist: ${src}`);
|
|
27
|
+
}
|
|
28
|
+
for (const file of findAllFiles(absSrc)) {
|
|
29
|
+
const relative2 = path.relative(absSrc, file);
|
|
30
|
+
expanded.set(file, path.join(absDest, relative2));
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
if (!fs.existsSync(absSrc)) {
|
|
34
|
+
throw new Error(`Source file does not exist: ${src}`);
|
|
35
|
+
}
|
|
36
|
+
expanded.set(absSrc, absDest);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return expanded;
|
|
40
|
+
}
|
|
41
|
+
function detectJsExtensionConvention(project) {
|
|
42
|
+
const sourceFiles = project.getSourceFiles().slice(0, 30);
|
|
43
|
+
let jsExtCount = 0;
|
|
44
|
+
let totalImports = 0;
|
|
45
|
+
for (const sf of sourceFiles) {
|
|
46
|
+
for (const decl of sf.getImportDeclarations()) {
|
|
47
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
48
|
+
if (specifier.startsWith(".")) {
|
|
49
|
+
totalImports++;
|
|
50
|
+
if (/\.jsx?$/.test(specifier)) jsExtCount++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return totalImports > 0 && jsExtCount / totalImports > 0.5;
|
|
55
|
+
}
|
|
56
|
+
function isModuleSpecifierCall(call) {
|
|
57
|
+
const expr = call.getExpression();
|
|
58
|
+
if (expr.getKind() === SyntaxKind.ImportKeyword) return true;
|
|
59
|
+
if (expr.getKind() === SyntaxKind.Identifier && expr.getText() === "require") return true;
|
|
60
|
+
const arg = getCallStringArg(call);
|
|
61
|
+
if (!arg) return false;
|
|
62
|
+
const val = arg.getLiteralValue();
|
|
63
|
+
return val.startsWith("./") || val.startsWith("../");
|
|
64
|
+
}
|
|
65
|
+
function getCallStringArg(call) {
|
|
66
|
+
const args = call.getArguments();
|
|
67
|
+
if (args.length === 0) return null;
|
|
68
|
+
const arg = args[0];
|
|
69
|
+
if (arg.getKind() !== SyntaxKind.StringLiteral) return null;
|
|
70
|
+
return arg.asKind(SyntaxKind.StringLiteral);
|
|
71
|
+
}
|
|
72
|
+
function isExplicitIndexImport(specifier) {
|
|
73
|
+
return /\/index$/.test(specifier) || specifier === "./index" || specifier === "../index";
|
|
74
|
+
}
|
|
75
|
+
function fixSpecifierExtension(literal, sf, project) {
|
|
76
|
+
const specifier = literal.getLiteralValue();
|
|
77
|
+
if (!specifier.startsWith(".")) return;
|
|
78
|
+
if (specifier.endsWith(".js") || specifier.endsWith(".jsx")) return;
|
|
79
|
+
if (/\.[a-z]+$/i.test(specifier) && !/\.tsx?$/i.test(specifier)) return;
|
|
80
|
+
const isExplicitIndex = isExplicitIndexImport(specifier);
|
|
81
|
+
let fixedSpecifier = isExplicitIndex ? specifier : specifier.replace(/\/index$/, "");
|
|
82
|
+
const dir = path.dirname(sf.getFilePath());
|
|
83
|
+
const resolved = path.resolve(dir, fixedSpecifier);
|
|
84
|
+
if (!isExplicitIndex) {
|
|
85
|
+
const indexCandidates = [path.join(resolved, "index.ts"), path.join(resolved, "index.tsx")];
|
|
86
|
+
if (indexCandidates.some((c) => project.getSourceFile(c))) return;
|
|
87
|
+
}
|
|
88
|
+
const tsxCandidates = [resolved + ".tsx", path.join(resolved, "index.tsx")];
|
|
89
|
+
const isTsx = tsxCandidates.some((c) => project.getSourceFile(c));
|
|
90
|
+
literal.setLiteralValue(fixedSpecifier + (isTsx ? ".jsx" : ".js"));
|
|
91
|
+
}
|
|
92
|
+
function buildRelativeSpecifier(fromFilePath, targetFilePath, usesJsExt) {
|
|
93
|
+
let rel = path.relative(path.dirname(fromFilePath), targetFilePath);
|
|
94
|
+
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
95
|
+
rel = rel.replace(/\.tsx?$/, "");
|
|
96
|
+
rel = rel.replace(/\/index$/, "");
|
|
97
|
+
if (usesJsExt) rel += targetFilePath.endsWith(".tsx") ? ".jsx" : ".js";
|
|
98
|
+
return rel;
|
|
99
|
+
}
|
|
100
|
+
function fixJsExtensions(project) {
|
|
101
|
+
for (const sf of project.getSourceFiles()) {
|
|
102
|
+
const declarations = [
|
|
103
|
+
...sf.getImportDeclarations(),
|
|
104
|
+
...sf.getExportDeclarations().filter((d) => d.getModuleSpecifierValue())
|
|
105
|
+
];
|
|
106
|
+
for (const decl of declarations) {
|
|
107
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
108
|
+
if (!specifier || !specifier.startsWith(".")) continue;
|
|
109
|
+
if (specifier.endsWith(".js") || specifier.endsWith(".jsx")) continue;
|
|
110
|
+
if (/\.[a-z]+$/i.test(specifier) && !/\.tsx?$/i.test(specifier)) continue;
|
|
111
|
+
const resolved = decl.getModuleSpecifierSourceFile();
|
|
112
|
+
if (resolved && /\/index\.tsx?$/.test(resolved.getFilePath()) && !isExplicitIndexImport(specifier)) continue;
|
|
113
|
+
const ext = resolved && /\.tsx$/.test(resolved.getFilePath()) ? ".jsx" : ".js";
|
|
114
|
+
decl.setModuleSpecifier(specifier + ext);
|
|
115
|
+
}
|
|
116
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
117
|
+
if (!isModuleSpecifierCall(call)) return;
|
|
118
|
+
const literal = getCallStringArg(call);
|
|
119
|
+
if (!literal) return;
|
|
120
|
+
fixSpecifierExtension(literal, sf, project);
|
|
121
|
+
});
|
|
122
|
+
sf.getDescendantsOfKind(SyntaxKind.ImportType).forEach((importType) => {
|
|
123
|
+
const argument = importType.getArgument();
|
|
124
|
+
const literals = argument.getDescendantsOfKind(SyntaxKind.StringLiteral);
|
|
125
|
+
if (literals.length === 0) return;
|
|
126
|
+
const literal = literals[0];
|
|
127
|
+
fixSpecifierExtension(literal, sf, project);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function buildCallRewriteContext(moveMap, destPaths, aliasMappings, useAliases) {
|
|
132
|
+
const reverseMap = /* @__PURE__ */ new Map();
|
|
133
|
+
for (const [src, dest] of moveMap) {
|
|
134
|
+
reverseMap.set(dest, src);
|
|
135
|
+
}
|
|
136
|
+
return { reverseMap, destPaths, aliasMappings, useAliases };
|
|
137
|
+
}
|
|
138
|
+
function rewriteCallSpecifier(literal, filePath, originalFilePath, moveMap, destPaths, project, usesJsExt, aliasMappings, useAliases) {
|
|
139
|
+
const specifier = literal.getLiteralValue();
|
|
140
|
+
const isRelative = specifier.startsWith(".");
|
|
141
|
+
let resolvedBase = null;
|
|
142
|
+
if (isRelative) {
|
|
143
|
+
const dir = path.dirname(originalFilePath);
|
|
144
|
+
resolvedBase = path.resolve(dir, specifier).replace(/\.jsx?$/, "");
|
|
145
|
+
} else if (aliasMappings && aliasMappings.length > 0) {
|
|
146
|
+
resolvedBase = resolveAliasToAbsolute(specifier, aliasMappings);
|
|
147
|
+
}
|
|
148
|
+
if (!resolvedBase) return false;
|
|
149
|
+
const candidates = resolveCandidates(resolvedBase);
|
|
150
|
+
let targetPath = null;
|
|
151
|
+
for (const c of candidates) {
|
|
152
|
+
if (moveMap.has(c) || project.getSourceFile(c) || destPaths.has(c)) {
|
|
153
|
+
targetPath = c;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!targetPath) return false;
|
|
158
|
+
const newTargetPath = moveMap.get(targetPath) ?? targetPath;
|
|
159
|
+
if (newTargetPath === targetPath && filePath === originalFilePath) return false;
|
|
160
|
+
let newSpecifier = null;
|
|
161
|
+
if (aliasMappings && aliasMappings.length > 0) {
|
|
162
|
+
const mode = useAliases ?? "preserve";
|
|
163
|
+
if (mode === "always" || mode === "preserve" && !isRelative) {
|
|
164
|
+
newSpecifier = absolutePathToAlias(newTargetPath, aliasMappings, usesJsExt);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!newSpecifier) {
|
|
168
|
+
newSpecifier = buildRelativeSpecifier(filePath, newTargetPath, usesJsExt);
|
|
169
|
+
}
|
|
170
|
+
if (newSpecifier !== specifier) {
|
|
171
|
+
literal.setLiteralValue(newSpecifier);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
function fixCallExpressions(project, moveMap, usesJsExt, ctx) {
|
|
177
|
+
let count = 0;
|
|
178
|
+
for (const sf of project.getSourceFiles()) {
|
|
179
|
+
const filePath = sf.getFilePath();
|
|
180
|
+
const originalFilePath = ctx.reverseMap.get(filePath) ?? filePath;
|
|
181
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
182
|
+
if (!isModuleSpecifierCall(call)) return;
|
|
183
|
+
const literal = getCallStringArg(call);
|
|
184
|
+
if (!literal) return;
|
|
185
|
+
if (rewriteCallSpecifier(literal, filePath, originalFilePath, moveMap, ctx.destPaths, project, usesJsExt, ctx.aliasMappings, ctx.useAliases)) {
|
|
186
|
+
count++;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
sf.getDescendantsOfKind(SyntaxKind.ImportType).forEach((importType) => {
|
|
190
|
+
const argument = importType.getArgument();
|
|
191
|
+
const literals = argument.getDescendantsOfKind(SyntaxKind.StringLiteral);
|
|
192
|
+
if (literals.length === 0) return;
|
|
193
|
+
const literal = literals[0];
|
|
194
|
+
if (rewriteCallSpecifier(literal, filePath, originalFilePath, moveMap, ctx.destPaths, project, usesJsExt, ctx.aliasMappings, ctx.useAliases)) {
|
|
195
|
+
count++;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return count;
|
|
200
|
+
}
|
|
201
|
+
function fixSideEffectImports(project, moveMap, usesJsExt) {
|
|
202
|
+
let count = 0;
|
|
203
|
+
for (const sf of project.getSourceFiles()) {
|
|
204
|
+
const filePath = sf.getFilePath();
|
|
205
|
+
for (const decl of sf.getImportDeclarations()) {
|
|
206
|
+
if (decl.getNamedImports().length > 0) continue;
|
|
207
|
+
if (decl.getDefaultImport()) continue;
|
|
208
|
+
if (decl.getNamespaceImport()) continue;
|
|
209
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
210
|
+
if (!specifier.startsWith(".")) continue;
|
|
211
|
+
const dir = path.dirname(filePath);
|
|
212
|
+
let resolved = path.resolve(dir, specifier).replace(/\.jsx?$/, "");
|
|
213
|
+
const candidates = resolveCandidates(resolved);
|
|
214
|
+
let targetPath = null;
|
|
215
|
+
for (const c of candidates) {
|
|
216
|
+
if (moveMap.has(c)) {
|
|
217
|
+
targetPath = c;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!targetPath) continue;
|
|
222
|
+
const newTargetPath = moveMap.get(targetPath);
|
|
223
|
+
if (newTargetPath === targetPath) continue;
|
|
224
|
+
const newRel = buildRelativeSpecifier(filePath, newTargetPath, usesJsExt);
|
|
225
|
+
if (newRel !== specifier) {
|
|
226
|
+
decl.setModuleSpecifier(newRel);
|
|
227
|
+
count++;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return count;
|
|
232
|
+
}
|
|
233
|
+
function parseAliasMappings(project, projectRoot) {
|
|
234
|
+
const compilerOptions = project.getCompilerOptions();
|
|
235
|
+
const configPaths = compilerOptions.paths;
|
|
236
|
+
if (!configPaths) return [];
|
|
237
|
+
const baseUrl = compilerOptions.baseUrl ?? projectRoot;
|
|
238
|
+
const mappings = [];
|
|
239
|
+
for (const [pattern, targets] of Object.entries(configPaths)) {
|
|
240
|
+
if (targets.length === 0) continue;
|
|
241
|
+
if (pattern.endsWith("/*")) {
|
|
242
|
+
const prefix = pattern.slice(0, -1);
|
|
243
|
+
const target = targets[0];
|
|
244
|
+
const base = target.endsWith("/*") ? target.slice(0, -2) : target;
|
|
245
|
+
mappings.push({ prefix, baseDir: path.resolve(baseUrl, base) });
|
|
246
|
+
} else {
|
|
247
|
+
const target = targets[0];
|
|
248
|
+
mappings.push({ prefix: pattern, baseDir: path.resolve(baseUrl, target), isExact: true });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return mappings.sort((a, b) => b.baseDir.length - a.baseDir.length);
|
|
252
|
+
}
|
|
253
|
+
function absolutePathToAlias(absPath, mappings, usesJsExt) {
|
|
254
|
+
for (const mapping of mappings) {
|
|
255
|
+
if (mapping.isExact) {
|
|
256
|
+
const target = mapping.baseDir.replace(/\.tsx?$/, "");
|
|
257
|
+
const absNoExt = absPath.replace(/\.tsx?$/, "");
|
|
258
|
+
if (absNoExt === target) {
|
|
259
|
+
let specifier2 = mapping.prefix;
|
|
260
|
+
if (usesJsExt) specifier2 += absPath.endsWith(".tsx") ? ".jsx" : ".js";
|
|
261
|
+
return specifier2;
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const rel = path.relative(mapping.baseDir, absPath);
|
|
266
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
|
|
267
|
+
let specifier = mapping.prefix + rel.replace(/\.tsx?$/, "");
|
|
268
|
+
if (usesJsExt) specifier += absPath.endsWith(".tsx") ? ".jsx" : ".js";
|
|
269
|
+
return specifier;
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
function resolveAliasToAbsolute(specifier, mappings) {
|
|
274
|
+
const bare = specifier.replace(/\.jsx?$/, "");
|
|
275
|
+
for (const mapping of mappings) {
|
|
276
|
+
if (mapping.isExact) {
|
|
277
|
+
if (bare === mapping.prefix) {
|
|
278
|
+
return mapping.baseDir.replace(/\.tsx?$/, "");
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (!bare.startsWith(mapping.prefix)) continue;
|
|
283
|
+
const remainder = bare.slice(mapping.prefix.length);
|
|
284
|
+
return path.resolve(mapping.baseDir, remainder);
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
function handleAliases(project, moveMap, involvedFiles, projectRoot, usesJsExt, mode) {
|
|
289
|
+
const mappings = parseAliasMappings(project, projectRoot);
|
|
290
|
+
if (mappings.length === 0) return 0;
|
|
291
|
+
let count = 0;
|
|
292
|
+
for (const sf of project.getSourceFiles()) {
|
|
293
|
+
const filePath = sf.getFilePath();
|
|
294
|
+
if (!involvedFiles.has(filePath)) continue;
|
|
295
|
+
const declarations = [
|
|
296
|
+
...sf.getImportDeclarations(),
|
|
297
|
+
...sf.getExportDeclarations().filter((d) => d.getModuleSpecifierValue())
|
|
298
|
+
];
|
|
299
|
+
for (const decl of declarations) {
|
|
300
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
301
|
+
if (!specifier) continue;
|
|
302
|
+
const isAlias = !specifier.startsWith(".");
|
|
303
|
+
if (isAlias) {
|
|
304
|
+
if (mode === "never") {
|
|
305
|
+
const resolved = resolveAliasToAbsolute(specifier, mappings);
|
|
306
|
+
if (!resolved) continue;
|
|
307
|
+
const candidates = resolveCandidates(resolved);
|
|
308
|
+
let targetFile = null;
|
|
309
|
+
for (const c of candidates) {
|
|
310
|
+
const newC = moveMap.get(c) ?? c;
|
|
311
|
+
if (project.getSourceFile(newC)) {
|
|
312
|
+
targetFile = newC;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (!targetFile) continue;
|
|
317
|
+
const relPath = buildRelativeSpecifier(filePath, targetFile, usesJsExt);
|
|
318
|
+
decl.setModuleSpecifier(relPath);
|
|
319
|
+
count++;
|
|
320
|
+
} else {
|
|
321
|
+
const resolved = resolveAliasToAbsolute(specifier, mappings);
|
|
322
|
+
if (!resolved) continue;
|
|
323
|
+
const candidates = resolveCandidates(resolved);
|
|
324
|
+
for (const candidate of candidates) {
|
|
325
|
+
const newPath = moveMap.get(candidate);
|
|
326
|
+
if (!newPath) continue;
|
|
327
|
+
const newAlias = absolutePathToAlias(newPath, mappings, usesJsExt);
|
|
328
|
+
if (newAlias) {
|
|
329
|
+
decl.setModuleSpecifier(newAlias);
|
|
330
|
+
} else {
|
|
331
|
+
const relPath = buildRelativeSpecifier(filePath, newPath, usesJsExt);
|
|
332
|
+
decl.setModuleSpecifier(relPath);
|
|
333
|
+
}
|
|
334
|
+
count++;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} else if (!isAlias && mode === "always") {
|
|
339
|
+
const resolved = decl.getModuleSpecifierSourceFile();
|
|
340
|
+
if (!resolved) continue;
|
|
341
|
+
const targetPath = resolved.getFilePath();
|
|
342
|
+
const alias = absolutePathToAlias(targetPath, mappings, usesJsExt);
|
|
343
|
+
if (alias) {
|
|
344
|
+
decl.setModuleSpecifier(alias);
|
|
345
|
+
count++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return count;
|
|
351
|
+
}
|
|
352
|
+
function detectStaleReferences(moveMap, projectRoot) {
|
|
353
|
+
const warnings = [];
|
|
354
|
+
const movedStems = /* @__PURE__ */ new Map();
|
|
355
|
+
for (const [src, dest] of moveMap) {
|
|
356
|
+
if (src === dest) continue;
|
|
357
|
+
const relSrc = path.relative(projectRoot, src).replace(/\.tsx?$/, "");
|
|
358
|
+
const relDest = path.relative(projectRoot, dest);
|
|
359
|
+
movedStems.set(relSrc, relDest);
|
|
360
|
+
}
|
|
361
|
+
if (movedStems.size === 0) return warnings;
|
|
362
|
+
const allFiles = findTsFiles(projectRoot);
|
|
363
|
+
for (const file of allFiles) {
|
|
364
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
365
|
+
const relFile = path.relative(projectRoot, file);
|
|
366
|
+
for (const [oldStem, newDest] of movedStems) {
|
|
367
|
+
const fileDir = path.dirname(file);
|
|
368
|
+
const oldAbs = path.resolve(projectRoot, oldStem);
|
|
369
|
+
let oldRel = path.relative(fileDir, oldAbs);
|
|
370
|
+
if (!oldRel.startsWith(".")) oldRel = "./" + oldRel;
|
|
371
|
+
const patterns = [oldRel, oldRel + ".js", oldRel + ".jsx"];
|
|
372
|
+
for (const pattern of patterns) {
|
|
373
|
+
const lines = content.split("\n");
|
|
374
|
+
for (let i = 0; i < lines.length; i++) {
|
|
375
|
+
const line = lines[i];
|
|
376
|
+
if (line.match(/\bfrom\s+["']/) && line.match(/^\s*(import|export)\s/)) continue;
|
|
377
|
+
if (line.match(/^\s*import\s+["']/)) continue;
|
|
378
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
379
|
+
const regex = new RegExp(`["']${escaped}["']`);
|
|
380
|
+
if (regex.test(line)) {
|
|
381
|
+
warnings.push(`${relFile}:${i + 1}: possible stale reference "${pattern}" (moved to ${newDest})`);
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return warnings;
|
|
389
|
+
}
|
|
390
|
+
function isGitRepo(dir) {
|
|
391
|
+
try {
|
|
392
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir, stdio: "pipe" });
|
|
393
|
+
return true;
|
|
394
|
+
} catch {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function checkDryRunConflicts(moveMap, projectRoot) {
|
|
399
|
+
const conflicts = [];
|
|
400
|
+
for (const [src, dest] of moveMap) {
|
|
401
|
+
if (src === dest) continue;
|
|
402
|
+
if (fs.existsSync(dest) && !moveMap.has(dest)) {
|
|
403
|
+
conflicts.push(dest);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (conflicts.length > 0) {
|
|
407
|
+
console.log(`
|
|
408
|
+
Conflicts detected: ${conflicts.length} destination(s) already exist`);
|
|
409
|
+
for (const c of conflicts) {
|
|
410
|
+
console.log(` conflict: ${path.relative(projectRoot, c)}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return conflicts;
|
|
414
|
+
}
|
|
415
|
+
function performTopologicalSort(moveEntries, destPaths) {
|
|
416
|
+
const srcToIdx = /* @__PURE__ */ new Map();
|
|
417
|
+
for (let i = 0; i < moveEntries.length; i++) {
|
|
418
|
+
srcToIdx.set(moveEntries[i][0], i);
|
|
419
|
+
}
|
|
420
|
+
const sorted = [];
|
|
421
|
+
const visited = /* @__PURE__ */ new Set();
|
|
422
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
423
|
+
function visit(idx) {
|
|
424
|
+
if (visited.has(idx)) return;
|
|
425
|
+
if (visiting.has(idx)) return;
|
|
426
|
+
visiting.add(idx);
|
|
427
|
+
const [, dest] = moveEntries[idx];
|
|
428
|
+
const depIdx = srcToIdx.get(dest);
|
|
429
|
+
if (depIdx !== void 0) {
|
|
430
|
+
visit(depIdx);
|
|
431
|
+
}
|
|
432
|
+
visiting.delete(idx);
|
|
433
|
+
visited.add(idx);
|
|
434
|
+
sorted.push(moveEntries[idx]);
|
|
435
|
+
}
|
|
436
|
+
for (let i = 0; i < moveEntries.length; i++) {
|
|
437
|
+
visit(i);
|
|
438
|
+
}
|
|
439
|
+
return sorted;
|
|
440
|
+
}
|
|
441
|
+
function stripIntroducedIndexSuffixes(project, preMoveCallSpecifiers, moveMap) {
|
|
442
|
+
const reverseForIndex = /* @__PURE__ */ new Map();
|
|
443
|
+
for (const [src, dest] of moveMap) reverseForIndex.set(dest, src);
|
|
444
|
+
for (const sf of project.getSourceFiles()) {
|
|
445
|
+
const filePath = sf.getFilePath();
|
|
446
|
+
const originalPath = reverseForIndex.get(filePath) ?? filePath;
|
|
447
|
+
const originalSpecs = preMoveCallSpecifiers.get(originalPath);
|
|
448
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
449
|
+
if (!isModuleSpecifierCall(call)) return;
|
|
450
|
+
const literal = getCallStringArg(call);
|
|
451
|
+
if (!literal) return;
|
|
452
|
+
const val = literal.getLiteralValue();
|
|
453
|
+
if (!(val.endsWith("/index") || val.endsWith("/index.js") || val.endsWith("/index.jsx"))) return;
|
|
454
|
+
if (originalSpecs) {
|
|
455
|
+
const stripped = val.replace(/\/index(\.jsx?)?$/, "$1");
|
|
456
|
+
if (originalSpecs.has(val)) return;
|
|
457
|
+
if (originalSpecs.has(stripped) || !originalSpecs.has(val)) {
|
|
458
|
+
literal.setLiteralValue(stripped);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function collectInvolvedFiles(project, moveMap, aliasMappings, isTsFile) {
|
|
465
|
+
const involvedFiles = /* @__PURE__ */ new Set();
|
|
466
|
+
for (const dest of moveMap.values()) {
|
|
467
|
+
if (isTsFile(dest)) involvedFiles.add(dest);
|
|
468
|
+
}
|
|
469
|
+
for (const sf of project.getSourceFiles()) {
|
|
470
|
+
const filePath = sf.getFilePath();
|
|
471
|
+
if (involvedFiles.has(filePath)) continue;
|
|
472
|
+
let isInvolved = false;
|
|
473
|
+
const allDecls = [
|
|
474
|
+
...sf.getImportDeclarations(),
|
|
475
|
+
...sf.getExportDeclarations().filter((d) => d.getModuleSpecifierValue())
|
|
476
|
+
];
|
|
477
|
+
for (const decl of allDecls) {
|
|
478
|
+
const target = decl.getModuleSpecifierSourceFile();
|
|
479
|
+
if (target && involvedFiles.has(target.getFilePath())) {
|
|
480
|
+
isInvolved = true;
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
484
|
+
if (specifier && !specifier.startsWith(".") && aliasMappings.length > 0) {
|
|
485
|
+
const resolved = resolveAliasToAbsolute(specifier, aliasMappings);
|
|
486
|
+
if (resolved) {
|
|
487
|
+
const candidates = resolveCandidates(resolved);
|
|
488
|
+
if (candidates.some((c) => moveMap.has(c))) {
|
|
489
|
+
isInvolved = true;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (isInvolved) involvedFiles.add(filePath);
|
|
496
|
+
}
|
|
497
|
+
return involvedFiles;
|
|
498
|
+
}
|
|
499
|
+
function commitMovesToDisk(moveMap, projectRoot, isTsFile, destPaths) {
|
|
500
|
+
let movedCount = 0;
|
|
501
|
+
for (const [src, dest] of moveMap) {
|
|
502
|
+
if (isTsFile(src)) continue;
|
|
503
|
+
if (src === dest) continue;
|
|
504
|
+
const destDir = path.dirname(dest);
|
|
505
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
506
|
+
if (fs.existsSync(dest)) {
|
|
507
|
+
throw new Error(`Destination file already exists: ${dest}`);
|
|
508
|
+
}
|
|
509
|
+
fs.copyFileSync(src, dest);
|
|
510
|
+
movedCount++;
|
|
511
|
+
}
|
|
512
|
+
const useGit = isGitRepo(projectRoot);
|
|
513
|
+
for (const [src, dest] of moveMap) {
|
|
514
|
+
if (src === dest) continue;
|
|
515
|
+
const isCaseRename = src.toLowerCase() === dest.toLowerCase();
|
|
516
|
+
if (isCaseRename) continue;
|
|
517
|
+
if (destPaths.has(src)) continue;
|
|
518
|
+
if (fs.existsSync(src)) {
|
|
519
|
+
if (useGit) {
|
|
520
|
+
try {
|
|
521
|
+
execFileSync("git", ["add", dest], { cwd: projectRoot, stdio: "pipe" });
|
|
522
|
+
execFileSync("git", ["rm", "--quiet", src], { cwd: projectRoot, stdio: "pipe" });
|
|
523
|
+
} catch {
|
|
524
|
+
fs.unlinkSync(src);
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
fs.unlinkSync(src);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return movedCount;
|
|
532
|
+
}
|
|
533
|
+
function cleanEmptyDirectories(moveMap, projectRoot) {
|
|
534
|
+
const dirsToCheck = /* @__PURE__ */ new Set();
|
|
535
|
+
for (const [src] of moveMap) {
|
|
536
|
+
dirsToCheck.add(path.dirname(src));
|
|
537
|
+
}
|
|
538
|
+
for (const dir of [...dirsToCheck].sort((a, b) => b.length - a.length)) {
|
|
539
|
+
let current = dir;
|
|
540
|
+
while (current !== projectRoot && current.startsWith(projectRoot)) {
|
|
541
|
+
try {
|
|
542
|
+
const entries = fs.readdirSync(current);
|
|
543
|
+
if (entries.length === 0) {
|
|
544
|
+
fs.rmdirSync(current);
|
|
545
|
+
console.log(` removed empty dir: ${path.relative(projectRoot, current)}/`);
|
|
546
|
+
} else {
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
current = path.dirname(current);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function executeMoves(manifest) {
|
|
557
|
+
const { projectRoot, dryRun = false, useAliases = "preserve" } = manifest;
|
|
558
|
+
const tsConfigPath = manifest.tsConfigPath ? path.resolve(projectRoot, manifest.tsConfigPath) : path.join(projectRoot, "tsconfig.json");
|
|
559
|
+
if (!fs.existsSync(tsConfigPath)) {
|
|
560
|
+
throw new Error(`No tsconfig.json found at ${tsConfigPath}`);
|
|
561
|
+
}
|
|
562
|
+
const moveMap = expandMoves(manifest.moves, projectRoot);
|
|
563
|
+
console.log(`Project: ${projectRoot}`);
|
|
564
|
+
console.log(`Dry run: ${dryRun}`);
|
|
565
|
+
console.log(`
|
|
566
|
+
${moveMap.size} file(s) to move:
|
|
567
|
+
`);
|
|
568
|
+
for (const [src, dest] of moveMap) {
|
|
569
|
+
console.log(` ${path.relative(projectRoot, src)} -> ${path.relative(projectRoot, dest)}`);
|
|
570
|
+
}
|
|
571
|
+
const project = new Project({ tsConfigFilePath: tsConfigPath });
|
|
572
|
+
const isTsFile = (f) => /\.tsx?$/.test(f);
|
|
573
|
+
for (const [src] of moveMap) {
|
|
574
|
+
if (!isTsFile(src)) continue;
|
|
575
|
+
if (!project.getSourceFile(src)) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
`Source file is not included in tsconfig: ${path.relative(projectRoot, src)}
|
|
578
|
+
ts-morph cannot rewrite imports for files outside the project.
|
|
579
|
+
Either add it to tsconfig "include" or use --tsconfig to specify a broader config.`
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const usesJsExt = detectJsExtensionConvention(project);
|
|
584
|
+
console.log(`
|
|
585
|
+
Import convention: ${usesJsExt ? ".js extensions" : "extensionless"}`);
|
|
586
|
+
const destPaths = new Set(moveMap.values());
|
|
587
|
+
if (dryRun) {
|
|
588
|
+
const conflicts = checkDryRunConflicts(moveMap, projectRoot);
|
|
589
|
+
console.log("\nDry run \u2014 no changes written.");
|
|
590
|
+
return { filesMoved: 0, importsRewritten: 0, conflicts: conflicts.length > 0 ? conflicts : void 0 };
|
|
591
|
+
}
|
|
592
|
+
let movedCount = 0;
|
|
593
|
+
const filteredEntries = [...moveMap.entries()].filter(([src, dest]) => src !== dest && isTsFile(src));
|
|
594
|
+
const moveEntries = performTopologicalSort(filteredEntries, destPaths);
|
|
595
|
+
const preMoveCallSpecifiers = /* @__PURE__ */ new Map();
|
|
596
|
+
for (const sf of project.getSourceFiles()) {
|
|
597
|
+
const specs = /* @__PURE__ */ new Set();
|
|
598
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
599
|
+
if (!isModuleSpecifierCall(call)) return;
|
|
600
|
+
const literal = getCallStringArg(call);
|
|
601
|
+
if (!literal) return;
|
|
602
|
+
specs.add(literal.getLiteralValue());
|
|
603
|
+
});
|
|
604
|
+
if (specs.size > 0) preMoveCallSpecifiers.set(sf.getFilePath(), specs);
|
|
605
|
+
}
|
|
606
|
+
const movedSources = /* @__PURE__ */ new Set();
|
|
607
|
+
for (const [src, dest] of moveEntries) {
|
|
608
|
+
const sourceFile = project.getSourceFile(src);
|
|
609
|
+
if (!sourceFile) {
|
|
610
|
+
console.warn(` warning: ${path.relative(projectRoot, src)} not found in project, skipping`);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
const destDir = path.dirname(dest);
|
|
614
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
615
|
+
const isCaseRename = src.toLowerCase() === dest.toLowerCase() && src !== dest;
|
|
616
|
+
if (isCaseRename) {
|
|
617
|
+
const tempDest = dest + CASE_RENAME_TEMP_SUFFIX;
|
|
618
|
+
sourceFile.move(tempDest, { overwrite: true });
|
|
619
|
+
const tempFile = project.getSourceFile(tempDest);
|
|
620
|
+
tempFile.move(dest, { overwrite: true });
|
|
621
|
+
} else {
|
|
622
|
+
const destExistsOnDisk = fs.existsSync(dest);
|
|
623
|
+
const destMovedAway = movedSources.has(dest);
|
|
624
|
+
const needsOverwrite = destExistsOnDisk && destMovedAway;
|
|
625
|
+
sourceFile.move(dest, { overwrite: needsOverwrite });
|
|
626
|
+
}
|
|
627
|
+
movedSources.add(src);
|
|
628
|
+
movedCount++;
|
|
629
|
+
}
|
|
630
|
+
console.log(`
|
|
631
|
+
Moved ${movedCount} TS file(s) in-memory`);
|
|
632
|
+
const tsMorphRewriteCount = project.getSourceFiles().filter((sf) => {
|
|
633
|
+
const fp = sf.getFilePath();
|
|
634
|
+
return !sf.isSaved() && !destPaths.has(fp);
|
|
635
|
+
}).length;
|
|
636
|
+
if (usesJsExt) {
|
|
637
|
+
console.log("Restoring .js import extensions...");
|
|
638
|
+
fixJsExtensions(project);
|
|
639
|
+
}
|
|
640
|
+
stripIntroducedIndexSuffixes(project, preMoveCallSpecifiers, moveMap);
|
|
641
|
+
const aliasMappings = parseAliasMappings(project, projectRoot);
|
|
642
|
+
const callCtx = buildCallRewriteContext(moveMap, destPaths, aliasMappings, useAliases);
|
|
643
|
+
const callExprFixCount = fixCallExpressions(project, moveMap, usesJsExt, callCtx);
|
|
644
|
+
if (callExprFixCount > 0) {
|
|
645
|
+
console.log(`Fixed ${callExprFixCount} dynamic import(s) and require() call(s)`);
|
|
646
|
+
}
|
|
647
|
+
const sideEffectFixCount = fixSideEffectImports(project, moveMap, usesJsExt);
|
|
648
|
+
if (sideEffectFixCount > 0) {
|
|
649
|
+
console.log(`Fixed ${sideEffectFixCount} side-effect import(s)`);
|
|
650
|
+
}
|
|
651
|
+
const involvedFiles = collectInvolvedFiles(project, moveMap, aliasMappings, isTsFile);
|
|
652
|
+
const aliasFixCount = handleAliases(project, moveMap, involvedFiles, projectRoot, usesJsExt, useAliases);
|
|
653
|
+
if (aliasFixCount > 0) {
|
|
654
|
+
console.log(`Fixed ${aliasFixCount} alias import(s)`);
|
|
655
|
+
}
|
|
656
|
+
console.log("Writing to disk...");
|
|
657
|
+
project.saveSync();
|
|
658
|
+
const staleWarnings = detectStaleReferences(moveMap, projectRoot);
|
|
659
|
+
if (staleWarnings.length > 0) {
|
|
660
|
+
console.log(`
|
|
661
|
+
Warnings: ${staleWarnings.length} possible stale reference(s) found:`);
|
|
662
|
+
for (const w of staleWarnings) {
|
|
663
|
+
console.log(` ${w}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const nonTsMovedCount = commitMovesToDisk(moveMap, projectRoot, isTsFile, destPaths);
|
|
667
|
+
movedCount += nonTsMovedCount;
|
|
668
|
+
cleanEmptyDirectories(moveMap, projectRoot);
|
|
669
|
+
console.log("\nDone! Run `npx tsc --noEmit` to verify.");
|
|
670
|
+
const totalRewrites = tsMorphRewriteCount + callExprFixCount + sideEffectFixCount + aliasFixCount;
|
|
671
|
+
return {
|
|
672
|
+
filesMoved: movedCount,
|
|
673
|
+
importsRewritten: totalRewrites,
|
|
674
|
+
warnings: staleWarnings.length > 0 ? staleWarnings : void 0
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export {
|
|
679
|
+
executeMoves
|
|
680
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// src/resolver.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", ".test", "coverage"]);
|
|
5
|
+
function walkDir(dir, filter) {
|
|
6
|
+
const results = [];
|
|
7
|
+
if (!fs.existsSync(dir)) return results;
|
|
8
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
9
|
+
const fullPath = path.join(dir, entry.name);
|
|
10
|
+
if (entry.isDirectory()) {
|
|
11
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
12
|
+
results.push(...walkDir(fullPath, filter));
|
|
13
|
+
} else if (entry.isFile() && filter(entry.name)) {
|
|
14
|
+
results.push(fullPath);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return results;
|
|
18
|
+
}
|
|
19
|
+
function findTsFiles(dir) {
|
|
20
|
+
return walkDir(dir, (name) => /\.tsx?$/.test(name) && !name.endsWith(".d.ts"));
|
|
21
|
+
}
|
|
22
|
+
function findAllFiles(dir) {
|
|
23
|
+
return walkDir(dir, () => true);
|
|
24
|
+
}
|
|
25
|
+
function resolveCandidates(base) {
|
|
26
|
+
return [base + ".ts", base + ".tsx", path.join(base, "index.ts"), path.join(base, "index.tsx")];
|
|
27
|
+
}
|
|
28
|
+
function resolveImportTarget(importerPath, specifier) {
|
|
29
|
+
if (!specifier.startsWith(".")) return null;
|
|
30
|
+
const dir = path.dirname(importerPath);
|
|
31
|
+
let resolved = path.resolve(dir, specifier);
|
|
32
|
+
resolved = resolved.replace(/\.jsx?$/, "");
|
|
33
|
+
const candidates = resolveCandidates(resolved);
|
|
34
|
+
for (const candidate of candidates) {
|
|
35
|
+
if (fs.existsSync(candidate)) {
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function computeNewImportPath(importerNewPath, targetNewPath, usesJsExtension) {
|
|
42
|
+
let relative2 = path.relative(
|
|
43
|
+
path.dirname(importerNewPath),
|
|
44
|
+
targetNewPath
|
|
45
|
+
);
|
|
46
|
+
if (!relative2.startsWith(".")) {
|
|
47
|
+
relative2 = "./" + relative2;
|
|
48
|
+
}
|
|
49
|
+
relative2 = relative2.replace(/\.tsx?$/, "");
|
|
50
|
+
if (usesJsExtension) {
|
|
51
|
+
const ext = targetNewPath.endsWith(".tsx") ? ".jsx" : ".js";
|
|
52
|
+
if (!relative2.endsWith(ext)) {
|
|
53
|
+
relative2 += ext;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return relative2;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
findTsFiles,
|
|
61
|
+
findAllFiles,
|
|
62
|
+
resolveCandidates,
|
|
63
|
+
resolveImportTarget,
|
|
64
|
+
computeNewImportPath
|
|
65
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
executeMoves
|
|
4
|
+
} from "./chunk-ASGBQAOU.js";
|
|
5
|
+
import "./chunk-MRSPJM6J.js";
|
|
6
|
+
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
function printUsage() {
|
|
11
|
+
console.log(`
|
|
12
|
+
ts-shove \u2014 Move TypeScript files with automatic import rewriting
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
ts-shove <manifest.json> Batch move from manifest file
|
|
16
|
+
ts-shove <source> <destination> Move a single file
|
|
17
|
+
ts-shove <source/dir/> <dest/dir/> Move a directory (trailing slash)
|
|
18
|
+
ts-shove --dry-run <manifest.json> Preview without changes
|
|
19
|
+
ts-shove --dry-run <source> <destination> Preview single move
|
|
20
|
+
|
|
21
|
+
Manifest format:
|
|
22
|
+
{
|
|
23
|
+
"projectRoot": "/absolute/path", (optional \u2014 defaults to git root or cwd)
|
|
24
|
+
"moves": {
|
|
25
|
+
"src/old.ts": "src/new.ts", (file move)
|
|
26
|
+
"src/old-dir/": "src/new-dir/" (directory move \u2014 trailing slash)
|
|
27
|
+
},
|
|
28
|
+
"dryRun": false, (optional)
|
|
29
|
+
"useAliases": "preserve" (optional \u2014 always | never | preserve)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--dry-run, -n Show what would change without modifying files
|
|
34
|
+
--root, -r <dir> Project root (default: git root or cwd)
|
|
35
|
+
--tsconfig, -t <path> Path to tsconfig.json (default: tsconfig.json in project root)
|
|
36
|
+
--use-aliases, -a <mode> Alias handling: always, never, preserve (default: preserve)
|
|
37
|
+
--help, -h Show this help
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
function findGitRoot() {
|
|
41
|
+
let dir = process.cwd();
|
|
42
|
+
while (dir !== path.dirname(dir)) {
|
|
43
|
+
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
44
|
+
dir = path.dirname(dir);
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function parseArgs(argv) {
|
|
49
|
+
const args = argv.slice(2);
|
|
50
|
+
let dryRun = false;
|
|
51
|
+
let root = null;
|
|
52
|
+
let tsConfigPath;
|
|
53
|
+
let useAliases;
|
|
54
|
+
const positional = [];
|
|
55
|
+
for (let i = 0; i < args.length; i++) {
|
|
56
|
+
const arg = args[i];
|
|
57
|
+
if (arg === "--help" || arg === "-h") {
|
|
58
|
+
printUsage();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
} else if (arg === "--dry-run" || arg === "-n") {
|
|
61
|
+
dryRun = true;
|
|
62
|
+
} else if ((arg === "--root" || arg === "-r") && i + 1 < args.length) {
|
|
63
|
+
root = path.resolve(args[++i]);
|
|
64
|
+
} else if ((arg === "--tsconfig" || arg === "-t") && i + 1 < args.length) {
|
|
65
|
+
tsConfigPath = args[++i];
|
|
66
|
+
} else if ((arg === "--use-aliases" || arg === "-a") && i + 1 < args.length) {
|
|
67
|
+
const val = args[++i];
|
|
68
|
+
if (val !== "always" && val !== "never" && val !== "preserve") {
|
|
69
|
+
console.error(`Invalid --use-aliases value: ${val}. Expected: always, never, preserve`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
useAliases = val;
|
|
73
|
+
} else if (!arg.startsWith("-")) {
|
|
74
|
+
positional.push(arg);
|
|
75
|
+
} else {
|
|
76
|
+
console.error(`Unknown option: ${arg}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (positional.length === 0) {
|
|
81
|
+
printUsage();
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
if (positional.length === 1) {
|
|
85
|
+
const manifestPath = path.resolve(positional[0]);
|
|
86
|
+
if (!fs.existsSync(manifestPath)) {
|
|
87
|
+
console.error(`Manifest not found: ${manifestPath}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const manifest2 = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
91
|
+
manifest2.projectRoot = manifest2.projectRoot ? path.resolve(manifest2.projectRoot) : root ?? findGitRoot() ?? process.cwd();
|
|
92
|
+
if (dryRun) manifest2.dryRun = true;
|
|
93
|
+
if (useAliases) manifest2.useAliases = useAliases;
|
|
94
|
+
if (tsConfigPath) manifest2.tsConfigPath = tsConfigPath;
|
|
95
|
+
return manifest2;
|
|
96
|
+
}
|
|
97
|
+
if (positional.length === 2) {
|
|
98
|
+
const projectRoot = root ?? findGitRoot() ?? process.cwd();
|
|
99
|
+
return {
|
|
100
|
+
projectRoot,
|
|
101
|
+
moves: { [positional[0]]: positional[1] },
|
|
102
|
+
dryRun,
|
|
103
|
+
useAliases,
|
|
104
|
+
tsConfigPath
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
console.error("Expected 1 (manifest) or 2 (source destination) arguments");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
var manifest = parseArgs(process.argv);
|
|
111
|
+
try {
|
|
112
|
+
executeMoves(manifest);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error("\nError:", err instanceof Error ? err.message : err);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
package/dist/mover.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type AliasMode = "always" | "never" | "preserve";
|
|
2
|
+
interface MoveManifest {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
moves: Record<string, string>;
|
|
5
|
+
dryRun?: boolean;
|
|
6
|
+
useAliases?: AliasMode;
|
|
7
|
+
tsConfigPath?: string;
|
|
8
|
+
}
|
|
9
|
+
interface MoveResult {
|
|
10
|
+
filesMoved: number;
|
|
11
|
+
importsRewritten: number;
|
|
12
|
+
conflicts?: string[];
|
|
13
|
+
warnings?: string[];
|
|
14
|
+
}
|
|
15
|
+
declare function executeMoves(manifest: MoveManifest): MoveResult;
|
|
16
|
+
|
|
17
|
+
export { type AliasMode, type MoveManifest, type MoveResult, executeMoves };
|
package/dist/mover.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
declare function findTsFiles(dir: string): string[];
|
|
2
|
+
declare function findAllFiles(dir: string): string[];
|
|
3
|
+
declare function resolveCandidates(base: string): string[];
|
|
4
|
+
declare function resolveImportTarget(importerPath: string, specifier: string): string | null;
|
|
5
|
+
declare function computeNewImportPath(importerNewPath: string, targetNewPath: string, usesJsExtension: boolean): string;
|
|
6
|
+
|
|
7
|
+
export { computeNewImportPath, findAllFiles, findTsFiles, resolveCandidates, resolveImportTarget };
|
package/dist/resolver.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeNewImportPath,
|
|
3
|
+
findAllFiles,
|
|
4
|
+
findTsFiles,
|
|
5
|
+
resolveCandidates,
|
|
6
|
+
resolveImportTarget
|
|
7
|
+
} from "./chunk-MRSPJM6J.js";
|
|
8
|
+
export {
|
|
9
|
+
computeNewImportPath,
|
|
10
|
+
findAllFiles,
|
|
11
|
+
findTsFiles,
|
|
12
|
+
resolveCandidates,
|
|
13
|
+
resolveImportTarget
|
|
14
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ts-shove",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Move TypeScript files with automatic import path rewriting via ts-morph AST analysis",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Alexander Fuerst <alexander@fuerst.one>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/fuerst-one/ts-shove.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/fuerst-one/ts-shove#readme",
|
|
13
|
+
"bugs": "https://github.com/fuerst-one/ts-shove/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"typescript",
|
|
16
|
+
"move",
|
|
17
|
+
"rename",
|
|
18
|
+
"refactor",
|
|
19
|
+
"imports",
|
|
20
|
+
"ts-morph",
|
|
21
|
+
"ast",
|
|
22
|
+
"codemod",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"ts-shove": "dist/cli.js"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"import": "./dist/mover.js",
|
|
35
|
+
"types": "./dist/mover.d.ts"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup src/cli.ts src/mover.ts src/resolver.ts --format esm --target node20 --clean --dts",
|
|
45
|
+
"dev": "tsx src/cli.ts",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"prepublishOnly": "pnpm test && pnpm build"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"ts-morph": "^25.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^25.5.0",
|
|
55
|
+
"@types/react": "^19.2.14",
|
|
56
|
+
"react": "^19.2.4",
|
|
57
|
+
"tsup": "^8.0.0",
|
|
58
|
+
"tsx": "^4.0.0",
|
|
59
|
+
"typescript": "^5.0.0",
|
|
60
|
+
"vitest": "^4.1.1"
|
|
61
|
+
}
|
|
62
|
+
}
|