front-cpu 0.1.2 → 0.1.4
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/dist/Pipeline.d.ts +2 -4
- package/dist/Pipeline.d.ts.map +1 -1
- package/dist/Pipeline.js +2 -2
- package/dist/Pipeline.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/CPUConsole.d.ts +1 -1
- package/dist/logging/CPUConsole.d.ts.map +1 -1
- package/dist/logging/CPUConsole.js +9 -4
- package/dist/logging/CPUConsole.js.map +1 -1
- package/dist/logging/console-i18n.d.ts +1 -1
- package/dist/logging/console-i18n.d.ts.map +1 -1
- package/dist/logging/console-i18n.js +0 -3
- package/dist/logging/console-i18n.js.map +1 -1
- package/dist/logging/stack-parser.d.ts +9 -0
- package/dist/logging/stack-parser.d.ts.map +1 -1
- package/dist/logging/stack-parser.js +157 -40
- package/dist/logging/stack-parser.js.map +1 -1
- package/dist/plugins/vite-call-source.d.ts +33 -0
- package/dist/plugins/vite-call-source.d.ts.map +1 -0
- package/dist/plugins/vite-call-source.js +312 -0
- package/dist/plugins/vite-call-source.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vite.d.ts +2 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +2 -0
- package/dist/vite.js.map +1 -0
- package/package.json +5 -1
- package/src/Pipeline.ts +4 -4
- package/src/debug.ts +3 -0
- package/src/index.ts +1 -0
- package/src/logging/CPUConsole.ts +692 -688
- package/src/logging/console-i18n.ts +157 -160
- package/src/logging/runtime.ts +3 -0
- package/src/logging/stack-parser.ts +300 -168
- package/src/plugins/vite-call-source.ts +388 -0
- package/src/types.ts +13 -0
- package/src/vite.ts +5 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import type { Plugin } from 'vite'
|
|
2
|
+
|
|
3
|
+
export interface FrontCpuCallSourcePluginOptions {
|
|
4
|
+
/**
|
|
5
|
+
* 是否启用插件
|
|
6
|
+
* 默认 true
|
|
7
|
+
*/
|
|
8
|
+
enabled?: boolean
|
|
9
|
+
/**
|
|
10
|
+
* 目标包名(默认 ['front-cpu', 'front-cpu/debug'])
|
|
11
|
+
*/
|
|
12
|
+
packageNames?: string[]
|
|
13
|
+
/**
|
|
14
|
+
* 生效阶段
|
|
15
|
+
* - serve: 仅开发环境(默认)
|
|
16
|
+
* - build: 仅构建
|
|
17
|
+
* - all: 开发和构建都启用
|
|
18
|
+
*/
|
|
19
|
+
apply?: 'serve' | 'build' | 'all'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface InjectContext {
|
|
23
|
+
id: string
|
|
24
|
+
packageNames: string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Edit {
|
|
28
|
+
start: number
|
|
29
|
+
end: number
|
|
30
|
+
value: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ParsedArg {
|
|
34
|
+
text: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SUPPORTED_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs|vue)$/i
|
|
38
|
+
const CALL_SOURCE_MARKER_RE = /\bcallSource\s*:/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 编译期注入 front-cpu dispatch 调用来源
|
|
42
|
+
*/
|
|
43
|
+
export function frontCpuCallSourcePlugin(
|
|
44
|
+
options: FrontCpuCallSourcePluginOptions = {}
|
|
45
|
+
): Plugin {
|
|
46
|
+
const {
|
|
47
|
+
enabled = true,
|
|
48
|
+
packageNames = ['front-cpu', 'front-cpu/debug'],
|
|
49
|
+
apply = 'serve',
|
|
50
|
+
} = options
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
name: 'front-cpu-call-source',
|
|
54
|
+
enforce: 'post',
|
|
55
|
+
apply(_, env) {
|
|
56
|
+
if (!enabled) return false
|
|
57
|
+
if (apply === 'all') return true
|
|
58
|
+
if (apply === 'serve') return env.command === 'serve'
|
|
59
|
+
return env.command === 'build'
|
|
60
|
+
},
|
|
61
|
+
transform(code, id) {
|
|
62
|
+
if (!shouldTransform(id)) return null
|
|
63
|
+
const transformed = injectCallSourceToDispatch(code, {
|
|
64
|
+
id,
|
|
65
|
+
packageNames,
|
|
66
|
+
})
|
|
67
|
+
if (transformed === code) return null
|
|
68
|
+
return {
|
|
69
|
+
code: transformed,
|
|
70
|
+
map: null,
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 纯函数:对单个模块代码进行注入改写(便于测试)
|
|
78
|
+
*/
|
|
79
|
+
export function injectCallSourceToDispatch(code: string, context: InjectContext): string {
|
|
80
|
+
const pipelineVars = collectPipelineVariables(code, context.packageNames)
|
|
81
|
+
if (pipelineVars.size === 0) return code
|
|
82
|
+
|
|
83
|
+
const edits: Edit[] = []
|
|
84
|
+
const normalizedId = normalizeInjectedFilePath(context.id)
|
|
85
|
+
|
|
86
|
+
for (const variable of pipelineVars) {
|
|
87
|
+
const pattern = new RegExp(
|
|
88
|
+
`\\b${escapeRegExp(variable)}\\s*(?:\\?\\.)?\\s*\\.\\s*dispatch\\s*\\(`,
|
|
89
|
+
'g'
|
|
90
|
+
)
|
|
91
|
+
let match: RegExpExecArray | null
|
|
92
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
93
|
+
const matchedText = match[0] || ''
|
|
94
|
+
const openParenOffset = matchedText.lastIndexOf('(')
|
|
95
|
+
if (openParenOffset < 0) continue
|
|
96
|
+
|
|
97
|
+
const openParenIndex = match.index + openParenOffset
|
|
98
|
+
const closeParenIndex = findMatchingParen(code, openParenIndex)
|
|
99
|
+
if (closeParenIndex < 0) continue
|
|
100
|
+
|
|
101
|
+
const argsText = code.slice(openParenIndex + 1, closeParenIndex)
|
|
102
|
+
const args = splitTopLevelArgs(argsText)
|
|
103
|
+
if (args.length < 2) continue
|
|
104
|
+
|
|
105
|
+
// 用户已显式传 callSource,不覆盖
|
|
106
|
+
if (args.length >= 4 && CALL_SOURCE_MARKER_RE.test(args[3]?.text || '')) {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const location = indexToLineColumn(code, match.index)
|
|
111
|
+
const callSourceLiteral = createCallSourceLiteral(normalizedId, location.line, location.column)
|
|
112
|
+
const nextArgsText = buildInjectedArgs(args, callSourceLiteral)
|
|
113
|
+
if (nextArgsText === argsText) continue
|
|
114
|
+
|
|
115
|
+
edits.push({
|
|
116
|
+
start: openParenIndex + 1,
|
|
117
|
+
end: closeParenIndex,
|
|
118
|
+
value: nextArgsText,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (edits.length === 0) return code
|
|
124
|
+
return applyEdits(code, edits)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function shouldTransform(id: string): boolean {
|
|
128
|
+
const clean = normalizeFileId(id)
|
|
129
|
+
if (!SUPPORTED_EXT_RE.test(clean) && !id.includes('?vue&type=script')) {
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
if (clean.includes('/node_modules/')) {
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function collectPipelineVariables(code: string, packageNames: string[]): Set<string> {
|
|
139
|
+
const constructors = collectPipelineConstructors(code, packageNames)
|
|
140
|
+
if (constructors.size === 0) return new Set()
|
|
141
|
+
|
|
142
|
+
const variables = new Set<string>()
|
|
143
|
+
for (const ctor of constructors) {
|
|
144
|
+
const declarationRegex = new RegExp(
|
|
145
|
+
`\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*new\\s+${escapeRegExp(ctor)}\\s*\\(`,
|
|
146
|
+
'g'
|
|
147
|
+
)
|
|
148
|
+
let m: RegExpExecArray | null
|
|
149
|
+
while ((m = declarationRegex.exec(code)) !== null) {
|
|
150
|
+
if (m[1]) variables.add(m[1])
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const assignmentRegex = new RegExp(
|
|
154
|
+
`\\b([A-Za-z_$][\\w$]*)\\s*=\\s*new\\s+${escapeRegExp(ctor)}\\s*\\(`,
|
|
155
|
+
'g'
|
|
156
|
+
)
|
|
157
|
+
while ((m = assignmentRegex.exec(code)) !== null) {
|
|
158
|
+
if (m[1]) variables.add(m[1])
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return variables
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function collectPipelineConstructors(code: string, packageNames: string[]): Set<string> {
|
|
166
|
+
const constructors = new Set<string>()
|
|
167
|
+
const importRegex = /import\s+(?:type\s+)?{([\s\S]*?)}\s+from\s+['"]([^'"]+)['"]/g
|
|
168
|
+
|
|
169
|
+
let match: RegExpExecArray | null
|
|
170
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
171
|
+
const importedFrom = match[2]
|
|
172
|
+
if (!importedFrom || !packageNames.includes(importedFrom)) continue
|
|
173
|
+
|
|
174
|
+
const specifiers = (match[1] || '').split(',')
|
|
175
|
+
for (const specifier of specifiers) {
|
|
176
|
+
const trimmed = specifier.trim()
|
|
177
|
+
if (!trimmed) continue
|
|
178
|
+
|
|
179
|
+
// Pipeline
|
|
180
|
+
if (trimmed === 'Pipeline') {
|
|
181
|
+
constructors.add('Pipeline')
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Pipeline as Xxx
|
|
186
|
+
const aliasMatch = trimmed.match(/^Pipeline\s+as\s+([A-Za-z_$][\w$]*)$/)
|
|
187
|
+
if (aliasMatch?.[1]) {
|
|
188
|
+
constructors.add(aliasMatch[1])
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return constructors
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function createCallSourceLiteral(file: string, line: number, column: number): string {
|
|
197
|
+
return `{ file: ${JSON.stringify(file)}, line: ${line}, column: ${column}, raw: 'compile-time' }`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildInjectedArgs(args: ParsedArg[], callSourceLiteral: string): string {
|
|
201
|
+
const normalized = args.map((arg) => arg.text.trim())
|
|
202
|
+
|
|
203
|
+
if (normalized.length >= 4) {
|
|
204
|
+
const existingOptions = normalized[3]
|
|
205
|
+
normalized[3] = `{ ...(${existingOptions || 'undefined'}), callSource: ${callSourceLiteral} }`
|
|
206
|
+
return normalized.join(', ')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (normalized.length === 3) {
|
|
210
|
+
normalized.push(`{ callSource: ${callSourceLiteral} }`)
|
|
211
|
+
return normalized.join(', ')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (normalized.length === 2) {
|
|
215
|
+
normalized.push('undefined')
|
|
216
|
+
normalized.push(`{ callSource: ${callSourceLiteral} }`)
|
|
217
|
+
return normalized.join(', ')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return args.map((arg) => arg.text).join(', ')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function splitTopLevelArgs(input: string): ParsedArg[] {
|
|
224
|
+
const args: ParsedArg[] = []
|
|
225
|
+
let start = 0
|
|
226
|
+
let parenDepth = 0
|
|
227
|
+
let bracketDepth = 0
|
|
228
|
+
let braceDepth = 0
|
|
229
|
+
let quote: '"' | "'" | '`' | null = null
|
|
230
|
+
let escaped = false
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < input.length; i++) {
|
|
233
|
+
const ch = input[i]
|
|
234
|
+
if (!ch) continue
|
|
235
|
+
|
|
236
|
+
if (quote) {
|
|
237
|
+
if (escaped) {
|
|
238
|
+
escaped = false
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
if (ch === '\\') {
|
|
242
|
+
escaped = true
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
if (ch === quote) {
|
|
246
|
+
quote = null
|
|
247
|
+
}
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
252
|
+
quote = ch
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (ch === '(') parenDepth++
|
|
257
|
+
else if (ch === ')') parenDepth--
|
|
258
|
+
else if (ch === '[') bracketDepth++
|
|
259
|
+
else if (ch === ']') bracketDepth--
|
|
260
|
+
else if (ch === '{') braceDepth++
|
|
261
|
+
else if (ch === '}') braceDepth--
|
|
262
|
+
|
|
263
|
+
if (ch === ',' && parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) {
|
|
264
|
+
args.push({ text: input.slice(start, i) })
|
|
265
|
+
start = i + 1
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const tail = input.slice(start)
|
|
270
|
+
if (tail.trim().length > 0 || input.trim().length > 0) {
|
|
271
|
+
args.push({ text: tail })
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return args
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function findMatchingParen(code: string, openIndex: number): number {
|
|
278
|
+
let depth = 0
|
|
279
|
+
let quote: '"' | "'" | '`' | null = null
|
|
280
|
+
let escaped = false
|
|
281
|
+
let inLineComment = false
|
|
282
|
+
let inBlockComment = false
|
|
283
|
+
|
|
284
|
+
for (let i = openIndex; i < code.length; i++) {
|
|
285
|
+
const ch = code[i]
|
|
286
|
+
const next = code[i + 1]
|
|
287
|
+
if (!ch) continue
|
|
288
|
+
|
|
289
|
+
if (inLineComment) {
|
|
290
|
+
if (ch === '\n') inLineComment = false
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (inBlockComment) {
|
|
295
|
+
if (ch === '*' && next === '/') {
|
|
296
|
+
inBlockComment = false
|
|
297
|
+
i++
|
|
298
|
+
}
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (quote) {
|
|
303
|
+
if (escaped) {
|
|
304
|
+
escaped = false
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
if (ch === '\\') {
|
|
308
|
+
escaped = true
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
if (ch === quote) {
|
|
312
|
+
quote = null
|
|
313
|
+
}
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (ch === '/' && next === '/') {
|
|
318
|
+
inLineComment = true
|
|
319
|
+
i++
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (ch === '/' && next === '*') {
|
|
324
|
+
inBlockComment = true
|
|
325
|
+
i++
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
330
|
+
quote = ch
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (ch === '(') {
|
|
335
|
+
depth++
|
|
336
|
+
continue
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (ch === ')') {
|
|
340
|
+
depth--
|
|
341
|
+
if (depth === 0) return i
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return -1
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function indexToLineColumn(code: string, index: number): { line: number; column: number } {
|
|
349
|
+
let line = 1
|
|
350
|
+
let lineStart = 0
|
|
351
|
+
for (let i = 0; i < index; i++) {
|
|
352
|
+
if (code[i] === '\n') {
|
|
353
|
+
line++
|
|
354
|
+
lineStart = i + 1
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
line,
|
|
359
|
+
column: index - lineStart + 1,
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function applyEdits(code: string, edits: Edit[]): string {
|
|
364
|
+
const sorted = [...edits].sort((a, b) => b.start - a.start)
|
|
365
|
+
let result = code
|
|
366
|
+
for (const edit of sorted) {
|
|
367
|
+
result = result.slice(0, edit.start) + edit.value + result.slice(edit.end)
|
|
368
|
+
}
|
|
369
|
+
return result
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function normalizeFileId(id: string): string {
|
|
373
|
+
const withoutQuery = id.split('?')[0] || id
|
|
374
|
+
return withoutQuery.replace(/\\/g, '/')
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function normalizeInjectedFilePath(id: string): string {
|
|
378
|
+
const normalized = normalizeFileId(id)
|
|
379
|
+
const srcIndex = normalized.lastIndexOf('/src/')
|
|
380
|
+
if (srcIndex >= 0) {
|
|
381
|
+
return normalized.slice(srcIndex + 1)
|
|
382
|
+
}
|
|
383
|
+
return normalized
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function escapeRegExp(value: string): string {
|
|
387
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
388
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -50,6 +50,19 @@ export interface InstructionContext {
|
|
|
50
50
|
callSource?: CallSource
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* dispatch 可选参数
|
|
55
|
+
*/
|
|
56
|
+
export interface DispatchOptions {
|
|
57
|
+
/** 指令标签(用于批量取消/筛选) */
|
|
58
|
+
tags?: string[]
|
|
59
|
+
/**
|
|
60
|
+
* 调用源信息(优先使用编译期注入)
|
|
61
|
+
* 未提供时会在运行时 fallback 到 Error.stack 解析
|
|
62
|
+
*/
|
|
63
|
+
callSource?: CallSource
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
/**
|
|
54
67
|
* WB阶段执行信息
|
|
55
68
|
*/
|