@npm-questionpro/wick-ui-i18n 2.0.0-next.7 → 2.0.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.
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @fileoverview recordWtCalls — handles wt() and useWt()() call expressions:
3
+ *
4
+ * 1. recordWtCall — records static string args into the translation dictionary.
5
+ * No code rewrite; wt/useWt handle runtime lookup.
6
+ *
7
+ * 2. transformWtTemplateLiteral — rewrites template literals with expressions
8
+ * so each static quasi becomes its own wt/useWt call:
9
+ *
10
+ * wt(`Hello ${name}`)
11
+ * → `${wt("Hello")} ${name}`
12
+ *
13
+ * useWt()(`Hello ${name}`)
14
+ * → `${useWt()("Hello")} ${name}`
15
+ *
16
+ * Both functions handle the same two call shapes:
17
+ * wt(...) — bare identifier callee
18
+ * useWt()(...) — call expression callee (useWt() returns the translate fn)
19
+ *
20
+ * Supported static arg forms:
21
+ * wt("hello") — StringLiteral → recorded, not rewritten
22
+ * wt(`hello`) — TemplateLiteral, no expressions → recorded, not rewritten
23
+ * wt(`hello ${n}`) — TemplateLiteral with expressions → each quasi recorded + rewritten
24
+ *
25
+ * Ignored:
26
+ * wt(variable) — dynamic, cannot be statically analysed
27
+ * wt() / wt(a, b) — wrong arity
28
+ */
29
+
30
+ import {splitQuasi, normalise} from './utils.js'
31
+
32
+ // ─── shared helper ────────────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * Detect whether `node` is a `wt(...)` or `useWt()(...)` call and return
36
+ * the wrapper string used to reconstruct translated quasis in the output.
37
+ *
38
+ * Returns `null` when the node is neither shape.
39
+ *
40
+ * @param {import('@babel/types').CallExpression} node
41
+ * @returns {{ wrapper: (text: string) => string, args: import('@babel/types').Node[] } | null}
42
+ */
43
+ function getWtInfo(node) {
44
+ const {callee, arguments: args} = node
45
+
46
+ // wt(...)
47
+ if (callee.type === 'Identifier' && callee.name === 'wt') {
48
+ return {wrapper: text => `wt(${JSON.stringify(text)})`, args}
49
+ }
50
+
51
+ // useWt()(...)
52
+ if (
53
+ callee.type === 'CallExpression' &&
54
+ callee.callee.type === 'Identifier' &&
55
+ callee.callee.name === 'useWt' &&
56
+ callee.arguments.length === 0
57
+ ) {
58
+ return {wrapper: text => `useWt()(${JSON.stringify(text)})`, args}
59
+ }
60
+
61
+ return null
62
+ }
63
+
64
+ // ─── recordWtCall ─────────────────────────────────────────────────────────────
65
+
66
+ /**
67
+ * If `path` is a `wt(staticString)` or `useWt()(staticString)` call, record
68
+ * the key in the processor dictionary. Returns `true` when a key was recorded.
69
+ *
70
+ * No code transformation is performed — wt/useWt handle runtime lookup.
71
+ *
72
+ * @param {import('@babel/traverse').NodePath} path - CallExpression path.
73
+ * @param {import('./processor.js').TranslationProcessor} processor
74
+ * @param {string} id - Source file path (for collision warnings).
75
+ * @returns {boolean}
76
+ */
77
+ export function recordWtCall(path, processor, id) {
78
+ const info = getWtInfo(path.node)
79
+ if (!info) return false
80
+
81
+ const {args} = info
82
+ if (args.length !== 1) return false
83
+
84
+ const arg = args[0]
85
+ let text = null
86
+
87
+ if (arg.type === 'StringLiteral') {
88
+ text = arg.value
89
+ } else if (arg.type === 'TemplateLiteral' && arg.expressions.length === 0) {
90
+ text = arg.quasis[0].value.cooked ?? arg.quasis[0].value.raw
91
+ }
92
+
93
+ if (text === null) return false
94
+
95
+ const cleanText = normalise(text)
96
+ if (!cleanText) return false
97
+
98
+ processor.record(cleanText, cleanText, id, '(wt)')
99
+ return true
100
+ }
101
+
102
+ // ─── transformWtTemplateLiteral ───────────────────────────────────────────────
103
+
104
+ /**
105
+ * Transform a `wt(...)` or `useWt()(...)` call whose argument is a TemplateLiteral
106
+ * with one or more dynamic expressions.
107
+ *
108
+ * Each static quasi is independently extracted and replaced with a nested
109
+ * `wt("text")` / `useWt()("text")` call; dynamic expressions are preserved in
110
+ * place. The whole call is replaced with a plain template literal:
111
+ *
112
+ * wt(`Hello ${name}`) → `${wt("Hello")} ${name}`
113
+ * wt(`${a} and ${b}`) → `${a} ${wt("and")} ${b}`
114
+ * useWt()(`Hello ${name}`) → `${useWt()("Hello")} ${name}`
115
+ * useWt()(`${a} and ${b}`) → `${a} ${useWt()("and")} ${b}`
116
+ *
117
+ * If no quasi contains translatable text the call is left untouched and the
118
+ * function returns `false`.
119
+ *
120
+ * @param {import('@babel/traverse').NodePath} path - CallExpression path.
121
+ * @param {string} code - Original source (for slicing expression text).
122
+ * @param {import('magic-string').default} ms
123
+ * @param {import('./processor.js').TranslationProcessor} processor
124
+ * @param {string} id - Source file path.
125
+ * @returns {boolean} `true` when the call was rewritten.
126
+ */
127
+ export function transformWtTemplateLiteral(path, code, ms, processor, id) {
128
+ const info = getWtInfo(path.node)
129
+ if (!info) return false
130
+
131
+ const {wrapper, args} = info
132
+ if (args.length !== 1) return false
133
+
134
+ const arg = args[0]
135
+ if (arg.type !== 'TemplateLiteral' || arg.expressions.length === 0) return false
136
+
137
+ const {quasis, expressions} = arg
138
+ const parts = ['`']
139
+ let hasTranslatable = false
140
+
141
+ for (let i = 0; i < quasis.length; i++) {
142
+ const cooked = quasis[i].value.cooked ?? quasis[i].value.raw
143
+ const {leading, text, trailing} = splitQuasi(cooked)
144
+
145
+ if (text) {
146
+ processor.record(text, text, id, '(wt)')
147
+ parts.push(`${leading}\${${wrapper(text)}}${trailing}`)
148
+ hasTranslatable = true
149
+ } else {
150
+ // empty or whitespace-only quasi — preserve as literal template text
151
+ parts.push(cooked)
152
+ }
153
+
154
+ if (i < expressions.length) {
155
+ const exprSrc = code.slice(expressions[i].start, expressions[i].end)
156
+ parts.push(`\${${exprSrc}}`)
157
+ }
158
+ }
159
+
160
+ parts.push('`')
161
+
162
+ if (!hasTranslatable) return false
163
+
164
+ ms.overwrite(path.node.start, path.node.end, parts.join(''))
165
+ return true
166
+ }
@@ -0,0 +1,106 @@
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
+ * except shared utilities (no domain objects).
9
+ *
10
+ * Wu* component recording is done inside transformFile's existing traversal
11
+ * (via the `telemetry` option) so no second parse is needed per file.
12
+ */
13
+
14
+ import fs from 'node:fs'
15
+ import path from 'node:path'
16
+
17
+ export class TelemetryCollector {
18
+ constructor() {
19
+ /** @type {Record<string, string>} short name → version e.g. { lib: '2.0.0-next.29' } */
20
+ this.versions = {}
21
+ /** @type {Map<string, number>} PascalCase component name → total usage count */
22
+ this.counts = new Map()
23
+ }
24
+
25
+ /** Clear component counts — called on every buildStart so watch-mode rebuilds are clean. */
26
+ reset() {
27
+ this.counts.clear()
28
+ }
29
+
30
+ /**
31
+ * Increment the usage count for a Wu* component and record its prop names.
32
+ * @param {string} name - e.g. 'WuButton'
33
+ * @param {string[]} props - Prop names present on this usage e.g. ['variant', 'disabled']
34
+ */
35
+ record(name, props = []) {
36
+ if (!this.counts.has(name)) this.counts.set(name, {count: 0, props: new Map()})
37
+ const entry = this.counts.get(name)
38
+ entry.count++
39
+ for (const prop of props) {
40
+ entry.props.set(prop, (entry.props.get(prop) ?? 0) + 1)
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Scan the consumer's node_modules for all @npm-questionpro/wick-ui-* packages
46
+ * and record their versions. Called once in configResolved when root is known.
47
+ *
48
+ * @param {string} root - Vite project root
49
+ */
50
+ scanVersions(root) {
51
+ const scopeDir = path.join(root, 'node_modules', '@npm-questionpro')
52
+ let entries
53
+ try {
54
+ entries = fs.readdirSync(scopeDir)
55
+ } catch {
56
+ // @npm-questionpro scope not present — skip silently
57
+ return
58
+ }
59
+ for (const name of entries) {
60
+ if (!name.startsWith('wick-ui-')) continue
61
+ try {
62
+ const pkg = JSON.parse(fs.readFileSync(path.join(scopeDir, name, 'package.json'), 'utf8'))
63
+ const shortKey = name.replace('wick-ui-', '')
64
+ this.versions[shortKey] = pkg.version
65
+ } catch {
66
+ // malformed or missing package.json — skip
67
+ }
68
+ }
69
+
70
+ // Also capture react and react-dom versions from the consumer's node_modules
71
+ for (const dep of ['react', 'react-dom']) {
72
+ try {
73
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'node_modules', dep, 'package.json'), 'utf8'))
74
+ this.versions[dep] = pkg.version
75
+ } catch {
76
+ // not installed — skip
77
+ }
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Produce the final telemetry payload as two clearly separated sections:
83
+ * - `versions`: package version map (sorted alphabetically)
84
+ * - `components`: Wu* usage counts with per-prop tallies (sorted alphabetically)
85
+ *
86
+ * Keeping the two sections distinct prevents a component named "react" or
87
+ * "lib" from silently shadowing a version entry.
88
+ *
89
+ * @returns {{ versions: Record<string, string>, components: Record<string, object> }}
90
+ */
91
+ toJSON() {
92
+ const versions = Object.fromEntries(Object.entries(this.versions).sort(([a], [b]) => a.localeCompare(b)))
93
+ const components = Object.fromEntries(
94
+ [...this.counts.entries()]
95
+ .sort(([a], [b]) => a.localeCompare(b))
96
+ .map(([name, {count, props}]) => [
97
+ name,
98
+ {
99
+ count,
100
+ props: Object.fromEntries([...props.entries()].sort(([a], [b]) => a.localeCompare(b))),
101
+ },
102
+ ]),
103
+ )
104
+ return {versions, components}
105
+ }
106
+ }
package/src/transform.js CHANGED
@@ -1,36 +1,23 @@
1
1
  /**
2
2
  * @fileoverview AST transform — parses a JSX/TSX/TS file and:
3
3
  * - Rewrites JSX text content inside Wu* components to `<WuTranslate>`
4
- * - Rewrites translatable props (placeholder, title, …) to `{wt("…")}`
4
+ * - Rewrites translatable props (placeholder, title, …) to `{useWt()("…")}`
5
5
  * - Prepends the necessary imports when added.
6
+ * - Optionally records Wu* component usages into a TelemetryCollector.
6
7
  */
7
8
 
8
9
  import MagicString from 'magic-string'
9
10
  import {parse} from '@babel/parser'
10
- import _traverse from '@babel/traverse'
11
11
  import {getComponentTree} from './debug.js'
12
12
  import {transformTemplateLiteralExpression} from './transformTemplateLiteral.js'
13
- import {transformJSXTextWithEntities} from './transformJSXTextWithEntities.js'
14
- import {recordWtCall, transformWtTemplateLiteral} from './transformWtCalls.js'
15
-
16
- const traverse = _traverse.default || _traverse
17
-
18
- /** Babel parser plugins applied to every file. */
19
- const BABEL_PLUGINS = ['jsx', 'typescript']
20
-
21
- /**
22
- * Matches any HTML entity: named (&amp;), decimal (&#169;), or hex (&#x00A9;).
23
- * Used to skip text segments that contain entities — they must be left as-is.
24
- * Note: for JSXText this must be tested against the RAW source, not the Babel
25
- * decoded .value (Babel turns &amp; → "&", &nbsp; → "\u00a0", etc.).
26
- */
27
- const HTML_ENTITY_RE = /&(?:[a-zA-Z][a-zA-Z0-9]*|#[0-9]+|#x[0-9a-fA-F]+);/
13
+ import {transformJsxTextWithEntities} from './transformReactTextWithEntities.js'
14
+ import {recordWtCall, transformWtTemplateLiteral} from './recordWtCalls.js'
15
+ import {traverse, BABEL_PLUGINS, HTML_ENTITY_RE, normalise, splitQuasi} from './utils.js'
28
16
 
29
17
  /** @param {import('@babel/types').Node} node @returns {string|null} */
30
18
  function getStaticString(node) {
31
19
  if (node.type === 'StringLiteral') return node.value
32
- if (node.type === 'TemplateLiteral' && !node.expressions.length)
33
- return node.quasis[0].value.cooked
20
+ if (node.type === 'TemplateLiteral' && !node.expressions.length) return node.quasis[0].value.cooked
34
21
  return null
35
22
  }
36
23
 
@@ -51,17 +38,7 @@ function handleConditional(expr, path, ms, processor, id) {
51
38
  for (const branch of [expr.consequent, expr.alternate]) {
52
39
  const text = getStaticString(branch)
53
40
  if (text !== null) {
54
- changed =
55
- handleCapture(
56
- path,
57
- text,
58
- branch.start,
59
- branch.end,
60
- ms,
61
- processor,
62
- id,
63
- true,
64
- ) || changed
41
+ changed = handleCapture(path, text, branch.start, branch.end, ms, processor, id, true) || changed
65
42
  } else if (branch.type === 'ConditionalExpression') {
66
43
  changed = handleConditional(branch, path, ms, processor, id) || changed
67
44
  }
@@ -83,20 +60,8 @@ function handleConditional(expr, path, ms, processor, id) {
83
60
  * @param {string} id - File path (for collision warnings).
84
61
  * @returns {boolean} `true` when replacement was made.
85
62
  */
86
- function handleCapture(
87
- path,
88
- text,
89
- start,
90
- end,
91
- ms,
92
- processor,
93
- id,
94
- skipExplicitKey = false,
95
- ) {
96
- const cleanText = text
97
- .trim()
98
- .replace(/\n/g, ' ')
99
- .replace(/\s{2,}/g, ' ')
63
+ function handleCapture(path, text, start, end, ms, processor, id, skipExplicitKey = false) {
64
+ const cleanText = normalise(text)
100
65
  if (!cleanText || !processor.shouldTranslate(path)) return false
101
66
  // For StringLiteral / TemplateLiteral quasis / ternary branches: entities are
102
67
  // not decoded by the JS parser, so cleanText still contains "&amp;" etc.
@@ -105,11 +70,67 @@ function handleCapture(
105
70
  const key = (!skipExplicitKey && processor.getExplicitKey(path)) || cleanText
106
71
  processor.record(key, cleanText, id, getComponentTree(path))
107
72
 
108
- ms.overwrite(
109
- start,
110
- end,
111
- `<WuTranslate __i18nKey=${JSON.stringify(key)}></WuTranslate>`,
112
- )
73
+ ms.overwrite(start, end, `<WuTranslate __i18nKey=${JSON.stringify(key)}></WuTranslate>`)
74
+ return true
75
+ }
76
+
77
+ /**
78
+ * Transform a JSX prop value that is a TemplateLiteral into a useWt()() call
79
+ * (or a new template literal with useWt()() for each static quasi).
80
+ *
81
+ * Static: Label={`hello`} → Label={useWt()("hello")}
82
+ * Dynamic: Label={`${name} hello`} → Label={`${name} ${useWt()("hello")}`}
83
+ * Dynamic: Label={`${a} and ${b}`} → Label={`${a} ${useWt()("and")} ${b}`}
84
+ *
85
+ * @param {import('@babel/traverse').NodePath} path - JSXAttribute path.
86
+ * @param {import('@babel/types').TemplateLiteral} expr
87
+ * @param {string} code
88
+ * @param {import('magic-string').default} ms
89
+ * @param {import('./processor.js').TranslationProcessor} processor
90
+ * @param {string} id
91
+ * @returns {boolean} `true` when a replacement was written.
92
+ */
93
+ function transformPropTemplateLiteral(path, expr, code, ms, processor, id) {
94
+ if (!processor.shouldTranslateProp(path.node.name.name, path)) return false
95
+
96
+ const {quasis, expressions} = expr
97
+ const componentTree = getComponentTree(path)
98
+
99
+ // Static template literal: no expressions
100
+ if (expressions.length === 0) {
101
+ const text = normalise(quasis[0].value.cooked ?? quasis[0].value.raw)
102
+ if (!text || HTML_ENTITY_RE.test(text)) return false
103
+ processor.record(text, text, id, componentTree)
104
+ // Overwrite the entire JSXExpressionContainer
105
+ ms.overwrite(path.node.value.start, path.node.value.end, `{useWt()(${JSON.stringify(text)})}`)
106
+ return true
107
+ }
108
+
109
+ // Dynamic template literal: walk quasis, replace static parts with useWt()()
110
+ const parts = ['`']
111
+ let hasTranslatable = false
112
+
113
+ for (let i = 0; i < quasis.length; i++) {
114
+ const cooked = quasis[i].value.cooked ?? quasis[i].value.raw
115
+ const {leading, text, trailing} = splitQuasi(cooked)
116
+
117
+ if (text && !HTML_ENTITY_RE.test(text)) {
118
+ processor.record(text, text, id, componentTree)
119
+ parts.push(`${leading}\${useWt()(${JSON.stringify(text)})}${trailing}`)
120
+ hasTranslatable = true
121
+ } else {
122
+ parts.push(cooked)
123
+ }
124
+
125
+ if (i < expressions.length) {
126
+ parts.push(`\${${code.slice(expressions[i].start, expressions[i].end)}}`)
127
+ }
128
+ }
129
+
130
+ parts.push('`')
131
+ if (!hasTranslatable) return false
132
+
133
+ ms.overwrite(path.node.value.start, path.node.value.end, `{${parts.join('')}}`)
113
134
  return true
114
135
  }
115
136
 
@@ -120,10 +141,14 @@ function handleCapture(
120
141
  * @param {string} code - Raw source of the file.
121
142
  * @param {string} id - File path.
122
143
  * @param {import('./processor.js').TranslationProcessor} processor
144
+ * @param {object} [options]
145
+ * @param {import('./telemetry.js').TelemetryCollector|null} [options.telemetry]
146
+ * When provided, Wu* JSX component usages are recorded into this collector
147
+ * inside the same traversal — avoiding a second parse of the file.
123
148
  * @returns {{ code: string, map: object } | null} Transformed result, or
124
149
  * `null` when no changes were made.
125
150
  */
126
- export function transformFile(code, id, processor) {
151
+ export function transformFile(code, id, processor, {telemetry} = {}) {
127
152
  const ast = parse(code, {
128
153
  sourceType: 'module',
129
154
  plugins: BABEL_PLUGINS,
@@ -132,19 +157,18 @@ export function transformFile(code, id, processor) {
132
157
  const ms = new MagicString(code)
133
158
  let needsImport = false
134
159
  let hasImport = false
135
- let hasWtTransform = false
136
- let needsWtImport = false
137
- let hasWtImport = false
160
+ let needsUseWtImport = false
161
+ let hasUseWtImport = false
138
162
 
139
163
  traverse(ast, {
140
- /** wt("static string") call — record the key in the dictionary.
141
- * No code transformation; wt() handles runtime lookup.
164
+ /**
165
+ * wt("static string") / useWt()("static string") record the key.
166
+ * wt(`quasi ${expr}`) / useWt()(`quasi ${expr}`) — rewrite each static
167
+ * quasi to its own wt/useWt call and record it.
142
168
  */
143
169
  CallExpression(path) {
144
170
  if (recordWtCall(path, processor, id)) return
145
- if (transformWtTemplateLiteral(path, code, ms, processor, id)) {
146
- hasWtTransform = true
147
- }
171
+ transformWtTemplateLiteral(path, code, ms, processor, id)
148
172
  },
149
173
 
150
174
  /** Track whether WuTranslate / wt are already imported so we don't duplicate them. */
@@ -152,7 +176,7 @@ export function transformFile(code, id, processor) {
152
176
  if (path.node.source.value.includes('wick-ui-lib')) {
153
177
  for (const s of path.node.specifiers) {
154
178
  if (s.imported?.name === 'WuTranslate') hasImport = true
155
- if (s.imported?.name === 'wt') hasWtImport = true
179
+ if (s.imported?.name === 'useWt') hasUseWtImport = true
156
180
  }
157
181
  }
158
182
  },
@@ -160,7 +184,7 @@ export function transformFile(code, id, processor) {
160
184
  /**
161
185
  * Data-file key extraction: records string values of configured object
162
186
  * property names (e.g. `label: 'Analytics'`) without rewriting code.
163
- * Enabled via `extractFromKeys` option.
187
+ * Enabled via the `dataLabelKeys` plugin option.
164
188
  */
165
189
  ObjectProperty(path) {
166
190
  if (!processor.extractFromKeys.size) return
@@ -169,10 +193,7 @@ export function transformFile(code, id, processor) {
169
193
  if (!keyName || !processor.extractFromKeys.has(keyName)) return
170
194
  const value = path.node.value
171
195
  if (value.type !== 'StringLiteral') return
172
- const text = value.value
173
- .trim()
174
- .replace(/\n/g, ' ')
175
- .replace(/\s{2,}/g, ' ')
196
+ const text = normalise(value.value)
176
197
  if (!text) return
177
198
  if (HTML_ENTITY_RE.test(text)) return
178
199
  processor.record(text, text, id, '(data)')
@@ -180,28 +201,39 @@ export function transformFile(code, id, processor) {
180
201
 
181
202
  /**
182
203
  * Translatable props (Label, placeholder, title, aria-label, ...)
183
- * rewritten to `{wt("foo")}` on Wu* components.
204
+ * rewritten to `{useWt()("foo")}` on Wu* components.
205
+ *
206
+ * Handles all three value shapes:
207
+ * Label="hello" → Label={useWt()("hello")}
208
+ * Label={`hello`} → Label={useWt()("hello")}
209
+ * Label={`${name} hello`} → Label={`${name} ${useWt()("hello")}`}
184
210
  */
185
211
  JSXAttribute(path) {
186
212
  const propName = path.node.name.name
187
213
  if (typeof propName !== 'string') return
188
214
  const value = path.node.value
189
- if (!value || value.type !== 'StringLiteral') return
190
-
191
- const rawValue = code.slice(value.start, value.end)
192
- if (HTML_ENTITY_RE.test(rawValue)) return
215
+ if (!value) return
193
216
 
194
- const text = value.value
195
- .trim()
196
- .replace(/\n/g, ' ')
197
- .replace(/\s{2,}/g, ' ')
198
- if (!text) return
199
-
200
- if (!processor.shouldTranslateProp(propName, path)) return
217
+ // Shape 1: StringLiteral — Label="hello"
218
+ if (value.type === 'StringLiteral') {
219
+ const rawValue = code.slice(value.start, value.end)
220
+ if (HTML_ENTITY_RE.test(rawValue)) return
221
+ const text = normalise(value.value)
222
+ if (!text) return
223
+ if (!processor.shouldTranslateProp(propName, path)) return
224
+ processor.record(text, text, id, getComponentTree(path))
225
+ ms.overwrite(value.start, value.end, `{useWt()(${JSON.stringify(text)})}`)
226
+ needsUseWtImport = true
227
+ return
228
+ }
201
229
 
202
- processor.record(text, text, id, getComponentTree(path))
203
- ms.overwrite(value.start, value.end, `{wt(${JSON.stringify(text)})}`)
204
- needsWtImport = true
230
+ // Shape 2 & 3: JSXExpressionContainer wrapping a TemplateLiteral
231
+ // Label={`hello`} or Label={`${name} hello`}
232
+ if (value.type === 'JSXExpressionContainer' && value.expression.type === 'TemplateLiteral') {
233
+ if (transformPropTemplateLiteral(path, value.expression, code, ms, processor, id)) {
234
+ needsUseWtImport = true
235
+ }
236
+ }
205
237
  },
206
238
 
207
239
  /** Plain JSX text: `<Foo>Hello world</Foo>` */
@@ -213,23 +245,13 @@ export function transformFile(code, id, processor) {
213
245
  // translatable text segments are still wrapped while entities stay put.
214
246
  const rawSource = code.slice(path.node.start, path.node.end)
215
247
  if (HTML_ENTITY_RE.test(rawSource)) {
216
- if (transformJSXTextWithEntities(path, rawSource, ms, processor, id)) {
248
+ if (transformJsxTextWithEntities(path, rawSource, ms, processor, id)) {
217
249
  needsImport = true
218
250
  }
219
251
  return
220
252
  }
221
253
  const start = path.node.start + text.indexOf(trimmed)
222
- if (
223
- handleCapture(
224
- path,
225
- trimmed,
226
- start,
227
- start + trimmed.length,
228
- ms,
229
- processor,
230
- id,
231
- )
232
- ) {
254
+ if (handleCapture(path, trimmed, start, start + trimmed.length, ms, processor, id)) {
233
255
  needsImport = true
234
256
  }
235
257
  },
@@ -249,10 +271,7 @@ export function transformFile(code, id, processor) {
249
271
 
250
272
  if (expr.type === 'StringLiteral') {
251
273
  text = expr.value
252
- } else if (
253
- expr.type === 'TemplateLiteral' &&
254
- expr.expressions.length > 0
255
- ) {
274
+ } else if (expr.type === 'TemplateLiteral' && expr.expressions.length > 0) {
256
275
  if (transformTemplateLiteralExpression(path, code, ms, processor, id)) {
257
276
  needsImport = true
258
277
  }
@@ -264,27 +283,30 @@ export function transformFile(code, id, processor) {
264
283
  return
265
284
  }
266
285
 
267
- if (
268
- text &&
269
- handleCapture(
270
- path,
271
- text,
272
- path.node.start,
273
- path.node.end,
274
- ms,
275
- processor,
276
- id,
277
- )
278
- ) {
286
+ if (text && handleCapture(path, text, path.node.start, path.node.end, ms, processor, id)) {
279
287
  needsImport = true
280
288
  }
281
289
  },
290
+
291
+ /**
292
+ * Telemetry: record Wu* component usages when a collector is provided.
293
+ * Runs in the same traversal to avoid parsing the file a second time.
294
+ */
295
+ JSXOpeningElement(path) {
296
+ if (!telemetry) return
297
+ const name = path.node.name.name
298
+ if (typeof name !== 'string' || !name.startsWith('Wu')) return
299
+ const props = path.node.attributes
300
+ .filter(a => a.type === 'JSXAttribute' && typeof a.name?.name === 'string')
301
+ .map(a => a.name.name)
302
+ telemetry.record(name, props)
303
+ },
282
304
  })
283
305
 
284
- if (!needsImport && !hasWtTransform && !needsWtImport) return null
306
+ if (!ms.hasChanged()) return null
285
307
 
286
- if (needsWtImport && !hasWtImport) {
287
- ms.prepend(`import { wt } from '@npm-questionpro/wick-ui-lib';\n`)
308
+ if (needsUseWtImport && !hasUseWtImport) {
309
+ ms.prepend(`import { useWt } from '@npm-questionpro/wick-ui-lib';\n`)
288
310
  }
289
311
 
290
312
  if (needsImport && !hasImport) {