@hhhpyb/system-assistant-client 1.3.6 → 1.3.7

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 CHANGED
@@ -24,7 +24,7 @@ export default {
24
24
 
25
25
  无需在 `nuxt.config.js` 中配置 `SA_GUIDE_APP_KEY`。模块会在构建时自动将应用标识注入 Nuxt 运行时配置。
26
26
 
27
- 模块还会自动接入 `@hhhpyb/guide-compiler`:Nuxt 2/Bridge 的 Webpack 与 Vite 客户端构建均会在不重复挂载的前提下生成 `.nuxt/sa-guide-manifest.json`,无需在业务项目中配置 compiler loaderVite plugin。
27
+ 模块已内置 guide compilerNuxt 2/Bridge 的 Webpack 与 Vite 客户端构建均会在不重复挂载的前提下生成 `.nuxt/sa-guide-manifest.json`,无需在业务项目额外安装 compiler 或配置 loaderVite plugin。
28
28
 
29
29
  ### 应用标识推导
30
30
 
@@ -69,7 +69,7 @@ URL 带 `?sa_studio=1` 时自动进入 Studio 点选模式,支持 `sa_app_key`
69
69
 
70
70
  ## 发布(维护者)
71
71
 
72
- 源码:`system-assistant/deployables/sa-guide-sdk/app/v1/system-assistant.client.js`
72
+ 源码:`system-assistant/deployables/sa-guide-sdk/app/v1/system-assistant.client.js`;内置 compiler 权威源码:`system-assistant/packages/guide-compiler/src`。
73
73
 
74
74
  ```bash
75
75
  cd packages/system-assistant-client
@@ -0,0 +1,15 @@
1
+ import { normalizeOptions, transformInlineTemplates, transformTemplate } from './core.js'
2
+
3
+ export function transformAngularTemplate(code, id, options = {}, sink = []) {
4
+ return transformTemplate(code, id, {
5
+ ...normalizeOptions(options),
6
+ framework: 'angular',
7
+ }, sink)
8
+ }
9
+
10
+ export function transformAngularComponentTs(code, id, options = {}, sink = []) {
11
+ return transformInlineTemplates(code, id, {
12
+ ...normalizeOptions(options),
13
+ framework: 'angular',
14
+ }, sink)
15
+ }
@@ -0,0 +1,852 @@
1
+ import crypto from 'node:crypto'
2
+ import path from 'node:path'
3
+
4
+ const DEFAULT_SENSITIVE_WORDS = [
5
+ 'salary', 'amount', 'money', 'payroll', 'approval-result',
6
+ '薪资', '工资', '金额', '审批结论', '人员信息', '身份证', '手机号',
7
+ ]
8
+
9
+ const ACTION_ALIASES = [
10
+ ['add', /^(add|new|create|新增|添加|新建)$/i],
11
+ ['edit', /^(edit|update|modify|编辑|修改)$/i],
12
+ ['detail', /^(detail|details|view|show|info|详情|查看)$/i],
13
+ ['delete', /^(delete|remove|del|删除|移除)$/i],
14
+ ['search', /^(search|query|find|查询|搜索)$/i],
15
+ ['export', /^(export|download|导出|下载)$/i],
16
+ ['import', /^(import|upload|导入|上传)$/i],
17
+ ['save', /^(save|保存)$/i],
18
+ ['submit', /^(submit|提交)$/i],
19
+ ['close', /^(close|cancel|关闭|取消)$/i],
20
+ ['filter', /^(filter|筛选|过滤)$/i],
21
+ ['switch', /^(switch|change|tab|切换)$/i],
22
+ ]
23
+
24
+ const TARGET_TAGS = new Set([
25
+ 'button', 'a', 'input', 'textarea', 'select', 'option', 'label',
26
+ 'table', 'thead', 'tbody', 'th', 'td', 'tr', 'form',
27
+ ])
28
+
29
+ const TARGET_COMPONENT_HINTS = [
30
+ 'button', 'btn', 'input', 'select', 'date', 'picker', 'tab', 'tabs', 'menu',
31
+ 'dialog', 'modal', 'drawer', 'table', 'column', 'form', 'toolbar', 'search',
32
+ ]
33
+
34
+ const VOID_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
35
+
36
+ function findTagCloseIndex(code, start) {
37
+ let quote = null
38
+ for (let i = start + 1; i < code.length; i += 1) {
39
+ const ch = code[i]
40
+ if (quote) {
41
+ if (ch === quote && code[i - 1] !== '\\') quote = null
42
+ continue
43
+ }
44
+ if (ch === '"' || ch === "'") {
45
+ quote = ch
46
+ continue
47
+ }
48
+ if (ch === '>') return i
49
+ }
50
+ return -1
51
+ }
52
+
53
+ function findNextTemplateTag(code, fromIndex = 0) {
54
+ let i = fromIndex
55
+ while (i < code.length) {
56
+ if (code[i] !== '<') {
57
+ i += 1
58
+ continue
59
+ }
60
+ if (code.startsWith('<!--', i)) {
61
+ const end = code.indexOf('-->', i + 4)
62
+ i = end >= 0 ? end + 3 : code.length
63
+ continue
64
+ }
65
+ if (code[i + 1] === '!' || code[i + 1] === '?') {
66
+ const closeIndex = findTagCloseIndex(code, i)
67
+ i = closeIndex >= 0 ? closeIndex + 1 : code.length
68
+ continue
69
+ }
70
+ if (code[i + 1] === '/') {
71
+ const closeTagMatch = code.slice(i + 2).match(/^([A-Za-z][\w.:-]*)/)
72
+ if (closeTagMatch) {
73
+ const tag = closeTagMatch[1]
74
+ const closeIndex = findTagCloseIndex(code, i)
75
+ if (closeIndex < 0) return null
76
+ return { index: i, tag, attrs: '', raw: code.slice(i, closeIndex + 1), closeIndex, isClose: true }
77
+ }
78
+ }
79
+ const nameMatch = code.slice(i + 1).match(/^([A-Za-z][\w.:-]*)/)
80
+ if (!nameMatch) {
81
+ i += 1
82
+ continue
83
+ }
84
+ const tag = nameMatch[1]
85
+ const closeIndex = findTagCloseIndex(code, i)
86
+ if (closeIndex < 0) return null
87
+ const raw = code.slice(i, closeIndex + 1)
88
+ const attrs = raw.slice(1 + tag.length, raw.length - 1)
89
+ return { index: i, tag, attrs, raw, closeIndex, isClose: false }
90
+ }
91
+ return null
92
+ }
93
+
94
+ export function createGuideCompiler(options = {}) {
95
+ const normalized = normalizeOptions(options)
96
+ const collected = []
97
+ return {
98
+ transformVue2(code, id) {
99
+ return transformVueSfc(code, id, { ...normalized, framework: 'vue2' }, collected)
100
+ },
101
+ transformVue3(code, id) {
102
+ return transformVueSfc(code, id, { ...normalized, framework: 'vue3' }, collected)
103
+ },
104
+ transformAngularTemplate(code, id) {
105
+ return transformTemplate(code, id, { ...normalized, framework: 'angular' }, collected)
106
+ },
107
+ transformAngularComponentTs(code, id) {
108
+ return transformInlineTemplates(code, id, { ...normalized, framework: 'angular' }, collected)
109
+ },
110
+ getManifest(extra = {}) {
111
+ return buildManifest(collected, normalized, extra)
112
+ },
113
+ }
114
+ }
115
+
116
+ export function normalizeOptions(options = {}) {
117
+ return {
118
+ enabled: options.enabled !== false,
119
+ appKey: sanitizeSegment(options.appKey || 'app') || 'app',
120
+ idPrefix: sanitizeSegment(options.idPrefix || options.appKey || 'app') || 'app',
121
+ routePrefix: sanitizeSegment(options.routePrefix || '') || '',
122
+ menuResolver: options.menuResolver || null,
123
+ rootDir: options.rootDir || process.cwd(),
124
+ include: options.include || null,
125
+ exclude: options.exclude || null,
126
+ sensitiveWords: [...DEFAULT_SENSITIVE_WORDS, ...(options.sensitiveWords || [])],
127
+ productionDuplicatePolicy: options.productionDuplicatePolicy || 'block_manual',
128
+ manifestOutput: options.manifestOutput || '',
129
+ lowConfidenceThreshold: Number(options.lowConfidenceThreshold || 60),
130
+ }
131
+ }
132
+
133
+ export function transformVueSfc(code, id, options = {}, sink = []) {
134
+ if (!options.enabled) return { code, manifest: buildManifest(sink, options) }
135
+ return replaceTemplateBlocks(code, (template, startOffset) => (
136
+ transformTemplate(template, id, options, sink, startOffset).code
137
+ ))
138
+ }
139
+
140
+ export function transformTemplate(code, id, options = {}, sink = [], baseOffset = 0) {
141
+ const opts = normalizeOptions(options)
142
+ if (!opts.enabled || !shouldProcess(id, opts)) return { code, manifest: buildManifest(sink, opts) }
143
+
144
+ const local = []
145
+ const replacements = []
146
+ const stack = []
147
+ let pos = 0
148
+ let tagInfo = findNextTemplateTag(code, pos)
149
+ while (tagInfo) {
150
+ const { index, tag, attrs, raw, closeIndex, isClose } = tagInfo
151
+ const lowerTag = tag.toLowerCase()
152
+ if (isClose) {
153
+ popStack(stack, lowerTag)
154
+ pos = closeIndex + 1
155
+ tagInfo = findNextTemplateTag(code, pos)
156
+ continue
157
+ }
158
+ const selfClosing = raw.endsWith('/>') || VOID_TAGS.has(lowerTag)
159
+
160
+ const context = stackContext(stack)
161
+ const classification = classifyTarget(tag, attrs, code, index, context)
162
+ if (classification.target) {
163
+ const manualId = getAttr(attrs, 'data-guide-id') || getAttr(attrs, 'data-guide') || getAttr(attrs, 'data-gui-code')
164
+ const openEnd = code.indexOf('>', index)
165
+ const simpleText = openEnd >= 0 ? extractElementText(code, openEnd + 1, lowerTag) : ''
166
+ const evidence = extractEvidence(tag, attrs, simpleText)
167
+ const source = manualId ? 'manual' : 'auto'
168
+ const item = buildManifestItem({
169
+ id,
170
+ tag,
171
+ attrs,
172
+ offset: baseOffset + index,
173
+ context,
174
+ classification,
175
+ evidence,
176
+ source,
177
+ manualId,
178
+ options: opts,
179
+ })
180
+ local.push(item)
181
+ if (!manualId) {
182
+ const insert = buildDataGuideAttrs(item)
183
+ const closeIndexInRaw = raw.endsWith('/>') ? raw.length - 2 : raw.length - 1
184
+ replacements.push({
185
+ start: index + closeIndexInRaw,
186
+ end: index + closeIndexInRaw,
187
+ text: insert,
188
+ })
189
+ }
190
+ }
191
+
192
+ if (!selfClosing) {
193
+ stack.push({
194
+ tag: lowerTag,
195
+ kind: classification.kind || '',
196
+ scope: inferScope(context, attrs, classification.kind),
197
+ text: extractSimpleText(code, closeIndex + 1),
198
+ guideId: getAttr(attrs, 'data-guide-id') || getAttr(attrs, 'data-guide') || getAttr(attrs, 'data-gui-code') || '',
199
+ })
200
+ }
201
+ pos = closeIndex + 1
202
+ tagInfo = findNextTemplateTag(code, pos)
203
+ }
204
+
205
+ resolveDuplicates(local, opts)
206
+ sink.push(...local)
207
+ return { code: applyReplacements(code, replacements, local), manifest: buildManifest(sink, opts) }
208
+ }
209
+
210
+ export function transformInlineTemplates(code, id, options = {}, sink = []) {
211
+ const opts = normalizeOptions(options)
212
+ if (!opts.enabled || !shouldProcess(id, { ...opts, include: opts.include || ['.ts'] })) {
213
+ return { code, manifest: buildManifest(sink, opts) }
214
+ }
215
+ const replacements = []
216
+ const re = /template\s*:\s*(`([\s\S]*?)`|"([^"]*?)"|'([^']*?)')/g
217
+ let match
218
+ while ((match = re.exec(code))) {
219
+ const quote = match[1][0]
220
+ const template = match[2] || match[3] || match[4] || ''
221
+ const result = transformTemplate(template, id, { ...opts, include: opts.include || ['.ts'] }, sink, match.index)
222
+ if (result.code === template) continue
223
+ replacements.push({
224
+ start: match.index + match[0].indexOf(match[1]) + 1,
225
+ end: match.index + match[0].indexOf(match[1]) + match[1].length - 1,
226
+ text: escapeInlineTemplate(result.code, quote),
227
+ })
228
+ }
229
+ if (!replacements.length) return { code, manifest: buildManifest(sink, opts) }
230
+ let out = code
231
+ replacements.slice().reverse().forEach((replacement) => {
232
+ out = out.slice(0, replacement.start) + replacement.text + out.slice(replacement.end)
233
+ })
234
+ return { code: out, manifest: buildManifest(sink, opts) }
235
+ }
236
+
237
+ export function buildManifest(items = [], options = {}, extra = {}) {
238
+ const opts = normalizeOptions(options)
239
+ const manifestItems = dedupeManifestItems(items)
240
+ return {
241
+ schema_version: 1,
242
+ generated_at: extra.generated_at || new Date().toISOString(),
243
+ app_key: extra.app_key || opts.appKey,
244
+ id_prefix: opts.idPrefix,
245
+ items: manifestItems.map((item) => ({ ...item })),
246
+ diagnostics: manifestDiagnostics(manifestItems, opts),
247
+ }
248
+ }
249
+
250
+ export function validateManifestContract(manifest = {}, guide = {}) {
251
+ const items = Array.isArray(manifest.items) ? manifest.items : []
252
+ const idSet = new Set(items.map((item) => item.guide_id).filter(Boolean))
253
+ const manualCounts = countBy(items.filter((item) => item.source === 'manual').map((item) => item.guide_id))
254
+ const used = collectGuideIdsFromGuide(guide)
255
+ const missing = used.filter((id) => !idSet.has(id))
256
+ const duplicateManual = Object.entries(manualCounts)
257
+ .filter(([, count]) => count > 1)
258
+ .map(([guide_id, count]) => ({ guide_id, count }))
259
+ const lowConfidence = items.filter((item) => item.source === 'auto' && Number(item.confidence || 0) < 60)
260
+ const drift = manifest.diagnostics && manifest.diagnostics.auto_id_drift || null
261
+ const status = missing.length || duplicateManual.length ? 'block' : (lowConfidence.length || drift && drift.status === 'warn' ? 'warn' : 'allow')
262
+ return {
263
+ status,
264
+ missing,
265
+ duplicate_manual: duplicateManual,
266
+ low_confidence: lowConfidence.map((item) => ({
267
+ guide_id: item.guide_id,
268
+ confidence: item.confidence,
269
+ file: item.file,
270
+ diagnostics: item.diagnostics,
271
+ })),
272
+ auto_id_drift: drift,
273
+ summary: {
274
+ used: used.length,
275
+ manifest_items: items.length,
276
+ missing: missing.length,
277
+ duplicate_manual: duplicateManual.length,
278
+ low_confidence: lowConfidence.length,
279
+ auto_id_drift: drift && drift.status === 'warn' ? 1 : 0,
280
+ },
281
+ }
282
+ }
283
+
284
+ export function compareManifestDrift(previous = {}, current = {}, options = {}) {
285
+ const threshold = Number(options.autoIdDriftWarnRate || 0.2)
286
+ const minChanged = Number(options.autoIdDriftWarnMinChanged || 5)
287
+ const previousItems = Array.isArray(previous.items) ? previous.items : []
288
+ const currentItems = Array.isArray(current.items) ? current.items : []
289
+ const previousAuto = previousItems.filter((item) => item && item.source === 'auto' && item.guide_id)
290
+ const currentAuto = currentItems.filter((item) => item && item.source === 'auto' && item.guide_id)
291
+ const previousByStableKey = new Map(previousAuto.map((item) => [manifestStableKey(item), item]).filter(([key]) => key))
292
+ const changedStableKeys = new Set()
293
+ const changed = currentAuto
294
+ .map((item) => {
295
+ const stableKey = manifestStableKey(item)
296
+ const previousItem = previousByStableKey.get(stableKey)
297
+ if (!previousItem || previousItem.guide_id === item.guide_id) return null
298
+ changedStableKeys.add(stableKey)
299
+ return {
300
+ stable_key: stableKey,
301
+ previous_guide_id: previousItem.guide_id,
302
+ current_guide_id: item.guide_id,
303
+ file: item.file || previousItem.file || '',
304
+ }
305
+ })
306
+ .filter(Boolean)
307
+ const previousIds = new Set(previousAuto.map((item) => item.guide_id))
308
+ const currentIds = new Set(currentAuto.map((item) => item.guide_id))
309
+ const removed = previousAuto
310
+ .filter((item) => !currentIds.has(item.guide_id) && !changedStableKeys.has(manifestStableKey(item)))
311
+ .map((item) => summarizeDriftItem(item))
312
+ const added = currentAuto
313
+ .filter((item) => !previousIds.has(item.guide_id) && !changedStableKeys.has(manifestStableKey(item)))
314
+ .map((item) => summarizeDriftItem(item))
315
+ const changedCount = removed.length + added.length + changed.length
316
+ const base = Math.max(previousAuto.length, currentAuto.length, 1)
317
+ const changedRate = changedCount / base
318
+ const shouldWarn = changedCount >= minChanged || changedRate > threshold
319
+ return {
320
+ auto_id_drift: {
321
+ status: shouldWarn ? 'warn' : 'allow',
322
+ previous_auto_count: previousAuto.length,
323
+ current_auto_count: currentAuto.length,
324
+ added_count: added.length,
325
+ removed_count: removed.length,
326
+ changed_count: changed.length,
327
+ changed_rate: Number(changedRate.toFixed(4)),
328
+ added: added.slice(0, 20),
329
+ removed: removed.slice(0, 20),
330
+ changed: changed.slice(0, 20),
331
+ },
332
+ }
333
+ }
334
+
335
+ function replaceTemplateBlocks(code, replacer) {
336
+ const re = /<template([^>]*)>([\s\S]*?)<\/template>/gi
337
+ let out = ''
338
+ let last = 0
339
+ let changed = false
340
+ let match
341
+ while ((match = re.exec(code))) {
342
+ const start = match.index + match[0].indexOf(match[2])
343
+ out += code.slice(last, start)
344
+ out += replacer(match[2], start)
345
+ last = start + match[2].length
346
+ changed = true
347
+ }
348
+ if (!changed) return { code }
349
+ out += code.slice(last)
350
+ return { code: out }
351
+ }
352
+
353
+ function shouldProcess(id, options) {
354
+ const normalized = id.replace(/\\/g, '/')
355
+ if (options.exclude && toMatcher(options.exclude)(normalized)) return false
356
+ if (options.include) return toMatcher(options.include)(normalized)
357
+ return /\.(vue|html|component\.html)$/i.test(normalized)
358
+ }
359
+
360
+ function toMatcher(value) {
361
+ const list = Array.isArray(value) ? value : [value]
362
+ return (input) => list.some((item) => {
363
+ if (!item) return false
364
+ if (item instanceof RegExp) return item.test(input)
365
+ return input.includes(String(item))
366
+ })
367
+ }
368
+
369
+ function escapeInlineTemplate(value, quote) {
370
+ if (quote === '`') return String(value).replace(/`/g, '\\`')
371
+ if (quote === '"') return String(value).replace(/"/g, '\\"')
372
+ return String(value).replace(/'/g, "\\'")
373
+ }
374
+
375
+ const CLOSE_BUTTON_CLASS_RE = /(?:^|\s)(el-dialog__headerbtn|vxe-modal--close-btn|ant-modal-close|ant-drawer-close|mat-mdc-dialog-close|mat-dialog-close)(?:\s|$)/i
376
+
377
+ function isCloseButtonClass(className = '') {
378
+ return CLOSE_BUTTON_CLASS_RE.test(className) || /headerbtn|modal--close|icon-close/i.test(className)
379
+ }
380
+
381
+ function structuralAttrText(attrs = '') {
382
+ return String(attrs || '')
383
+ .replace(/(?:@|v-on:|v-bind:|:)(?:click|tap)[^=]*=\s*(?:"[^"]*"|'[^']*')/gi, ' ')
384
+ .replace(/\bon[a-z]+\s*=\s*(?:"[^"]*"|'[^']*')/gi, ' ')
385
+ }
386
+
387
+ function isToolbarActionClass(className = '') {
388
+ return /btn[_-]|btn-green|btn_green|el-button|ant-btn/i.test(className)
389
+ }
390
+
391
+ function extractElementText(code, startOffset, tagName) {
392
+ const closeToken = `</${tagName}`
393
+ const closeIndex = code.toLowerCase().indexOf(closeToken.toLowerCase(), startOffset)
394
+ const slice = code.slice(startOffset, closeIndex >= 0 ? closeIndex : Math.min(code.length, startOffset + 240))
395
+ const langLiteral = slice.match(/\$lang\(\s*['"]([^'"]+)['"]\s*\)/)
396
+ if (langLiteral) return cleanText(langLiteral[1])
397
+ return cleanText(slice.replace(/<[^>]+>/g, ' '))
398
+ }
399
+
400
+ function classifyTarget(tag, attrs, code, offset, context) {
401
+ const lowerTag = tag.toLowerCase()
402
+ const componentName = lowerTag.replace(/^[a-z]+-/, '')
403
+ const attrText = attrs.toLowerCase()
404
+ const structuralAttrs = structuralAttrText(attrs).toLowerCase()
405
+ const eventName = extractEventName(attrs)
406
+ const className = getAttr(attrs, 'class') || ''
407
+ const role = getAttr(attrs, 'role') || ''
408
+ const inRow = context.kinds.includes('table') || /(v-for|\*ngfor|ng-repeat)/i.test(attrs)
409
+ const openEnd = code.indexOf('>', offset)
410
+ const simpleText = openEnd >= 0 ? extractElementText(code, openEnd + 1, lowerTag) : ''
411
+
412
+ if (TARGET_TAGS.has(lowerTag)) {
413
+ const closeButton = isCloseButtonClass(className)
414
+ return {
415
+ target: true,
416
+ kind: kindFromTag(lowerTag, role, className, inRow),
417
+ confidence: closeButton ? 80 : 80,
418
+ overlay_action: closeButton ? 'close' : undefined,
419
+ }
420
+ }
421
+ if (role && /button|tab|menuitem|dialog|table|row|cell|tabpanel/i.test(role)) {
422
+ return { target: true, kind: kindFromRole(role), confidence: 82 }
423
+ }
424
+ if (eventName && /click|tap|select|change|submit/i.test(attrs) && (isToolbarActionClass(className) || simpleText)) {
425
+ return { target: true, kind: inRow ? 'row_action' : 'button', confidence: 75 }
426
+ }
427
+ if (/toolbar|tool-bar|filter|search|query|form|dialog|drawer|modal|btnlist/i.test(className)) {
428
+ return { target: true, kind: kindFromClass(className), confidence: 66 }
429
+ }
430
+ if (TARGET_COMPONENT_HINTS.some((hint) => componentName.includes(hint) || structuralAttrs.includes(hint))) {
431
+ return { target: true, kind: kindFromComponent(componentName, structuralAttrs, inRow), confidence: 72 }
432
+ }
433
+ if (eventName && /click|tap|select|change|submit/i.test(attrs)) {
434
+ return { target: true, kind: inRow ? 'row_action' : 'button', confidence: 62 }
435
+ }
436
+ if (isCloseButtonClass(className)) {
437
+ const inOverlay = context.scopes.includes('dialog') || context.scopes.includes('drawer') || context.kinds.includes('region')
438
+ return { target: true, kind: 'button', confidence: inOverlay ? 80 : 72, overlay_action: 'close' }
439
+ }
440
+ return { target: false, kind: '', confidence: 0 }
441
+ }
442
+
443
+ function kindFromTag(tag, role, className, inRow) {
444
+ if (isCloseButtonClass(className)) return 'button'
445
+ if (/dialog|modal/.test(className) || role === 'dialog') return 'region'
446
+ if (/drawer/.test(className)) return 'region'
447
+ if (tag === 'table') return 'table'
448
+ if (tag === 'th') return 'column'
449
+ if (/btnList/.test(className) || inRow && ['button', 'a'].includes(tag)) return 'row_action'
450
+ if (['input', 'textarea', 'select', 'option'].includes(tag)) return 'input'
451
+ if (tag === 'form') return 'region'
452
+ if (role === 'tab') return 'tab'
453
+ return 'button'
454
+ }
455
+
456
+ function kindFromRole(role) {
457
+ if (role === 'tab') return 'tab'
458
+ if (role === 'dialog' || role === 'tabpanel') return 'region'
459
+ if (role === 'table') return 'table'
460
+ if (role === 'cell' || role === 'columnheader') return 'column'
461
+ if (role === 'menuitem') return 'menu_item'
462
+ return 'button'
463
+ }
464
+
465
+ function kindFromComponent(componentName, attrText, inRow) {
466
+ if (/dialog|modal|drawer|form/.test(componentName) || /dialog|modal|drawer|form/.test(attrText)) return 'region'
467
+ if (/table/.test(componentName)) return 'table'
468
+ if (/column/.test(componentName)) return 'column'
469
+ if (/tab/.test(componentName)) return 'tab'
470
+ if (/menu/.test(componentName)) return 'menu_item'
471
+ if (inRow) return 'row_action'
472
+ if (/input|select|date|picker/.test(componentName)) return 'input'
473
+ return 'button'
474
+ }
475
+
476
+ function kindFromClass(className) {
477
+ if (/dialog|drawer|modal|toolbar|filter|search|query|form/i.test(className)) return 'region'
478
+ if (/btnList/.test(className)) return 'row_action'
479
+ return 'button'
480
+ }
481
+
482
+ function extractEvidence(tag, attrs, simpleText) {
483
+ const eventName = extractEventName(attrs)
484
+ return {
485
+ text: cleanText(simpleText),
486
+ aria: getAttr(attrs, 'aria-label') || '',
487
+ title: getAttr(attrs, 'title') || '',
488
+ placeholder: getAttr(attrs, 'placeholder') || '',
489
+ name: getAttr(attrs, 'name') || '',
490
+ eventName,
491
+ component: tag,
492
+ }
493
+ }
494
+
495
+ function buildManifestItem(input) {
496
+ const { id, tag, attrs, offset, context, classification, evidence, source, manualId, options } = input
497
+ const file = normalizeFile(id, options.rootDir)
498
+ const pageContext = resolvePageContext(file, options)
499
+ const page = pageContext.page
500
+ const scope = inferScope(context, attrs, classification.kind)
501
+ const semantic = semanticSegment(evidence) || classification.kind || tag
502
+ const base = manualId || [options.idPrefix, page, scope, semantic].filter(Boolean).join('.')
503
+ const hash = shortHash(`${file}:${offset}:${tag}:${semantic}`)
504
+ const generatedBase = sanitizeGuideId(base) || options.idPrefix
505
+ const generatedId = source === 'manual' ? manualId : `${generatedBase}__${hash}`
506
+ const guideId = source === 'manual' ? manualId : generatedId
507
+ const diagnostics = []
508
+ const sensitive = findSensitiveParts(guideId, options.sensitiveWords)
509
+ if (sensitive.length) diagnostics.push({ type: 'sensitive_word', values: sensitive })
510
+ if (classification.confidence < options.lowConfidenceThreshold) diagnostics.push({ type: 'low_confidence', confidence: classification.confidence })
511
+ if (source === 'auto' && !semantic) diagnostics.push({ type: 'missing_semantic' })
512
+ return {
513
+ guide_id: guideId,
514
+ source,
515
+ target_kind: classification.kind || 'element',
516
+ scope,
517
+ route: pageContext.route,
518
+ menu: pageContext.menu,
519
+ page,
520
+ file,
521
+ component: tag,
522
+ text: evidence.text || '',
523
+ aria: evidence.aria || '',
524
+ title: evidence.title || '',
525
+ event_name: evidence.eventName || (classification.overlay_action === 'close' ? 'close' : ''),
526
+ row_policy: classification.kind === 'row_action' ? 'first_visible' : '',
527
+ confidence: source === 'manual' ? 100 : classification.confidence,
528
+ hash,
529
+ generated: source === 'auto',
530
+ diagnostics,
531
+ }
532
+ }
533
+
534
+ function resolvePageContext(file, options) {
535
+ const fallbackPage = pageKeyFromFile(file, options)
536
+ const fallback = {
537
+ route: options.routePrefix || '',
538
+ menu: options.routePrefix || '',
539
+ page: fallbackPage,
540
+ }
541
+ const resolver = options.menuResolver
542
+ if (!resolver) return fallback
543
+ let resolved = null
544
+ if (typeof resolver === 'function') {
545
+ resolved = resolver({ file, appKey: options.appKey, routePrefix: options.routePrefix, fallbackPage }) || null
546
+ } else if (resolver && typeof resolver === 'object') {
547
+ resolved = resolver[file] || resolver[fallbackPage] || null
548
+ }
549
+ if (!resolved || typeof resolved !== 'object') return fallback
550
+ const route = String(resolved.route || resolved.routePath || fallback.route || '')
551
+ const menu = sanitizeSegment(resolved.menu || resolved.menuKey || resolved.menuCode || route || fallback.menu)
552
+ const page = sanitizePageKey(resolved.page || resolved.pageInstance || resolved.pageName || fallbackPage)
553
+ return {
554
+ route,
555
+ menu,
556
+ page,
557
+ }
558
+ }
559
+
560
+ function resolveDuplicates(items, options) {
561
+ const seen = new Map()
562
+ items.forEach((item) => {
563
+ const previous = seen.get(item.guide_id)
564
+ if (!previous) {
565
+ seen.set(item.guide_id, item)
566
+ return
567
+ }
568
+ if (previous.source === 'manual' && item.source === 'manual') {
569
+ previous.diagnostics.push({ type: 'duplicate_manual', duplicate_file: item.file })
570
+ item.diagnostics.push({ type: 'duplicate_manual', duplicate_file: previous.file })
571
+ return
572
+ }
573
+ if (item.source === 'auto') {
574
+ item.guide_id = `${item.guide_id}__${item.hash}`
575
+ item.diagnostics.push({ type: previous.source === 'manual' ? 'manual_auto_conflict' : 'auto_duplicate_suffix' })
576
+ seen.set(item.guide_id, item)
577
+ return
578
+ }
579
+ if (previous.source === 'auto') {
580
+ previous.guide_id = `${previous.guide_id}__${previous.hash}`
581
+ previous.diagnostics.push({ type: 'manual_auto_conflict' })
582
+ seen.set(previous.guide_id, previous)
583
+ seen.set(item.guide_id, item)
584
+ return
585
+ }
586
+ })
587
+ }
588
+
589
+ function buildDataGuideAttrs(item) {
590
+ const attrs = [
591
+ ` data-guide-id="${escapeAttr(item.guide_id)}"`,
592
+ ` data-guide-kind="${escapeAttr(item.target_kind)}"`,
593
+ ` data-guide-scope="${escapeAttr(item.scope)}"`,
594
+ ' data-guide-generated="true"',
595
+ ]
596
+ if (item.row_policy) attrs.push(` data-guide-row-policy="${escapeAttr(item.row_policy)}"`)
597
+ return attrs.join('')
598
+ }
599
+
600
+ function applyReplacements(code, replacements, localItems) {
601
+ if (!replacements.length) return code
602
+ let out = code
603
+ const autoItems = localItems.filter((item) => item.source === 'auto')
604
+ replacements.slice().reverse().forEach((replacement, indexFromEnd) => {
605
+ const item = autoItems[autoItems.length - 1 - indexFromEnd]
606
+ const text = item ? buildDataGuideAttrs(item) : replacement.text
607
+ out = out.slice(0, replacement.start) + text + out.slice(replacement.end)
608
+ })
609
+ return out
610
+ }
611
+
612
+ function manifestDiagnostics(items, options) {
613
+ const counts = countBy(items.map((item) => item.guide_id))
614
+ const duplicates = Object.entries(counts).filter(([, count]) => count > 1).map(([guide_id, count]) => ({ guide_id, count }))
615
+ const manualDuplicates = duplicates.filter((dup) => items.filter((item) => item.guide_id === dup.guide_id && item.source === 'manual').length > 1)
616
+ const sensitive = items.filter((item) => item.diagnostics.some((diag) => diag.type === 'sensitive_word'))
617
+ const lowConfidence = items.filter((item) => item.diagnostics.some((diag) => diag.type === 'low_confidence'))
618
+ return {
619
+ total: items.length,
620
+ auto: items.filter((item) => item.source === 'auto').length,
621
+ manual: items.filter((item) => item.source === 'manual').length,
622
+ duplicates,
623
+ manual_duplicates: manualDuplicates,
624
+ sensitive: sensitive.map((item) => item.guide_id),
625
+ low_confidence: lowConfidence.map((item) => item.guide_id),
626
+ status: manualDuplicates.length && options.productionDuplicatePolicy === 'block_manual' ? 'block' : (duplicates.length || sensitive.length || lowConfidence.length ? 'warn' : 'allow'),
627
+ }
628
+ }
629
+
630
+ function dedupeManifestItems(items) {
631
+ const seen = new Set()
632
+ const out = []
633
+ ;(items || []).forEach((item) => {
634
+ if (!item || !item.guide_id) return
635
+ const key = [
636
+ item.guide_id,
637
+ item.source || '',
638
+ item.file || '',
639
+ item.component || '',
640
+ item.hash || '',
641
+ item.target_kind || '',
642
+ item.scope || '',
643
+ item.text || '',
644
+ item.aria || '',
645
+ item.title || '',
646
+ item.event_name || '',
647
+ item.row_policy || '',
648
+ ].join('|')
649
+ if (seen.has(key)) return
650
+ seen.add(key)
651
+ out.push(item)
652
+ })
653
+ return out
654
+ }
655
+
656
+ function manifestStableKey(item) {
657
+ if (!item) return ''
658
+ return [
659
+ item.hash || '',
660
+ item.file || '',
661
+ item.component || '',
662
+ item.scope || '',
663
+ item.target_kind || '',
664
+ item.text || item.aria || item.title || item.event_name || '',
665
+ ].join('|')
666
+ }
667
+
668
+ function summarizeDriftItem(item) {
669
+ return {
670
+ guide_id: item.guide_id,
671
+ file: item.file || '',
672
+ component: item.component || '',
673
+ scope: item.scope || '',
674
+ text: item.text || item.aria || item.title || item.event_name || '',
675
+ }
676
+ }
677
+
678
+ function collectGuideIdsFromGuide(guide) {
679
+ const out = []
680
+ function visitLocator(locator) {
681
+ if (!locator || typeof locator !== 'object') return
682
+ const id = locator.guide_code || locator.guide_id
683
+ if (id) out.push(id)
684
+ if (locator.context) visitContext(locator.context)
685
+ ;(locator.context_stack || []).forEach(visitContext)
686
+ ;(locator.pre_actions || []).forEach((action) => visitLocator(action && action.target))
687
+ }
688
+ function visitContext(context) {
689
+ if (!context || typeof context !== 'object') return
690
+ visitLocator(context.root_locator || context.rootLocator)
691
+ }
692
+ ;(guide.steps || []).forEach(visitLocator)
693
+ return Array.from(new Set(out))
694
+ }
695
+
696
+ function inferScope(context, attrs, kind) {
697
+ const cls = getAttr(attrs, 'class') || ''
698
+ if (context.scopes && context.scopes.length) return context.scopes[context.scopes.length - 1]
699
+ if (/toolbar|tool-bar|operation/i.test(cls) || context.kinds.includes('toolbar')) return 'toolbar'
700
+ if (/filter|search|query/i.test(cls) || context.kinds.includes('filter')) return 'filter'
701
+ if (/dialog|modal/i.test(cls) || context.kinds.includes('dialog')) return 'dialog'
702
+ if (/drawer/i.test(cls) || context.kinds.includes('drawer')) return 'drawer'
703
+ if (/table|grid/i.test(cls) || context.kinds.includes('table') || kind === 'row_action' || kind === 'column') return 'table'
704
+ if (/form/i.test(cls) || context.kinds.includes('form')) return 'form'
705
+ if (/menu/i.test(cls) || context.kinds.includes('menu')) return 'menu'
706
+ return 'main'
707
+ }
708
+
709
+ function stackContext(stack) {
710
+ return {
711
+ kinds: stack.map((item) => item.kind).filter(Boolean),
712
+ scopes: stack.map((item) => item.scope).filter(Boolean),
713
+ guideIds: stack.map((item) => item.guideId).filter(Boolean),
714
+ texts: stack.map((item) => item.text).filter(Boolean),
715
+ }
716
+ }
717
+
718
+ function popStack(stack, lowerTag) {
719
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
720
+ const item = stack.pop()
721
+ if (item.tag === lowerTag) return
722
+ }
723
+ }
724
+
725
+ function getAttr(attrs, name) {
726
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
727
+ const re = new RegExp(`(?:^|\\s)(?:[:@\\w.-]+:)?${escaped}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s"'=<>` + '`' + `]+))`, 'i')
728
+ const match = attrs.match(re)
729
+ return match ? (match[2] || match[3] || match[4] || '') : ''
730
+ }
731
+
732
+ function extractEventName(attrs) {
733
+ const patterns = [
734
+ /(?:@click|v-on:click|:on-click|\(click\)|ng-click|onClick)\s*=\s*"([^"]+)"/i,
735
+ /(?:@click|v-on:click|:on-click|\(click\)|ng-click|onClick)\s*=\s*'([^']+)'/i,
736
+ ]
737
+ for (const pattern of patterns) {
738
+ const match = attrs.match(pattern)
739
+ if (match) return normalizeEventName(match[1])
740
+ }
741
+ return ''
742
+ }
743
+
744
+ function normalizeEventName(value) {
745
+ const raw = String(value || '').split(/[({\s]/)[0].replace(/^this\./, '')
746
+ const tokens = raw
747
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
748
+ .replace(/[_-]+/g, ' ')
749
+ .split(/\s+/)
750
+ .filter(Boolean)
751
+ const last = tokens[tokens.length - 1] || raw
752
+ return aliasAction(last) || sanitizeSegment(last)
753
+ }
754
+
755
+ function semanticSegment(evidence) {
756
+ const textCandidates = [
757
+ evidence.aria,
758
+ evidence.title,
759
+ evidence.placeholder,
760
+ evidence.name,
761
+ evidence.text,
762
+ ].map((item) => aliasAction(item) || sanitizeSegment(item)).filter(Boolean)
763
+ if (textCandidates.length) return textCandidates[0]
764
+ const eventName = evidence.eventName || ''
765
+ if (eventName && !/(?:Dialog|Modal|Drawer)$/i.test(eventName)) {
766
+ const fromEvent = aliasAction(eventName) || sanitizeSegment(eventName)
767
+ if (fromEvent) return fromEvent
768
+ }
769
+ const fallback = [
770
+ evidence.eventName,
771
+ evidence.component,
772
+ ].map((item) => aliasAction(item) || sanitizeSegment(item)).filter(Boolean)
773
+ return fallback[0] || ''
774
+ }
775
+
776
+ function aliasAction(value) {
777
+ const normalized = cleanText(value)
778
+ for (const [alias, pattern] of ACTION_ALIASES) {
779
+ if (pattern.test(normalized)) return alias
780
+ }
781
+ return ''
782
+ }
783
+
784
+ function extractSimpleText(code, offset) {
785
+ const nextTag = code.indexOf('<', offset)
786
+ const slice = code.slice(offset, nextTag >= 0 ? nextTag : Math.min(code.length, offset + 120))
787
+ return cleanText(slice)
788
+ }
789
+
790
+ function cleanText(value) {
791
+ return String(value || '').replace(/\{\{[^}]*}}/g, '').replace(/\s+/g, ' ').trim()
792
+ }
793
+
794
+ function pageKeyFromFile(file, options) {
795
+ const withoutExt = file.replace(/\.(vue|html|component\.html|ts|js)$/i, '').replace(/\.component$/i, '')
796
+ const trimmed = withoutExt.replace(/^(src\/)?(views|pages|routes|app)\//, '')
797
+ return [options.routePrefix, trimmed].filter(Boolean).map(sanitizePath).filter(Boolean).join('.') || 'page'
798
+ }
799
+
800
+ function normalizeFile(file, rootDir) {
801
+ const rel = path.relative(rootDir || process.cwd(), file)
802
+ return (rel && !rel.startsWith('..')) ? rel.replace(/\\/g, '/') : file.replace(/\\/g, '/')
803
+ }
804
+
805
+ function sanitizeGuideId(value) {
806
+ return String(value || '')
807
+ .split('.')
808
+ .map(sanitizeSegment)
809
+ .filter(Boolean)
810
+ .join('.')
811
+ }
812
+
813
+ function sanitizePath(value) {
814
+ return String(value || '').split(/[\\/]+/).map(sanitizeSegment).filter(Boolean).join('.')
815
+ }
816
+
817
+ function sanitizePageKey(value) {
818
+ return String(value || '').split(/[\\/.]+/).map(sanitizeSegment).filter(Boolean).join('.')
819
+ }
820
+
821
+ function sanitizeSegment(value) {
822
+ const raw = String(value || '').trim()
823
+ if (!raw) return ''
824
+ const ascii = raw
825
+ .normalize('NFKD')
826
+ .replace(/[\u0300-\u036f]/g, '')
827
+ .replace(/[^A-Za-z0-9_\-\u4e00-\u9fa5]+/g, '-')
828
+ .replace(/^-+|-+$/g, '')
829
+ if (!ascii) return ''
830
+ return ascii.length > 36 ? ascii.slice(0, 36).replace(/-+$/g, '') : ascii
831
+ }
832
+
833
+ function shortHash(value) {
834
+ return crypto.createHash('sha1').update(String(value)).digest('hex').slice(0, 6)
835
+ }
836
+
837
+ function findSensitiveParts(value, words) {
838
+ const lower = String(value || '').toLowerCase()
839
+ return (words || []).filter((word) => word && lower.includes(String(word).toLowerCase()))
840
+ }
841
+
842
+ function countBy(values) {
843
+ const counts = {}
844
+ values.filter(Boolean).forEach((value) => {
845
+ counts[value] = (counts[value] || 0) + 1
846
+ })
847
+ return counts
848
+ }
849
+
850
+ function escapeAttr(value) {
851
+ return String(value || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;')
852
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ createGuideCompiler,
3
+ transformTemplate,
4
+ transformInlineTemplates,
5
+ transformVueSfc,
6
+ buildManifest,
7
+ validateManifestContract,
8
+ compareManifestDrift,
9
+ } from './core.js'
10
+
11
+ export { transformVue2 } from './vue2.js'
12
+ export { transformVue3 } from './vue3.js'
13
+ export { transformAngularTemplate, transformAngularComponentTs } from './angular.js'
14
+ export { systemAssistantGuideVitePlugin } from './vite.js'
@@ -0,0 +1,20 @@
1
+ const path = require('path')
2
+ const GuideManifestWebpackPlugin = require('./webpack-plugin.cjs')
3
+
4
+ module.exports = function setupGuideCompilerNuxt2(config, options) {
5
+ const opts = Object.assign({}, options || {})
6
+ const loaderPath = path.resolve(__dirname, 'webpack-loader.cjs')
7
+ config.module = config.module || {}
8
+ config.module.rules = config.module.rules || []
9
+ config.module.rules.unshift({
10
+ enforce: 'pre',
11
+ test: /\.vue$/,
12
+ use: [{
13
+ loader: loaderPath,
14
+ options: opts,
15
+ }],
16
+ })
17
+ config.plugins = config.plugins || []
18
+ config.plugins.push(new GuideManifestWebpackPlugin(opts))
19
+ return config
20
+ }
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { createGuideCompiler, normalizeOptions } from './core.js'
4
+
5
+ export function systemAssistantGuideVitePlugin(options = {}) {
6
+ const normalized = normalizeOptions(options)
7
+ const compiler = createGuideCompiler(normalized)
8
+ return {
9
+ name: 'system-assistant-guide-compiler',
10
+ enforce: 'pre',
11
+ transform(code, id) {
12
+ if (!normalized.enabled) return null
13
+ if (/\.vue$/i.test(id)) {
14
+ const result = compiler.transformVue3(code, id)
15
+ return result.code === code ? null : { code: result.code, map: null }
16
+ }
17
+ if (/\.(html|component\.html)$/i.test(id)) {
18
+ const result = compiler.transformAngularTemplate(code, id)
19
+ return result.code === code ? null : { code: result.code, map: null }
20
+ }
21
+ if (/\.ts$/i.test(id) && /template\s*:/.test(code)) {
22
+ const result = compiler.transformAngularComponentTs(code, id)
23
+ return result.code === code ? null : { code: result.code, map: null }
24
+ }
25
+ return null
26
+ },
27
+ generateBundle() {
28
+ const manifest = compiler.getManifest()
29
+ if (normalized.productionDuplicatePolicy === 'block_manual' && manifest.diagnostics.manual_duplicates.length) {
30
+ const duplicates = manifest.diagnostics.manual_duplicates.map((item) => `${item.guide_id}(${item.count})`).join(', ')
31
+ throw new Error(`[system-assistant-guide-compiler] 手写 data-guide-id 重复,生产构建已阻断:${duplicates}`)
32
+ }
33
+ if (!normalized.manifestOutput) return
34
+ const outputPath = path.resolve(process.cwd(), normalized.manifestOutput)
35
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true })
36
+ fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2), 'utf8')
37
+ },
38
+ }
39
+ }
@@ -0,0 +1,8 @@
1
+ import { normalizeOptions, transformVueSfc } from './core.js'
2
+
3
+ export function transformVue2(code, id, options = {}, sink = []) {
4
+ return transformVueSfc(code, id, {
5
+ ...normalizeOptions(options),
6
+ framework: 'vue2',
7
+ }, sink)
8
+ }
@@ -0,0 +1,8 @@
1
+ import { normalizeOptions, transformVueSfc } from './core.js'
2
+
3
+ export function transformVue3(code, id, options = {}, sink = []) {
4
+ return transformVueSfc(code, id, {
5
+ ...normalizeOptions(options),
6
+ framework: 'vue3',
7
+ }, sink)
8
+ }
@@ -0,0 +1,71 @@
1
+ const crypto = require('crypto')
2
+
3
+ const state = {
4
+ files: new Map(),
5
+ options: {},
6
+ }
7
+
8
+ function getLoaderOptions(ctx) {
9
+ if (ctx && typeof ctx.getOptions === 'function') return ctx.getOptions() || {}
10
+ const query = ctx && ctx.query
11
+ if (!query) return {}
12
+ if (typeof query === 'object') return query
13
+ if (typeof query === 'string') {
14
+ const text = query.replace(/^\?/, '')
15
+ if (!text) return {}
16
+ try {
17
+ return JSON.parse(text)
18
+ } catch (e) {
19
+ return text.split('&').reduce((acc, pair) => {
20
+ const parts = pair.split('=')
21
+ if (parts[0]) acc[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1] || '')
22
+ return acc
23
+ }, {})
24
+ }
25
+ }
26
+ return {}
27
+ }
28
+
29
+ function sourceHash(source) {
30
+ return crypto.createHash('sha1').update(String(source || '')).digest('hex').slice(0, 8)
31
+ }
32
+
33
+ module.exports = function guideCompilerWebpackLoader(source) {
34
+ const callback = this.async()
35
+ const options = getLoaderOptions(this)
36
+ const resourcePath = this.resourcePath || ''
37
+ const resourceQuery = this.resourceQuery || ''
38
+ if (this.cacheable) this.cacheable()
39
+ if (/type=/.test(resourceQuery)) {
40
+ callback(null, source)
41
+ return
42
+ }
43
+ state.options = Object.assign({}, state.options, options)
44
+
45
+ import('./index.js').then((mod) => {
46
+ const sink = []
47
+ const result = /\.vue$/i.test(resourcePath)
48
+ ? mod.transformVue2(source, resourcePath, options, sink)
49
+ : mod.transformTemplate(source, resourcePath, options, sink)
50
+ state.files.set(resourcePath, {
51
+ hash: sourceHash(source),
52
+ items: sink,
53
+ })
54
+ callback(null, result.code)
55
+ }, (error) => {
56
+ callback(error)
57
+ })
58
+ }
59
+
60
+ module.exports.getGuideCompilerState = function getGuideCompilerState() {
61
+ return state
62
+ }
63
+
64
+ module.exports.getGuideManifestItems = function getGuideManifestItems() {
65
+ return Array.from(state.files.values()).flatMap((entry) => entry.items || [])
66
+ }
67
+
68
+ module.exports.resetGuideManifest = function resetGuideManifest() {
69
+ state.files.clear()
70
+ state.options = {}
71
+ }
@@ -0,0 +1,33 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const loader = require('./webpack-loader.cjs')
4
+
5
+ class SystemAssistantGuideWebpackPlugin {
6
+ constructor(options) {
7
+ this.options = options || {}
8
+ }
9
+
10
+ apply(compiler) {
11
+ compiler.hooks.emit.tapAsync('SystemAssistantGuideWebpackPlugin', (compilation, done) => {
12
+ import('./index.js').then((mod) => {
13
+ const loaderState = loader.getGuideCompilerState()
14
+ const options = Object.assign({}, loaderState.options || {}, this.options)
15
+ const manifest = mod.buildManifest(loader.getGuideManifestItems(), options)
16
+ const json = JSON.stringify(manifest, null, 2)
17
+ const assetName = options.manifestAsset || 'sa-guide-manifest.json'
18
+ compilation.assets[assetName] = {
19
+ source: () => json,
20
+ size: () => Buffer.byteLength(json),
21
+ }
22
+ if (options.manifestOutput) {
23
+ const outputPath = path.resolve(options.manifestOutput)
24
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true })
25
+ fs.writeFileSync(outputPath, json, 'utf8')
26
+ }
27
+ done()
28
+ }, done)
29
+ })
30
+ }
31
+ }
32
+
33
+ module.exports = SystemAssistantGuideWebpackPlugin
package/nuxt.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  const { resolve } = require('node:path')
2
2
  const { readFileSync } = require('node:fs')
3
- const setupGuideCompilerNuxt2 = require('@hhhpyb/guide-compiler/nuxt2')
3
+ const setupGuideCompilerNuxt2 = require('./dist/compiler/nuxt2.cjs')
4
4
 
5
5
  const APP_KEY_ENV = 'SA_GUIDE_APP_KEY'
6
6
  const GUIDE_COMPILER_WEBPACK_MARKER = Symbol.for('system-assistant.guide-compiler.webpack')
@@ -67,7 +67,7 @@ function installGuideCompilerWebpack(config, options) {
67
67
 
68
68
  async function installGuideCompilerVite(config, options) {
69
69
  if (!config || config[GUIDE_COMPILER_VITE_MARKER]) return config
70
- const { systemAssistantGuideVitePlugin } = await import('@hhhpyb/guide-compiler/vite')
70
+ const { systemAssistantGuideVitePlugin } = await import('./dist/compiler/vite.js')
71
71
  config.plugins = config.plugins || []
72
72
  if (!config.plugins.some((plugin) => plugin && plugin.name === 'system-assistant-guide-compiler')) {
73
73
  config.plugins.push(systemAssistantGuideVitePlugin(options))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hhhpyb/system-assistant-client",
3
- "version": "1.3.6",
3
+ "version": "1.3.7",
4
4
  "description": "Nuxt 2 client plugin for System Assistant smart guide SDK",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -33,12 +33,9 @@
33
33
  "optional": true
34
34
  }
35
35
  },
36
- "dependencies": {
37
- "@hhhpyb/guide-compiler": "0.1.0"
38
- },
39
36
  "scripts": {
40
37
  "build": "node scripts/copy-dist.js",
41
- "test": "node --test test/nuxt.test.cjs",
38
+ "test": "npm run build && node --test test/nuxt.test.cjs",
42
39
  "prepublishOnly": "npm run build"
43
40
  }
44
41
  }