@npm-questionpro/wick-ui-i18n 2.0.0-next.28 → 2.0.0-next.30

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/index.js CHANGED
@@ -22,6 +22,7 @@ import {createFilter} from 'vite'
22
22
  import {TranslationProcessor} from './src/processor.js'
23
23
  import {transformFile} from './src/transform.js'
24
24
  import {printReport} from './src/debug.js'
25
+ import {TelemetryCollector, collectComponents} from './src/telemetry.js'
25
26
 
26
27
  /**
27
28
  * @typedef {object} WickI18nOptions
@@ -40,6 +41,8 @@ import {printReport} from './src/debug.js'
40
41
  * @returns {import('vite').Plugin}
41
42
  */
42
43
  export default function wickuiI18nPlugin(options = {}) {
44
+ const telemetry = new TelemetryCollector()
45
+
43
46
  const processor = new TranslationProcessor({
44
47
  components: options.components || [],
45
48
  ignoreComponents: options.ignoreComponents,
@@ -64,6 +67,7 @@ export default function wickuiI18nPlugin(options = {}) {
64
67
  */
65
68
  configResolved(resolvedConfig) {
66
69
  base = resolvedConfig.base
70
+ telemetry.scanVersions(resolvedConfig.root)
67
71
  },
68
72
 
69
73
  /**
@@ -73,6 +77,7 @@ export default function wickuiI18nPlugin(options = {}) {
73
77
  buildStart() {
74
78
  processor.dictionary.clear()
75
79
  processor.entries = []
80
+ telemetry.reset()
76
81
  },
77
82
 
78
83
  /**
@@ -88,6 +93,7 @@ export default function wickuiI18nPlugin(options = {}) {
88
93
  /\bwt\(/.test(code) ||
89
94
  (processor.components.size > 0 && [...processor.components].some(c => code.includes(c))) ||
90
95
  (processor.extractFromKeys.size > 0 && [...processor.extractFromKeys].some(k => code.includes(k)))
96
+ if (filter(id) && code.includes('Wu')) collectComponents(code, telemetry)
91
97
  if (!filter(id) || !hasAnyTarget) return null
92
98
  return transformFile(code, id, processor)
93
99
  },
@@ -102,10 +108,20 @@ export default function wickuiI18nPlugin(options = {}) {
102
108
  res.setHeader('Content-Type', 'application/json')
103
109
  res.end(JSON.stringify(Object.fromEntries(processor.dictionary), null, 2))
104
110
  })
111
+ server.middlewares.use(`${base}telemetry.json`, (_req, res) => {
112
+ res.setHeader('Content-Type', 'application/json')
113
+ res.end(JSON.stringify(telemetry.toJSON()))
114
+ })
105
115
  },
106
116
 
107
117
  /** Emit the translation dictionary as a build asset and print debug table. */
108
118
  generateBundle() {
119
+ this.emitFile({
120
+ type: 'asset',
121
+ fileName: 'telemetry.json',
122
+ source: JSON.stringify(telemetry.toJSON()),
123
+ })
124
+
109
125
  this.emitFile({
110
126
  type: 'asset',
111
127
  fileName: 'wick-ui-i18n.json',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npm-questionpro/wick-ui-i18n",
3
- "version": "2.0.0-next.28",
3
+ "version": "2.0.0-next.30",
4
4
  "private": false,
5
5
  "license": "ISC",
6
6
  "description": "Auto-translation AST wrapper for Wick UI",
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @fileoverview Telemetry collector — scans consumer node_modules for
3
+ * @npm-questionpro/wick-ui-* versions and counts Wu* JSX component usages
4
+ * across the entire build. Emitted as `telemetry.json` at build time and
5
+ * served at `GET /telemetry.json` in dev.
6
+ *
7
+ * Intentionally isolated from i18n logic — no imports from other src/ files.
8
+ */
9
+
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+ import {parse} from '@babel/parser'
13
+ import _traverse from '@babel/traverse'
14
+
15
+ const traverse = _traverse.default || _traverse
16
+
17
+ /** Babel parser plugins applied to every file. */
18
+ const BABEL_PLUGINS = ['jsx', 'typescript']
19
+
20
+ export class TelemetryCollector {
21
+ constructor() {
22
+ /** @type {Record<string, string>} short name → version e.g. { lib: '2.0.0-next.29' } */
23
+ this.versions = {}
24
+ /** @type {Map<string, number>} PascalCase component name → total usage count */
25
+ this.counts = new Map()
26
+ }
27
+
28
+ /** Clear component counts — called on every buildStart so watch-mode rebuilds are clean. */
29
+ reset() {
30
+ this.counts.clear()
31
+ }
32
+
33
+ /**
34
+ * Increment the usage count for a Wu* component and record its prop names.
35
+ * @param {string} name - e.g. 'WuButton'
36
+ * @param {string[]} props - Prop names present on this usage e.g. ['variant', 'disabled']
37
+ */
38
+ record(name, props = []) {
39
+ if (!this.counts.has(name)) this.counts.set(name, {count: 0, props: new Map()})
40
+ const entry = this.counts.get(name)
41
+ entry.count++
42
+ for (const prop of props) {
43
+ entry.props.set(prop, (entry.props.get(prop) ?? 0) + 1)
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Scan the consumer's node_modules for all @npm-questionpro/wick-ui-* packages
49
+ * and record their versions. Called once in configResolved when root is known.
50
+ *
51
+ * @param {string} root - Vite project root
52
+ */
53
+ scanVersions(root) {
54
+ const scopeDir = path.join(root, 'node_modules', '@npm-questionpro')
55
+ let entries
56
+ try {
57
+ entries = fs.readdirSync(scopeDir)
58
+ } catch {
59
+ // @npm-questionpro scope not present — skip silently
60
+ return
61
+ }
62
+ for (const name of entries) {
63
+ if (!name.startsWith('wick-ui-')) continue
64
+ try {
65
+ const pkg = JSON.parse(fs.readFileSync(path.join(scopeDir, name, 'package.json'), 'utf8'))
66
+ const shortKey = name.replace('wick-ui-', '')
67
+ this.versions[shortKey] = pkg.version
68
+ } catch {
69
+ // malformed or missing package.json — skip
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Produce the final telemetry payload: versions first (sorted), then
76
+ * component counts (sorted alphabetically).
77
+ *
78
+ * @returns {Record<string, string|number>}
79
+ */
80
+ toJSON() {
81
+ const sortedVersions = Object.fromEntries(Object.entries(this.versions).sort(([a], [b]) => a.localeCompare(b)))
82
+ const sortedCounts = Object.fromEntries(
83
+ [...this.counts.entries()]
84
+ .sort(([a], [b]) => a.localeCompare(b))
85
+ .map(([name, {count, props}]) => [
86
+ name,
87
+ {
88
+ count,
89
+ props: Object.fromEntries([...props.entries()].sort(([a], [b]) => a.localeCompare(b))),
90
+ },
91
+ ]),
92
+ )
93
+ return {...sortedVersions, ...sortedCounts}
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Parse a single file and record all Wu* JSX component usages into the collector.
99
+ * Skips files that fail to parse without throwing.
100
+ *
101
+ * @param {string} code - Raw source of the file.
102
+ * @param {TelemetryCollector} collector
103
+ */
104
+ export function collectComponents(code, collector) {
105
+ let ast
106
+ try {
107
+ ast = parse(code, {sourceType: 'module', plugins: BABEL_PLUGINS})
108
+ } catch {
109
+ return
110
+ }
111
+
112
+ traverse(ast, {
113
+ JSXOpeningElement(path) {
114
+ const name = path.node.name.name
115
+ if (typeof name === 'string' && name.startsWith('Wu')) {
116
+ const props = path.node.attributes
117
+ .filter(a => a.type === 'JSXAttribute' && typeof a.name?.name === 'string')
118
+ .map(a => a.name.name)
119
+ collector.record(name, props)
120
+ }
121
+ },
122
+ })
123
+ }