assistant-ui 0.0.112 → 0.0.113
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/dist/codemods/v0-12/assistant-api-to-aui.d.ts.map +1 -1
- package/dist/codemods/v0-12/assistant-api-to-aui.js +87 -62
- package/dist/codemods/v0-12/assistant-api-to-aui.js.map +1 -1
- package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts.map +1 -1
- package/dist/codemods/v0-12/primitive-if-to-aui-if.js +31 -45
- package/dist/codemods/v0-12/primitive-if-to-aui-if.js.map +1 -1
- package/dist/commands/add.d.ts +1 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +4 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +33 -10
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/info.d.ts.map +1 -1
- package/dist/commands/info.js +2 -1
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +25 -28
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/update.js +6 -7
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +5 -2
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/index.js +2 -6
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-skill.d.ts +1 -1
- package/dist/lib/agent-skill.d.ts.map +1 -1
- package/dist/lib/agent-skill.js.map +1 -1
- package/dist/lib/create-project.d.ts +2 -3
- package/dist/lib/create-project.d.ts.map +1 -1
- package/dist/lib/create-project.js +17 -42
- package/dist/lib/create-project.js.map +1 -1
- package/dist/lib/handle-cli-error.d.ts +5 -0
- package/dist/lib/handle-cli-error.d.ts.map +1 -0
- package/dist/lib/handle-cli-error.js +17 -0
- package/dist/lib/handle-cli-error.js.map +1 -0
- package/dist/lib/install-ai-sdk-lib.js +8 -1
- package/dist/lib/install-ai-sdk-lib.js.map +1 -1
- package/dist/lib/install-edge-lib.js +4 -8
- package/dist/lib/install-edge-lib.js.map +1 -1
- package/dist/lib/run-spawn.d.ts +16 -2
- package/dist/lib/run-spawn.d.ts.map +1 -1
- package/dist/lib/run-spawn.js +87 -9
- package/dist/lib/run-spawn.js.map +1 -1
- package/dist/lib/transform.d.ts +1 -1
- package/dist/lib/transform.d.ts.map +1 -1
- package/dist/lib/transform.js +12 -25
- package/dist/lib/transform.js.map +1 -1
- package/dist/lib/upgrade.js +20 -16
- package/dist/lib/upgrade.js.map +1 -1
- package/dist/lib/utils/file-scanner.d.ts +9 -1
- package/dist/lib/utils/file-scanner.d.ts.map +1 -1
- package/dist/lib/utils/file-scanner.js +22 -20
- package/dist/lib/utils/file-scanner.js.map +1 -1
- package/dist/lib/utils/package-installer.js +1 -1
- package/dist/lib/utils/package-manager.d.ts +3 -1
- package/dist/lib/utils/package-manager.d.ts.map +1 -1
- package/dist/lib/utils/package-manager.js +22 -10
- package/dist/lib/utils/package-manager.js.map +1 -1
- package/package.json +6 -6
- package/plugin/skills/assistant-ui/SKILL.md +3 -3
- package/src/codemods/v0-12/__tests__/assistant-api-to-aui.test.ts +370 -0
- package/src/codemods/v0-12/__tests__/primitive-if-to-aui-if.test.ts +100 -0
- package/src/codemods/v0-12/assistant-api-to-aui.ts +227 -134
- package/src/codemods/v0-12/primitive-if-to-aui-if.ts +44 -72
- package/src/commands/add.ts +5 -5
- package/src/commands/create.ts +41 -7
- package/src/commands/info.ts +1 -0
- package/src/commands/init.ts +4 -6
- package/src/commands/mcp.ts +20 -29
- package/src/commands/update.ts +6 -8
- package/src/commands/upgrade.ts +5 -2
- package/src/index.ts +2 -7
- package/src/lib/agent-skill.ts +2 -1
- package/src/lib/create-project.ts +24 -58
- package/src/lib/handle-cli-error.test.ts +67 -0
- package/src/lib/handle-cli-error.ts +17 -0
- package/src/lib/install-ai-sdk-lib.ts +13 -1
- package/src/lib/install-edge-lib.ts +7 -8
- package/src/lib/run-spawn.test.ts +148 -0
- package/src/lib/run-spawn.ts +121 -11
- package/src/lib/transform.test.ts +62 -0
- package/src/lib/transform.ts +21 -37
- package/src/lib/upgrade.ts +23 -19
- package/src/lib/utils/file-scanner.ts +26 -20
- package/src/lib/utils/package-manager.ts +29 -12
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
spawn: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("cross-spawn", async (importOriginal) => ({
|
|
10
|
+
...(await importOriginal()),
|
|
11
|
+
spawn: mocks.spawn,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
hasActiveSpawn,
|
|
16
|
+
runSpawn,
|
|
17
|
+
runSpawnCapture,
|
|
18
|
+
SpawnExitError,
|
|
19
|
+
SpawnSignalError,
|
|
20
|
+
} from "./run-spawn";
|
|
21
|
+
|
|
22
|
+
const createChild = () =>
|
|
23
|
+
Object.assign(new EventEmitter(), {
|
|
24
|
+
kill: vi.fn(() => true),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const trackSignal = (signal: NodeJS.Signals) => {
|
|
28
|
+
const before = new Set(process.listeners(signal));
|
|
29
|
+
return {
|
|
30
|
+
count: before.size,
|
|
31
|
+
added: () =>
|
|
32
|
+
process.listeners(signal).find((listener) => !before.has(listener)),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe("runSpawn", () => {
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.restoreAllMocks();
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("forwards the first termination signal and cleans up listeners", async () => {
|
|
43
|
+
const child = createChild();
|
|
44
|
+
mocks.spawn.mockReturnValue(child);
|
|
45
|
+
const sigterm = trackSignal("SIGTERM");
|
|
46
|
+
const result = runSpawn("assistant-ui", ["create"]);
|
|
47
|
+
const signalHandler = sigterm.added();
|
|
48
|
+
|
|
49
|
+
expect(signalHandler).toBeDefined();
|
|
50
|
+
signalHandler?.("SIGTERM");
|
|
51
|
+
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
|
52
|
+
|
|
53
|
+
child.emit("close", null, "SIGTERM");
|
|
54
|
+
await expect(result).rejects.toMatchObject({
|
|
55
|
+
signal: "SIGTERM",
|
|
56
|
+
forwarded: true,
|
|
57
|
+
});
|
|
58
|
+
expect(process.listenerCount("SIGTERM")).toBe(sigterm.count);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("escalates a repeated termination signal", async () => {
|
|
62
|
+
const child = createChild();
|
|
63
|
+
mocks.spawn.mockReturnValue(child);
|
|
64
|
+
const sigterm = trackSignal("SIGTERM");
|
|
65
|
+
const result = runSpawn("assistant-ui", ["create"]);
|
|
66
|
+
const signalHandler = sigterm.added();
|
|
67
|
+
|
|
68
|
+
expect(signalHandler).toBeDefined();
|
|
69
|
+
signalHandler?.("SIGTERM");
|
|
70
|
+
signalHandler?.("SIGTERM");
|
|
71
|
+
|
|
72
|
+
expect(child.kill).toHaveBeenNthCalledWith(1, "SIGTERM");
|
|
73
|
+
expect(child.kill).toHaveBeenNthCalledWith(2, "SIGKILL");
|
|
74
|
+
await expect(result).rejects.toBeInstanceOf(SpawnSignalError);
|
|
75
|
+
expect(process.listenerCount("SIGTERM")).toBe(sigterm.count);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("reports an active spawn only while the child is running", async () => {
|
|
79
|
+
const child = createChild();
|
|
80
|
+
mocks.spawn.mockReturnValue(child);
|
|
81
|
+
|
|
82
|
+
expect(hasActiveSpawn()).toBe(false);
|
|
83
|
+
const result = runSpawn("assistant-ui", ["create"]);
|
|
84
|
+
expect(hasActiveSpawn()).toBe(true);
|
|
85
|
+
|
|
86
|
+
child.emit("close", 0, null);
|
|
87
|
+
await result;
|
|
88
|
+
|
|
89
|
+
expect(hasActiveSpawn()).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("preserves normal child exit handling", async () => {
|
|
93
|
+
const child = createChild();
|
|
94
|
+
mocks.spawn.mockReturnValue(child);
|
|
95
|
+
const result = runSpawn("assistant-ui", ["create"]);
|
|
96
|
+
|
|
97
|
+
child.emit("close", 0, null);
|
|
98
|
+
|
|
99
|
+
await expect(result).resolves.toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("reports nonzero child exits", async () => {
|
|
103
|
+
const child = createChild();
|
|
104
|
+
mocks.spawn.mockReturnValue(child);
|
|
105
|
+
const result = runSpawn("assistant-ui", ["create"]);
|
|
106
|
+
|
|
107
|
+
child.emit("close", 7, null);
|
|
108
|
+
|
|
109
|
+
await expect(result).rejects.toEqual(expect.any(SpawnExitError));
|
|
110
|
+
await expect(result).rejects.toMatchObject({ code: 7 });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("distinguishes child signals from forwarded signals", async () => {
|
|
114
|
+
const child = createChild();
|
|
115
|
+
mocks.spawn.mockReturnValue(child);
|
|
116
|
+
const result = runSpawn("assistant-ui", ["create"]);
|
|
117
|
+
|
|
118
|
+
child.emit("close", null, "SIGSEGV");
|
|
119
|
+
|
|
120
|
+
await expect(result).rejects.toMatchObject({
|
|
121
|
+
signal: "SIGSEGV",
|
|
122
|
+
forwarded: false,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("captures output without blocking the event loop", async () => {
|
|
127
|
+
const child = Object.assign(new EventEmitter(), {
|
|
128
|
+
kill: vi.fn(() => true),
|
|
129
|
+
stdout: new PassThrough(),
|
|
130
|
+
stderr: new PassThrough(),
|
|
131
|
+
});
|
|
132
|
+
mocks.spawn.mockReturnValue(child);
|
|
133
|
+
const result = runSpawnCapture("jscodeshift", ["--version"]);
|
|
134
|
+
|
|
135
|
+
child.stdout.write("Processing file one\n");
|
|
136
|
+
child.stderr.write("warning\n");
|
|
137
|
+
child.stdout.end();
|
|
138
|
+
child.stderr.end();
|
|
139
|
+
child.emit("close", 0, null);
|
|
140
|
+
|
|
141
|
+
await expect(result).resolves.toMatchObject({
|
|
142
|
+
code: 0,
|
|
143
|
+
signal: null,
|
|
144
|
+
stdout: "Processing file one\n",
|
|
145
|
+
stderr: "warning\n",
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
package/src/lib/run-spawn.ts
CHANGED
|
@@ -2,31 +2,141 @@ import { spawn } from "cross-spawn";
|
|
|
2
2
|
|
|
3
3
|
export class SpawnExitError extends Error {
|
|
4
4
|
code: number;
|
|
5
|
+
stderr: string;
|
|
5
6
|
|
|
6
|
-
constructor(code: number) {
|
|
7
|
-
super(
|
|
7
|
+
constructor(code: number, stderr = "") {
|
|
8
|
+
super(
|
|
9
|
+
stderr
|
|
10
|
+
? `Process exited with code ${code}\n${stderr}`
|
|
11
|
+
: `Process exited with code ${code}`,
|
|
12
|
+
);
|
|
8
13
|
this.code = code;
|
|
14
|
+
this.stderr = stderr;
|
|
9
15
|
}
|
|
10
16
|
}
|
|
11
17
|
|
|
12
|
-
export
|
|
18
|
+
export class SpawnSignalError extends Error {
|
|
19
|
+
signal: NodeJS.Signals;
|
|
20
|
+
forwarded: boolean;
|
|
21
|
+
|
|
22
|
+
constructor(signal: NodeJS.Signals, forwarded: boolean) {
|
|
23
|
+
super(`Process terminated by ${signal}`);
|
|
24
|
+
this.signal = signal;
|
|
25
|
+
this.forwarded = forwarded;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SpawnResult {
|
|
30
|
+
code: number | null;
|
|
31
|
+
signal: NodeJS.Signals | null;
|
|
32
|
+
stdout: string;
|
|
33
|
+
stderr: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let activeSpawns = 0;
|
|
37
|
+
|
|
38
|
+
// A spawn that is forwarding a signal owns termination for that window, so
|
|
39
|
+
// callers with their own cleanup listeners defer to it instead of racing it.
|
|
40
|
+
export const hasActiveSpawn = (): boolean => activeSpawns > 0;
|
|
41
|
+
|
|
42
|
+
function spawnProcess(
|
|
13
43
|
command: string,
|
|
14
44
|
args: string[],
|
|
15
|
-
cwd
|
|
16
|
-
|
|
45
|
+
cwd: string | undefined,
|
|
46
|
+
captureOutput: boolean,
|
|
47
|
+
): Promise<SpawnResult> {
|
|
17
48
|
return new Promise((resolve, reject) => {
|
|
18
49
|
const child = spawn(command, args, {
|
|
19
|
-
stdio: "inherit",
|
|
50
|
+
stdio: captureOutput ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
20
51
|
cwd,
|
|
21
52
|
});
|
|
53
|
+
let forwardedSignal: NodeJS.Signals | null = null;
|
|
54
|
+
let settled = false;
|
|
55
|
+
let stdout = "";
|
|
56
|
+
let stderr = "";
|
|
57
|
+
|
|
58
|
+
if (captureOutput) {
|
|
59
|
+
child.stdout?.setEncoding("utf8");
|
|
60
|
+
child.stdout?.on("data", (chunk: string) => {
|
|
61
|
+
stdout += chunk;
|
|
62
|
+
});
|
|
63
|
+
child.stderr?.setEncoding("utf8");
|
|
64
|
+
child.stderr?.on("data", (chunk: string) => {
|
|
65
|
+
stderr += chunk;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const cleanup = () => {
|
|
70
|
+
activeSpawns -= 1;
|
|
71
|
+
process.off("SIGINT", onSigint);
|
|
72
|
+
process.off("SIGTERM", onSigterm);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const rejectWithSignal = (signal: NodeJS.Signals) => {
|
|
76
|
+
settled = true;
|
|
77
|
+
cleanup();
|
|
78
|
+
reject(new SpawnSignalError(signal, forwardedSignal !== null));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const forwardSignal = (signal: NodeJS.Signals) => {
|
|
82
|
+
if (forwardedSignal !== null) {
|
|
83
|
+
child.kill("SIGKILL");
|
|
84
|
+
rejectWithSignal(forwardedSignal);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
forwardedSignal = signal;
|
|
89
|
+
child.kill(signal);
|
|
90
|
+
};
|
|
91
|
+
const onSigint = () => forwardSignal("SIGINT");
|
|
92
|
+
const onSigterm = () => forwardSignal("SIGTERM");
|
|
22
93
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
94
|
+
activeSpawns += 1;
|
|
95
|
+
process.on("SIGINT", onSigint);
|
|
96
|
+
process.on("SIGTERM", onSigterm);
|
|
97
|
+
|
|
98
|
+
child.on("error", (error) => {
|
|
99
|
+
if (settled) return;
|
|
100
|
+
settled = true;
|
|
101
|
+
cleanup();
|
|
102
|
+
if (forwardedSignal !== null) {
|
|
103
|
+
reject(new SpawnSignalError(forwardedSignal, true));
|
|
27
104
|
} else {
|
|
28
|
-
|
|
105
|
+
reject(error);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
child.on("close", (code, signal) => {
|
|
109
|
+
if (settled) return;
|
|
110
|
+
settled = true;
|
|
111
|
+
cleanup();
|
|
112
|
+
if (forwardedSignal !== null) {
|
|
113
|
+
reject(new SpawnSignalError(forwardedSignal, true));
|
|
114
|
+
} else {
|
|
115
|
+
resolve({ code, signal: signal ?? null, stdout, stderr });
|
|
29
116
|
}
|
|
30
117
|
});
|
|
31
118
|
});
|
|
32
119
|
}
|
|
120
|
+
|
|
121
|
+
export function runSpawn(
|
|
122
|
+
command: string,
|
|
123
|
+
args: string[],
|
|
124
|
+
cwd?: string,
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
return spawnProcess(command, args, cwd, false).then(({ code, signal }) => {
|
|
127
|
+
if (signal != null) {
|
|
128
|
+
throw new SpawnSignalError(signal, false);
|
|
129
|
+
}
|
|
130
|
+
if (code !== 0) {
|
|
131
|
+
throw new SpawnExitError(code || 1);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function runSpawnCapture(
|
|
137
|
+
command: string,
|
|
138
|
+
args: string[],
|
|
139
|
+
cwd?: string,
|
|
140
|
+
): Promise<SpawnResult> {
|
|
141
|
+
return spawnProcess(command, args, cwd, true);
|
|
142
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
runSpawnCapture: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("./run-spawn", async (importOriginal) => ({
|
|
8
|
+
...(await importOriginal()),
|
|
9
|
+
runSpawnCapture: mocks.runSpawnCapture,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { transform } from "./transform";
|
|
13
|
+
import { SpawnExitError } from "./run-spawn";
|
|
14
|
+
|
|
15
|
+
describe("transform", () => {
|
|
16
|
+
it("runs codemods asynchronously and reports progress", async () => {
|
|
17
|
+
mocks.runSpawnCapture.mockResolvedValue({
|
|
18
|
+
code: 0,
|
|
19
|
+
signal: null,
|
|
20
|
+
stdout: "Processing file one\nProcessing file two\n",
|
|
21
|
+
stderr: "",
|
|
22
|
+
});
|
|
23
|
+
const onProgress = vi.fn();
|
|
24
|
+
|
|
25
|
+
const errors = await transform(
|
|
26
|
+
"v0-8/ui-package-split",
|
|
27
|
+
"/tmp/project",
|
|
28
|
+
{ dry: true },
|
|
29
|
+
{
|
|
30
|
+
logStatus: false,
|
|
31
|
+
onProgress,
|
|
32
|
+
relevantFiles: ["/tmp/project/app.tsx"],
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(errors).toEqual([]);
|
|
37
|
+
expect(onProgress).toHaveBeenCalledWith(2);
|
|
38
|
+
expect(mocks.runSpawnCapture).toHaveBeenCalledWith(
|
|
39
|
+
"npx",
|
|
40
|
+
expect.arrayContaining(["jscodeshift", "--dry"]),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("surfaces the codemod's stderr when it exits nonzero", async () => {
|
|
45
|
+
mocks.runSpawnCapture.mockResolvedValue({
|
|
46
|
+
code: 1,
|
|
47
|
+
signal: null,
|
|
48
|
+
stdout: "",
|
|
49
|
+
stderr: "SyntaxError: Unexpected token\n",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const failure = transform(
|
|
53
|
+
"v0-8/ui-package-split",
|
|
54
|
+
"/tmp/project",
|
|
55
|
+
{ dry: true },
|
|
56
|
+
{ logStatus: false, relevantFiles: ["/tmp/project/app.tsx"] },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
await expect(failure).rejects.toBeInstanceOf(SpawnExitError);
|
|
60
|
+
await expect(failure).rejects.toThrow("SyntaxError: Unexpected token");
|
|
61
|
+
});
|
|
62
|
+
});
|
package/src/lib/transform.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
1
|
import debug from "debug";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import type { TransformOptions } from "./transform-options";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
5
|
import * as fs from "node:fs";
|
|
7
6
|
import { sync as globSync } from "glob";
|
|
7
|
+
import { runSpawnCapture, SpawnExitError, SpawnSignalError } from "./run-spawn";
|
|
8
8
|
|
|
9
9
|
const log = debug("codemod:transform");
|
|
10
10
|
const error = debug("codemod:transform:error");
|
|
@@ -106,7 +106,7 @@ function parseErrors(transform: string, output: string): TransformErrors {
|
|
|
106
106
|
return errors;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
export function transform(
|
|
109
|
+
export async function transform(
|
|
110
110
|
codemod: string,
|
|
111
111
|
source: string,
|
|
112
112
|
transformOptions: TransformOptions,
|
|
@@ -115,7 +115,7 @@ export function transform(
|
|
|
115
115
|
onProgress?: (processedFiles: number) => void;
|
|
116
116
|
relevantFiles?: string[];
|
|
117
117
|
} = { logStatus: true },
|
|
118
|
-
): TransformErrors {
|
|
118
|
+
): Promise<TransformErrors> {
|
|
119
119
|
if (options.logStatus) {
|
|
120
120
|
log(`Applying codemod '${codemod}': ${source}`);
|
|
121
121
|
}
|
|
@@ -133,44 +133,28 @@ export function transform(
|
|
|
133
133
|
|
|
134
134
|
const command = buildCommand(codemodPath, targetFiles, transformOptions);
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
const result = await runSpawnCapture(command[0]!, command.slice(1));
|
|
137
|
+
if (result.signal !== null) {
|
|
138
|
+
throw new SpawnSignalError(result.signal, false);
|
|
139
|
+
}
|
|
140
|
+
if (!options.onProgress && result.code !== 0) {
|
|
141
|
+
throw new SpawnExitError(result.code || 1, result.stderr);
|
|
142
|
+
}
|
|
142
143
|
|
|
143
|
-
|
|
144
|
+
const { stdout } = result;
|
|
144
145
|
|
|
145
|
-
|
|
146
|
+
if (options.onProgress) {
|
|
146
147
|
const processedFiles = (stdout.match(/Processing file/g) || []).length;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
148
|
+
options.onProgress(processedFiles);
|
|
149
|
+
}
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
return errors;
|
|
160
|
-
} else {
|
|
161
|
-
// Use the original synchronous approach if no progress callback
|
|
162
|
-
const stdout = execFileSync(command[0]!, command.slice(1), {
|
|
163
|
-
encoding: "utf8",
|
|
164
|
-
stdio: "pipe",
|
|
151
|
+
const errors = parseErrors(codemod, stdout);
|
|
152
|
+
if (options.logStatus && errors.length > 0) {
|
|
153
|
+
errors.forEach(({ transform, filename, summary }) => {
|
|
154
|
+
error(
|
|
155
|
+
`Error applying codemod [codemod=${transform}, path=${filename}, summary=${summary}]`,
|
|
156
|
+
);
|
|
165
157
|
});
|
|
166
|
-
const errors = parseErrors(codemod, stdout);
|
|
167
|
-
if (options.logStatus && errors.length > 0) {
|
|
168
|
-
errors.forEach(({ transform, filename, summary }) => {
|
|
169
|
-
error(
|
|
170
|
-
`Error applying codemod [codemod=${transform}, path=${filename}, summary=${summary}]`,
|
|
171
|
-
);
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
return errors;
|
|
175
158
|
}
|
|
159
|
+
return errors;
|
|
176
160
|
}
|
package/src/lib/upgrade.ts
CHANGED
|
@@ -42,6 +42,7 @@ export async function upgrade(options: TransformOptions) {
|
|
|
42
42
|
{
|
|
43
43
|
format: "Progress |{bar}| {percentage}% | ETA: {eta}s || {status}",
|
|
44
44
|
hideCursor: true,
|
|
45
|
+
gracefulExit: true,
|
|
45
46
|
},
|
|
46
47
|
Presets.shades_classic,
|
|
47
48
|
);
|
|
@@ -49,28 +50,31 @@ export async function upgrade(options: TransformOptions) {
|
|
|
49
50
|
bar.start(totalWork, 0, { status: "Starting..." });
|
|
50
51
|
const allErrors: TransformErrors = [];
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
try {
|
|
54
|
+
for (const codemod of bundle) {
|
|
55
|
+
bar.update(completedWork, { status: `Running ${codemod}...` });
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
// Use a custom progress callback to update the progress bar
|
|
58
|
+
const errors = await transform(codemod, cwd, options, {
|
|
59
|
+
logStatus: false,
|
|
60
|
+
onProgress: (processedFiles: number) => {
|
|
61
|
+
completedWork = bundle.indexOf(codemod) * fileCount + processedFiles;
|
|
62
|
+
bar.update(Math.min(completedWork, totalWork), {
|
|
63
|
+
status: `Running ${codemod} (${processedFiles}/${fileCount} files)`,
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
relevantFiles, // Pass the pre-computed relevant files
|
|
67
|
+
});
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
allErrors.push(...errors);
|
|
70
|
+
completedWork = (bundle.indexOf(codemod) + 1) * fileCount;
|
|
71
|
+
bar.update(completedWork, { status: `Completed ${codemod}` });
|
|
72
|
+
}
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
bar.update(totalWork, { status: "Checking dependencies..." });
|
|
75
|
+
} finally {
|
|
76
|
+
bar.stop();
|
|
77
|
+
}
|
|
74
78
|
|
|
75
79
|
if (allErrors.length > 0) {
|
|
76
80
|
log("Some codemods did not apply successfully to all files. Details:");
|
|
@@ -8,6 +8,23 @@ export interface ScanOptions {
|
|
|
8
8
|
ignore?: string[];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export function* readProjectFiles(
|
|
12
|
+
pattern: string,
|
|
13
|
+
options: { cwd: string; ignore?: string[] },
|
|
14
|
+
): Generator<{ file: string; fullPath: string; content: string }> {
|
|
15
|
+
const files = globSync(pattern, options);
|
|
16
|
+
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const fullPath = path.join(options.cwd, file);
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
21
|
+
yield { file, fullPath, content };
|
|
22
|
+
} catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
export function scanForImport(
|
|
12
29
|
importPattern: string | string[],
|
|
13
30
|
options: ScanOptions = {},
|
|
@@ -20,20 +37,13 @@ export function scanForImport(
|
|
|
20
37
|
"**/build/**",
|
|
21
38
|
];
|
|
22
39
|
|
|
23
|
-
const files = globSync(pattern, { cwd, ignore });
|
|
24
40
|
const patterns = Array.isArray(importPattern)
|
|
25
41
|
? importPattern
|
|
26
42
|
: [importPattern];
|
|
27
43
|
|
|
28
|
-
for (const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const content = fs.readFileSync(fullPath, "utf8");
|
|
32
|
-
if (patterns.some((p) => content.includes(p))) {
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
} catch {
|
|
36
|
-
// Ignore files that cannot be read
|
|
44
|
+
for (const { content } of readProjectFiles(pattern, { cwd, ignore })) {
|
|
45
|
+
if (patterns.some((p) => content.includes(p))) {
|
|
46
|
+
return true;
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
49
|
|
|
@@ -52,18 +62,14 @@ export function getFilesContaining(
|
|
|
52
62
|
"**/build/**",
|
|
53
63
|
];
|
|
54
64
|
|
|
55
|
-
const files = globSync(pattern, { cwd, ignore });
|
|
56
65
|
const result: string[] = [];
|
|
57
66
|
|
|
58
|
-
for (const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
} catch {
|
|
66
|
-
// Ignore files that cannot be read
|
|
67
|
+
for (const { fullPath, content } of readProjectFiles(pattern, {
|
|
68
|
+
cwd,
|
|
69
|
+
ignore,
|
|
70
|
+
})) {
|
|
71
|
+
if (content.includes(searchString)) {
|
|
72
|
+
result.push(fullPath);
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
3
|
import { detect } from "detect-package-manager";
|
|
5
4
|
import * as readline from "node:readline";
|
|
6
5
|
import { logger } from "./logger";
|
|
6
|
+
import { runSpawn, SpawnSignalError } from "../run-spawn";
|
|
7
|
+
|
|
8
|
+
export type PackageManagerName = "npm" | "pnpm" | "yarn" | "bun";
|
|
7
9
|
|
|
8
10
|
export function askQuestion(query: string): Promise<string> {
|
|
9
11
|
return new Promise((resolve) => {
|
|
@@ -62,25 +64,40 @@ export async function getInstallCommand(
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
function detectFromUserAgent(): PackageManagerName | undefined {
|
|
68
|
+
const ua = process.env.npm_config_user_agent;
|
|
69
|
+
if (!ua) return undefined;
|
|
70
|
+
if (ua.startsWith("bun/")) return "bun";
|
|
71
|
+
if (ua.startsWith("pnpm/")) return "pnpm";
|
|
72
|
+
if (ua.startsWith("yarn/")) return "yarn";
|
|
73
|
+
if (ua.startsWith("npm/")) return "npm";
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function resolvePackageManagerForCwd(
|
|
78
|
+
cwd: string,
|
|
79
|
+
packageManager?: PackageManagerName,
|
|
80
|
+
): Promise<PackageManagerName> {
|
|
81
|
+
if (packageManager) return packageManager;
|
|
82
|
+
const fromAgent = detectFromUserAgent();
|
|
83
|
+
if (fromAgent) return fromAgent;
|
|
84
|
+
try {
|
|
85
|
+
return await detect({ cwd });
|
|
86
|
+
} catch {
|
|
87
|
+
return "npm";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
65
91
|
export async function installPackage(
|
|
66
92
|
packageName: string,
|
|
67
93
|
cwd?: string,
|
|
68
94
|
): Promise<boolean> {
|
|
69
95
|
try {
|
|
70
96
|
const { command, args } = await getInstallCommand(packageName, cwd);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (result.error || result.status !== 0) {
|
|
74
|
-
logger.error(
|
|
75
|
-
`Installation failed${
|
|
76
|
-
result.error ? `: ${String(result.error)}` : "."
|
|
77
|
-
}`,
|
|
78
|
-
);
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
|
|
97
|
+
await runSpawn(command, args, cwd);
|
|
82
98
|
return true;
|
|
83
99
|
} catch (e) {
|
|
100
|
+
if (e instanceof SpawnSignalError) throw e;
|
|
84
101
|
logger.error(`Installation failed: ${String(e)}`);
|
|
85
102
|
return false;
|
|
86
103
|
}
|