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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Pipeline.d.ts +2 -4
  3. package/dist/Pipeline.d.ts.map +1 -1
  4. package/dist/Pipeline.js +2 -2
  5. package/dist/Pipeline.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/logging/CPUConsole.d.ts +1 -1
  10. package/dist/logging/CPUConsole.d.ts.map +1 -1
  11. package/dist/logging/CPUConsole.js +9 -4
  12. package/dist/logging/CPUConsole.js.map +1 -1
  13. package/dist/logging/console-i18n.d.ts +1 -1
  14. package/dist/logging/console-i18n.d.ts.map +1 -1
  15. package/dist/logging/console-i18n.js +0 -3
  16. package/dist/logging/console-i18n.js.map +1 -1
  17. package/dist/logging/stack-parser.d.ts +9 -0
  18. package/dist/logging/stack-parser.d.ts.map +1 -1
  19. package/dist/logging/stack-parser.js +157 -40
  20. package/dist/logging/stack-parser.js.map +1 -1
  21. package/dist/plugins/vite-call-source.d.ts +33 -0
  22. package/dist/plugins/vite-call-source.d.ts.map +1 -0
  23. package/dist/plugins/vite-call-source.js +312 -0
  24. package/dist/plugins/vite-call-source.js.map +1 -0
  25. package/dist/types.d.ts +12 -0
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/vite.d.ts +2 -0
  28. package/dist/vite.d.ts.map +1 -0
  29. package/dist/vite.js +2 -0
  30. package/dist/vite.js.map +1 -0
  31. package/package.json +5 -1
  32. package/src/Pipeline.ts +4 -4
  33. package/src/debug.ts +3 -0
  34. package/src/index.ts +1 -0
  35. package/src/logging/CPUConsole.ts +692 -688
  36. package/src/logging/console-i18n.ts +157 -160
  37. package/src/logging/runtime.ts +3 -0
  38. package/src/logging/stack-parser.ts +300 -168
  39. package/src/plugins/vite-call-source.ts +388 -0
  40. package/src/types.ts +13 -0
  41. 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
  */
package/src/vite.ts ADDED
@@ -0,0 +1,5 @@
1
+ export {
2
+ frontCpuCallSourcePlugin,
3
+ injectCallSourceToDispatch,
4
+ type FrontCpuCallSourcePluginOptions,
5
+ } from './plugins/vite-call-source'