@weapp-vite/miniprogram-automator 1.2.23 → 1.3.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 +22 -0
- package/dist/index.d.mts +36 -3
- package/dist/index.mjs +320 -164
- package/dist/{launch-CSjvON-f.mjs → launch-anJvfi-6.mjs} +1025 -332
- package/dist/operation.d.mts +64 -0
- package/dist/operation.mjs +233 -0
- package/package.json +15 -4
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/operation/errors.d.ts
|
|
2
|
+
export declare const OPERATION_TIMEOUT_CODE = "DEVTOOLS_OPERATION_TIMEOUT";
|
|
3
|
+
export type OperationErrorCategory = 'invalid-path' | 'login-required' | 'service-unavailable' | 'connection-transient' | 'protocol-rejected' | 'timeout' | 'canceled' | 'unknown';
|
|
4
|
+
/** 根据原始错误链分类;未知错误默认不重试,路径和登录错误优先。 */
|
|
5
|
+
export declare function classifyOperationError(error: unknown): OperationErrorCategory;
|
|
6
|
+
export declare function isRecoverableOperationError(error: unknown): boolean;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/operation/login.d.ts
|
|
9
|
+
/** 只采用原生 CLI 明确且不矛盾的 login 布尔字段,不推断日志语义。 */
|
|
10
|
+
export declare function readWechatLoginState(stdout: string): boolean | undefined;
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/operation/index.d.ts
|
|
13
|
+
export interface OperationStepOptions<T> {
|
|
14
|
+
stage?: string;
|
|
15
|
+
waitForExit?: boolean;
|
|
16
|
+
disposeLate?: (value: T) => void | Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export interface OperationDiagnostics {
|
|
19
|
+
version: 1;
|
|
20
|
+
inner?: OperationDiagnostics;
|
|
21
|
+
category?: OperationErrorCategory;
|
|
22
|
+
stage: string;
|
|
23
|
+
lastSuccessfulStage?: string;
|
|
24
|
+
elapsedMs: number;
|
|
25
|
+
remainingMs: number;
|
|
26
|
+
attempts: number;
|
|
27
|
+
pendingOperations: number;
|
|
28
|
+
cleanup: Array<{
|
|
29
|
+
resource: string;
|
|
30
|
+
status: 'released' | 'failed' | 'pending';
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
/** 一次操作共享绝对截止时间;超时取消底层 I/O,并隔离迟到结果。 */
|
|
34
|
+
export declare class OperationLifecycle {
|
|
35
|
+
readonly controller: AbortController;
|
|
36
|
+
readonly signal: AbortSignal;
|
|
37
|
+
readonly deadlineAt: number;
|
|
38
|
+
private cleanupDeadlineAt;
|
|
39
|
+
private readonly startedAt;
|
|
40
|
+
private readonly pendingExits;
|
|
41
|
+
private readonly cleanups;
|
|
42
|
+
private readonly cleanupResults;
|
|
43
|
+
private readonly timeoutError;
|
|
44
|
+
private stage;
|
|
45
|
+
private lastSuccessfulStage?;
|
|
46
|
+
private attempts;
|
|
47
|
+
private lastFailure;
|
|
48
|
+
constructor(timeoutMs: number, label: string, signal?: AbortSignal);
|
|
49
|
+
private detachParent;
|
|
50
|
+
get timedOut(): boolean;
|
|
51
|
+
get diagnostics(): OperationDiagnostics;
|
|
52
|
+
attempt(): void;
|
|
53
|
+
recordFailure(error: unknown): void;
|
|
54
|
+
throwIfAborted(): void;
|
|
55
|
+
remainingMs(cap?: number): number;
|
|
56
|
+
own(cleanup: () => void | Promise<void>, resource?: string): () => boolean;
|
|
57
|
+
private release;
|
|
58
|
+
step<T>(factory: () => Promise<T>, options?: OperationStepOptions<T>): Promise<T>;
|
|
59
|
+
phase<T>(timeoutMs: number, label: string, factory: (phase: OperationLifecycle) => Promise<T>): Promise<T>;
|
|
60
|
+
pause(ms: number): Promise<void>;
|
|
61
|
+
private cleanup;
|
|
62
|
+
run<T>(factory: (lifecycle: OperationLifecycle) => Promise<T>): Promise<T>;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
//#region src/operation/errors.ts
|
|
3
|
+
const OPERATION_TIMEOUT_CODE = "DEVTOOLS_OPERATION_TIMEOUT";
|
|
4
|
+
/** 根据原始错误链分类;未知错误默认不重试,路径和登录错误优先。 */
|
|
5
|
+
function classifyOperationError(error) {
|
|
6
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7
|
+
const parts = [];
|
|
8
|
+
let originalText = "";
|
|
9
|
+
let current = error;
|
|
10
|
+
while (current && typeof current === "object" && !seen.has(current)) {
|
|
11
|
+
seen.add(current);
|
|
12
|
+
const item = current;
|
|
13
|
+
const values = [
|
|
14
|
+
item.name,
|
|
15
|
+
item.code === 10 ? "code: 10" : item.code,
|
|
16
|
+
item.message,
|
|
17
|
+
item.stderr,
|
|
18
|
+
item.stdout
|
|
19
|
+
].filter((part) => typeof part === "string");
|
|
20
|
+
parts.push(...values);
|
|
21
|
+
originalText = values.join("\n");
|
|
22
|
+
current = item.cause;
|
|
23
|
+
}
|
|
24
|
+
const text = parts.join("\n");
|
|
25
|
+
if (/ENOENT|EACCES|not found|doesn't exist|cliPath is not correct|projectPath is not provided/i.test(text)) return "invalid-path";
|
|
26
|
+
if (/DEVTOOLS_LOGIN_REQUIRED|需要重新登录|need\s+re-?login|code\s*[:=]\s*10\b/i.test(text)) return "login-required";
|
|
27
|
+
if (/unexpected server response|requires at least version|protocol.*reject|unsupported response|invalid JSON|invalid autoPort/i.test(text)) return "protocol-rejected";
|
|
28
|
+
if (/DEVTOOLS_OPERATION_TIMEOUT|DEVTOOLS_PROTOCOL_TIMEOUT|timed? ?out/i.test(text)) return "timeout";
|
|
29
|
+
if (/ECONNREFUSED|service port.*disabled|http port is open/i.test(text)) return "service-unavailable";
|
|
30
|
+
if (/ECONNRESET|EPIPE|Extension context invalidated|Failed connecting to|closed before handshake/i.test(originalText)) return "connection-transient";
|
|
31
|
+
if (/AbortError|ABORT_ERR|cancel(?:led|ed)/i.test(text)) return "canceled";
|
|
32
|
+
return "unknown";
|
|
33
|
+
}
|
|
34
|
+
function isRecoverableOperationError(error) {
|
|
35
|
+
const category = classifyOperationError(error);
|
|
36
|
+
return category === "connection-transient" || category === "service-unavailable" || category === "timeout";
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/operation/login.ts
|
|
40
|
+
/** 只采用原生 CLI 明确且不矛盾的 login 布尔字段,不推断日志语义。 */
|
|
41
|
+
function readWechatLoginState(stdout) {
|
|
42
|
+
const results = /* @__PURE__ */ new Set();
|
|
43
|
+
for (const line of stripVTControlCharacters(stdout).split(/\r?\n/)) try {
|
|
44
|
+
const value = JSON.parse(line);
|
|
45
|
+
if (value && typeof value === "object" && "login" in value && typeof value.login === "boolean") results.add(value.login);
|
|
46
|
+
} catch {}
|
|
47
|
+
return results.size === 1 ? [...results][0] : void 0;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/operation/index.ts
|
|
51
|
+
const errorDiagnostics = /* @__PURE__ */ new WeakMap();
|
|
52
|
+
/** 一次操作共享绝对截止时间;超时取消底层 I/O,并隔离迟到结果。 */
|
|
53
|
+
var OperationLifecycle = class OperationLifecycle {
|
|
54
|
+
controller = new AbortController();
|
|
55
|
+
signal = this.controller.signal;
|
|
56
|
+
deadlineAt;
|
|
57
|
+
cleanupDeadlineAt;
|
|
58
|
+
startedAt = performance.now();
|
|
59
|
+
pendingExits = /* @__PURE__ */ new Set();
|
|
60
|
+
cleanups = /* @__PURE__ */ new Map();
|
|
61
|
+
cleanupResults = [];
|
|
62
|
+
timeoutError;
|
|
63
|
+
stage = "initialization";
|
|
64
|
+
lastSuccessfulStage;
|
|
65
|
+
attempts = 0;
|
|
66
|
+
lastFailure;
|
|
67
|
+
constructor(timeoutMs, label, signal) {
|
|
68
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new RangeError("Operation timeout must be a positive finite number");
|
|
69
|
+
this.deadlineAt = this.startedAt + timeoutMs;
|
|
70
|
+
this.cleanupDeadlineAt = this.deadlineAt;
|
|
71
|
+
this.timeoutError = Object.assign(/* @__PURE__ */ new Error(`Timeout in ${label} after ${timeoutMs}ms`), { code: OPERATION_TIMEOUT_CODE });
|
|
72
|
+
if (signal) {
|
|
73
|
+
const abort = () => this.controller.abort(signal.reason);
|
|
74
|
+
if (signal.aborted) abort();
|
|
75
|
+
else {
|
|
76
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
77
|
+
this.detachParent = () => signal.removeEventListener("abort", abort);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
detachParent = () => {};
|
|
82
|
+
get timedOut() {
|
|
83
|
+
return this.signal.aborted && this.signal.reason === this.timeoutError;
|
|
84
|
+
}
|
|
85
|
+
get diagnostics() {
|
|
86
|
+
return {
|
|
87
|
+
version: 1,
|
|
88
|
+
stage: this.stage,
|
|
89
|
+
lastSuccessfulStage: this.lastSuccessfulStage,
|
|
90
|
+
elapsedMs: Math.max(0, performance.now() - this.startedAt),
|
|
91
|
+
remainingMs: Math.max(0, this.deadlineAt - performance.now()),
|
|
92
|
+
attempts: this.attempts,
|
|
93
|
+
pendingOperations: this.pendingExits.size,
|
|
94
|
+
cleanup: this.cleanupResults.map((entry) => ({ ...entry }))
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
attempt() {
|
|
98
|
+
this.throwIfAborted();
|
|
99
|
+
this.attempts += 1;
|
|
100
|
+
}
|
|
101
|
+
recordFailure(error) {
|
|
102
|
+
if (error !== this.timeoutError) this.lastFailure = error;
|
|
103
|
+
}
|
|
104
|
+
throwIfAborted() {
|
|
105
|
+
if (!this.signal.aborted && performance.now() >= this.deadlineAt) this.controller.abort(this.timeoutError);
|
|
106
|
+
this.signal.throwIfAborted();
|
|
107
|
+
}
|
|
108
|
+
remainingMs(cap = Number.POSITIVE_INFINITY) {
|
|
109
|
+
this.throwIfAborted();
|
|
110
|
+
return Math.max(1, Math.min(cap, Math.ceil(this.deadlineAt - performance.now())));
|
|
111
|
+
}
|
|
112
|
+
own(cleanup, resource = "resource") {
|
|
113
|
+
this.cleanups.set(cleanup, resource);
|
|
114
|
+
if (this.signal.aborted) this.release(cleanup, resource);
|
|
115
|
+
this.throwIfAborted();
|
|
116
|
+
return () => this.cleanups.delete(cleanup);
|
|
117
|
+
}
|
|
118
|
+
release(cleanup, resource) {
|
|
119
|
+
this.cleanups.delete(cleanup);
|
|
120
|
+
const entry = {
|
|
121
|
+
resource,
|
|
122
|
+
status: "pending"
|
|
123
|
+
};
|
|
124
|
+
this.cleanupResults.push(entry);
|
|
125
|
+
try {
|
|
126
|
+
const result = cleanup();
|
|
127
|
+
return Promise.resolve(result).then(() => {
|
|
128
|
+
entry.status = "released";
|
|
129
|
+
}, () => {
|
|
130
|
+
entry.status = "failed";
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
entry.status = "failed";
|
|
134
|
+
return Promise.resolve();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async step(factory, options = {}) {
|
|
138
|
+
this.throwIfAborted();
|
|
139
|
+
const stage = options.stage;
|
|
140
|
+
if (stage) this.stage = stage;
|
|
141
|
+
let removeAbort = () => {};
|
|
142
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
143
|
+
const onAbort = () => reject(this.signal.reason);
|
|
144
|
+
this.signal.addEventListener("abort", onAbort, { once: true });
|
|
145
|
+
removeAbort = () => this.signal.removeEventListener("abort", onAbort);
|
|
146
|
+
});
|
|
147
|
+
const operation = (async () => {
|
|
148
|
+
this.throwIfAborted();
|
|
149
|
+
return await factory();
|
|
150
|
+
})().then(async (value) => {
|
|
151
|
+
try {
|
|
152
|
+
this.throwIfAborted();
|
|
153
|
+
} catch (error) {
|
|
154
|
+
await options.disposeLate?.(value);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
if (stage) this.lastSuccessfulStage = stage;
|
|
158
|
+
return value;
|
|
159
|
+
});
|
|
160
|
+
if (options.waitForExit) this.pendingExits.add(operation);
|
|
161
|
+
operation.finally(() => this.pendingExits.delete(operation)).catch(() => {});
|
|
162
|
+
try {
|
|
163
|
+
return await Promise.race([operation, aborted]);
|
|
164
|
+
} finally {
|
|
165
|
+
removeAbort();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async phase(timeoutMs, label, factory) {
|
|
169
|
+
const phase = new OperationLifecycle(this.remainingMs(timeoutMs), label, this.signal);
|
|
170
|
+
phase.cleanupDeadlineAt = Math.min(this.deadlineAt, phase.deadlineAt + 250);
|
|
171
|
+
return await this.step(() => phase.run(() => phase.step(() => factory(phase), { waitForExit: true })), {
|
|
172
|
+
stage: label,
|
|
173
|
+
waitForExit: true
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async pause(ms) {
|
|
177
|
+
let timer;
|
|
178
|
+
try {
|
|
179
|
+
await this.step(() => new Promise((resolve) => {
|
|
180
|
+
timer = setTimeout(resolve, Math.min(ms, this.remainingMs()));
|
|
181
|
+
}));
|
|
182
|
+
} finally {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async cleanup() {
|
|
187
|
+
const pending = [...this.pendingExits, ...[...this.cleanups].map(([cleanup, resource]) => this.release(cleanup, resource))];
|
|
188
|
+
if (!pending.length) return;
|
|
189
|
+
if (performance.now() >= this.cleanupDeadlineAt) {
|
|
190
|
+
await Promise.resolve();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
let timer;
|
|
194
|
+
try {
|
|
195
|
+
await Promise.race([Promise.allSettled(pending), new Promise((resolve) => {
|
|
196
|
+
timer = setTimeout(resolve, Math.max(0, Math.min(250, this.cleanupDeadlineAt - performance.now())));
|
|
197
|
+
})]);
|
|
198
|
+
} finally {
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async run(factory) {
|
|
203
|
+
let timer;
|
|
204
|
+
try {
|
|
205
|
+
timer = setTimeout(() => this.controller.abort(this.timeoutError), this.remainingMs());
|
|
206
|
+
return await this.step(() => factory(this));
|
|
207
|
+
} catch (error) {
|
|
208
|
+
this.controller.abort(error);
|
|
209
|
+
await this.cleanup();
|
|
210
|
+
if (error === this.timeoutError && this.lastFailure !== void 0) Object.defineProperty(error, "cause", {
|
|
211
|
+
value: this.lastFailure,
|
|
212
|
+
configurable: true
|
|
213
|
+
});
|
|
214
|
+
if (error instanceof Error && Object.isExtensible(error)) {
|
|
215
|
+
const inner = errorDiagnostics.get(error);
|
|
216
|
+
const operation = {
|
|
217
|
+
...this.diagnostics,
|
|
218
|
+
category: classifyOperationError(error),
|
|
219
|
+
...inner ? { inner } : {}
|
|
220
|
+
};
|
|
221
|
+
errorDiagnostics.set(error, operation);
|
|
222
|
+
Object.assign(error, { operation });
|
|
223
|
+
}
|
|
224
|
+
throw error;
|
|
225
|
+
} finally {
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
this.detachParent();
|
|
228
|
+
this.cleanups.clear();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
//#endregion
|
|
233
|
+
export { OPERATION_TIMEOUT_CODE, OperationLifecycle, classifyOperationError, isRecoverableOperationError, readWechatLoginState };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weapp-vite/miniprogram-automator",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"description": "完全兼容微信 miniprogram-automator 的现代化替代实现",
|
|
6
6
|
"author": "ice breaker <1324318532@qq.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
"types": "./dist/index.d.mts",
|
|
32
32
|
"import": "./dist/index.mjs"
|
|
33
33
|
},
|
|
34
|
+
"./operation": {
|
|
35
|
+
"types": "./dist/operation.d.mts",
|
|
36
|
+
"import": "./dist/operation.mjs"
|
|
37
|
+
},
|
|
34
38
|
"./package.json": "./package.json"
|
|
35
39
|
},
|
|
36
40
|
"main": "./dist/index.mjs",
|
|
@@ -44,7 +48,7 @@
|
|
|
44
48
|
"node": "^20.19.0 || >=22.12.0"
|
|
45
49
|
},
|
|
46
50
|
"dependencies": {
|
|
47
|
-
"@weapp-vite/qr": "1.1.
|
|
51
|
+
"@weapp-vite/qr": "1.1.9",
|
|
48
52
|
"obug": "^3.0.0",
|
|
49
53
|
"ws": "^8.22.0"
|
|
50
54
|
},
|
|
@@ -53,15 +57,22 @@
|
|
|
53
57
|
"registry": "https://registry.npmjs.org"
|
|
54
58
|
},
|
|
55
59
|
"devDependencies": {
|
|
56
|
-
"@types/ws": "^8.18.
|
|
60
|
+
"@types/ws": "^8.18.2",
|
|
57
61
|
"sharp": "^0.35.5"
|
|
58
62
|
},
|
|
63
|
+
"tsd": {
|
|
64
|
+
"compilerOptions": {
|
|
65
|
+
"module": "NodeNext",
|
|
66
|
+
"moduleResolution": "NodeNext"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
59
69
|
"scripts": {
|
|
60
70
|
"dev": "tsdown -w --sourcemap",
|
|
61
71
|
"build": "tsdown",
|
|
62
72
|
"test": "vitest run --config ./vitest.config.ts",
|
|
63
73
|
"test:dev": "vitest --config ./vitest.config.ts",
|
|
64
|
-
"test:types": "pnpm build &&
|
|
74
|
+
"test:types": "pnpm build && pnpm run test:types:check",
|
|
75
|
+
"test:types:check": "tsd --files \"test-d/**/*.test-d.ts\"",
|
|
65
76
|
"typecheck": "tsc --noEmit -p ./tsconfig.json",
|
|
66
77
|
"lint": "eslint .",
|
|
67
78
|
"lint:fix": "eslint . --fix"
|