vite-plugin-vue-testid 1.0.3 → 1.0.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/README.en.md CHANGED
@@ -173,6 +173,18 @@ a-tree-select-0-dropdown-tree-parent-1 # parent node (injected after
173
173
  a-tree-select-0-dropdown-tree-my-leaf # leaf node
174
174
  ```
175
175
 
176
+ ### Compile-time Menu
177
+
178
+ ```
179
+ a-menu-0 # menu container
180
+ a-menu-item-1 # menu item
181
+ a-menu-item-2 # menu item
182
+ a-sub-menu-3 # submenu (title)
183
+ a-menu-item-4 # submenu item
184
+ a-menu-item-group-5 # menu item group (title)
185
+ a-menu-item-6 # group item
186
+ ```
187
+
176
188
  ---
177
189
 
178
190
  ## Configuration
@@ -289,6 +301,10 @@ injectCurrentDropdowns({ attributeName: 'data-testid' })
289
301
  | `a-form-item` | ✅ | — |
290
302
  | `a-button` | ✅ | — |
291
303
  | `a-modal` | ✅ id+testid | — |
304
+ | `a-menu` | ✅ | — |
305
+ | `a-menu-item` | ✅ | — |
306
+ | `a-sub-menu` | ✅ | — |
307
+ | `a-menu-item-group` | ✅ | — |
292
308
 
293
309
  ---
294
310
 
package/README.md CHANGED
@@ -173,6 +173,18 @@ a-tree-select-0-dropdown-tree-parent-1 # 父节点(展开后注入
173
173
  a-tree-select-0-dropdown-tree-my-leaf # 叶子节点
174
174
  ```
175
175
 
176
+ ### 编译期 Menu
177
+
178
+ ```
179
+ a-menu-0 # 菜单容器
180
+ a-menu-item-1 # 菜单项
181
+ a-menu-item-2 # 菜单项
182
+ a-sub-menu-3 # 子菜单(标题)
183
+ a-menu-item-4 # 子菜单内的菜单项
184
+ a-menu-item-group-5 # 菜单分组(标题)
185
+ a-menu-item-6 # 分组内的菜单项
186
+ ```
187
+
176
188
  ---
177
189
 
178
190
  ## 配置
@@ -289,6 +301,10 @@ injectCurrentDropdowns({ attributeName: 'data-testid' })
289
301
  | `a-form-item` | ✅ | — |
290
302
  | `a-button` | ✅ | — |
291
303
  | `a-modal` | ✅ id+testid | — |
304
+ | `a-menu` | ✅ | — |
305
+ | `a-menu-item` | ✅ | — |
306
+ | `a-sub-menu` | ✅ | — |
307
+ | `a-menu-item-group` | ✅ | — |
292
308
 
293
309
  ---
294
310
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,QAAQ,EAER,iBAAiB,EAClB,MAAM,oBAAoB,CAAA;AAI3B,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IAErB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAEvD;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IAEvB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAuMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,CAAC,EAAE,yBAAyB,GAClC,CAAC,IAAI,EAAE,QAAQ,GAAG,iBAAiB,KAAK,IAAI,CAG9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,QAAQ,EAER,iBAAiB,EAClB,MAAM,oBAAoB,CAAA;AAI3B,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IAErB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAEvD;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IAEvB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAwMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,CAAC,EAAE,yBAAyB,GAClC,CAAC,IAAI,EAAE,QAAQ,GAAG,iBAAiB,KAAK,IAAI,CAG9C"}
package/dist/index.js CHANGED
@@ -18,7 +18,11 @@ var DEFAULT_COMPONENTS = [
18
18
  "a-upload",
19
19
  "a-form-item",
20
20
  "a-button",
21
- "a-modal"
21
+ "a-modal",
22
+ "a-menu",
23
+ "a-menu-item",
24
+ "a-sub-menu",
25
+ "a-menu-item-group"
22
26
  ];
23
27
  var DEFAULT_ID_COMPONENTS = [
24
28
  "a-date-picker",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { ConstantTypes, NodeTypes } from '@vue/compiler-core'\nimport type {\n AttributeNode,\n DirectiveNode,\n ExpressionNode,\n RootNode,\n SimpleExpressionNode,\n TemplateChildNode,\n} from '@vue/compiler-core'\n\n// ──────────────────────── Types ────────────────────────\n\nexport interface VueTestIdTransformOptions {\n /**\n * 需要注入 testId 的组件标签列表(kebab-case)\n * @default ant-design-vue 常用表单组件\n */\n components?: string[]\n\n /**\n * 自定义 testId 生成函数\n * @param tagName 组件标签名\n * @param count 全局递增序号\n * @returns testId 字符串\n */\n generateId?: (tagName: string, count: number) => string\n\n /**\n * testId 属性名\n * @default 'data-testid'\n */\n attributeName?: string\n\n /**\n * 需要同时注入 id + testId 的组件(如弹窗、日期选择器等 teleport 组件)\n * @default ['a-date-picker', 'a-range-picker', 'a-time-picker', 'a-modal']\n */\n idComponents?: string[]\n\n /**\n * teleport 组件的 id 属性名\n * @default 'id'\n */\n idAttributeName?: string\n}\n\ntype ResolvedOptions = Required<VueTestIdTransformOptions>\n\nconst DEFAULT_COMPONENTS = [\n 'a-input', 'a-textarea', 'a-input-number',\n 'a-select', 'a-cascader', 'a-tree-select',\n 'a-radio-group', 'a-checkbox-group',\n 'a-date-picker', 'a-range-picker', 'a-time-picker',\n 'a-switch', 'a-slider', 'a-rate', 'a-upload',\n 'a-form-item',\n 'a-button',\n 'a-modal',\n]\n\nconst DEFAULT_ID_COMPONENTS = [\n 'a-date-picker', 'a-range-picker', 'a-time-picker', 'a-modal', 'a-upload',\n]\n\n// ──────────────────────── Helpers ────────────────────────\n\nfunction resolveOptions(raw?: VueTestIdTransformOptions): ResolvedOptions {\n return {\n components: raw?.components ?? DEFAULT_COMPONENTS,\n generateId: raw?.generateId ?? ((tag, n) => `${tag}-${n}`),\n attributeName: raw?.attributeName ?? 'data-testid',\n idComponents: raw?.idComponents ?? DEFAULT_ID_COMPONENTS,\n idAttributeName: raw?.idAttributeName ?? 'id',\n }\n}\n\n/**\n * 确保 slot props 解构表达式中包含 index。\n * 兼容 SIMPLE_EXPRESSION (type=4) 和 COMPOUND_EXPRESSION (type=8)。\n */\nfunction ensureSlotIndex(exp: ExpressionNode | undefined): void {\n if (!exp) return\n\n if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {\n if (!/\\bindex\\b/.test(exp.content)) {\n exp.content = exp.content.replace(/\\s*\\}$/, ', index}')\n }\n } else if (exp.type === NodeTypes.COMPOUND_EXPRESSION) {\n const hasIndex = exp.children.some(c => {\n if (typeof c === 'string') return /\\bindex\\b/.test(c)\n if (typeof c === 'symbol') return false\n if (c.type === NodeTypes.TEXT || c.type === NodeTypes.SIMPLE_EXPRESSION) {\n return /\\bindex\\b/.test(c.content)\n }\n return false\n })\n if (hasIndex) return\n\n for (let i = exp.children.length - 1; i >= 0; i--) {\n const c = exp.children[i]\n if (typeof c === 'string') {\n exp.children[i] = c.replace(/}$/, ', index}')\n break\n }\n if (typeof c !== 'symbol' && c.type === NodeTypes.TEXT) {\n c.content = c.content.replace(/}$/, ', index}')\n break\n }\n }\n }\n}\n\nfunction hasExistingTestId(\n node: { props: (AttributeNode | DirectiveNode)[] },\n attrName: string,\n): boolean {\n return node.props.some(\n p =>\n (p.type === NodeTypes.ATTRIBUTE && p.name === attrName) ||\n (p.type === NodeTypes.DIRECTIVE &&\n p.name === 'bind' &&\n p.arg &&\n (p.arg as SimpleExpressionNode).type === NodeTypes.SIMPLE_EXPRESSION &&\n (p.arg as SimpleExpressionNode).content === attrName),\n )\n}\n\nfunction createStaticAttr(name: string, value: string, loc: RootNode['loc']): AttributeNode {\n return {\n type: NodeTypes.ATTRIBUTE,\n name,\n nameLoc: loc,\n value: { type: NodeTypes.TEXT, content: value, loc },\n loc,\n }\n}\n\nfunction createBindDirective(\n attrName: string,\n exprContent: string,\n loc: RootNode['loc'],\n): DirectiveNode {\n return {\n type: NodeTypes.DIRECTIVE,\n name: 'bind',\n arg: {\n type: NodeTypes.SIMPLE_EXPRESSION,\n content: attrName,\n isStatic: true,\n constType: ConstantTypes.CAN_STRINGIFY,\n loc,\n },\n exp: {\n type: NodeTypes.SIMPLE_EXPRESSION,\n content: exprContent,\n isStatic: false,\n constType: ConstantTypes.NOT_CONSTANT,\n loc,\n },\n modifiers: [],\n loc,\n }\n}\n\n// ──────────────────────── Core Transform ────────────────────────\n\n/**\n * 创建 Vue 编译器 nodeTransform,自动为模板元素注入 data-testid。\n *\n * 特性:\n * - 自动跳过手动标注了 data-testid 的元素\n * - 支持 <a-table> 内 #bodyCell 插槽的动态 testId(行+列唯一)\n * - 自动补全 bodyCell slot props 中的 index 解构\n * - teleport 组件(日期选择器、弹窗)同时注入 id + data-testid\n */\nfunction createASTTransform(opts: ResolvedOptions) {\n let count = 0\n let insideTableDepth = 0\n let insideBodyCellDepth = 0\n\n const componentSet = new Set(opts.components)\n const idComponentSet = new Set(opts.idComponents)\n\n return (node: RootNode | TemplateChildNode) => {\n if (node.type !== NodeTypes.ELEMENT) return\n\n const tagName = node.tag.toLowerCase()\n\n // ──────────── <a-table> 上下文 ────────────\n if (tagName === 'a-table') {\n insideTableDepth++\n return () => { insideTableDepth-- }\n }\n\n // ──────────── <template #bodyCell> 上下文 ────────────\n if (tagName === 'template' && insideTableDepth > 0) {\n const slotDir = node.props.find(\n p =>\n p.type === NodeTypes.DIRECTIVE &&\n p.name === 'slot' &&\n p.arg &&\n (p.arg as SimpleExpressionNode).type === NodeTypes.SIMPLE_EXPRESSION &&\n (p.arg as SimpleExpressionNode).content === 'bodyCell',\n )\n if (slotDir && slotDir.type === NodeTypes.DIRECTIVE) {\n ensureSlotIndex(slotDir.exp)\n insideBodyCellDepth++\n return () => { insideBodyCellDepth-- }\n }\n }\n\n // table 内部但非 bodyCell → 跳过\n if (insideTableDepth > 0 && insideBodyCellDepth === 0) return\n\n // 非目标组件 → 跳过\n if (!componentSet.has(tagName)) return\n\n // 已有 testId → 跳过\n if (hasExistingTestId(node, opts.attributeName)) return\n\n const testIdValue = opts.generateId(tagName, count++)\n\n // ── bodyCell 内:动态 bind(引用 column.dataIndex + index) ──\n if (insideBodyCellDepth > 0) {\n const expr =\n `\\`\\${column?.dataIndex || column?.key || 'cell'}-\\${index}-${testIdValue}\\``\n node.props.push(createBindDirective(opts.attributeName, expr, node.loc))\n return\n }\n\n // ── teleport 组件:id + data-testid 双注入 ──\n if (idComponentSet.has(tagName)) {\n node.props.push(createStaticAttr(opts.idAttributeName, testIdValue, node.loc))\n node.props.push(createStaticAttr(opts.attributeName, testIdValue, node.loc))\n return\n }\n\n // ── 普通组件:静态 data-testid ──\n node.props.push(createStaticAttr(opts.attributeName, testIdValue, node.loc))\n }\n}\n\n// ──────────────────────── Public API ────────────────────────\n\n/**\n * 创建 Vue 编译器 nodeTransform 函数。\n * 直接传给 `@vitejs/plugin-vue` 的 `template.compilerOptions.nodeTransforms`。\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import vue from '@vitejs/plugin-vue'\n * import { createVueTestIdTransform } from 'vite-plugin-vue-testid'\n *\n * export default defineConfig({\n * plugins: [\n * vue({\n * template: {\n * compilerOptions: {\n * nodeTransforms: [createVueTestIdTransform()]\n * }\n * }\n * })\n * ]\n * })\n * ```\n *\n * @example 自定义配置\n * ```ts\n * createVueTestIdTransform({\n * components: ['a-input', 'a-select', 'el-input'],\n * generateId: (tag, n) => `myapp-${tag}-${n}`,\n * attributeName: 'data-cy',\n * })\n * ```\n */\nexport function createVueTestIdTransform(\n rawOpts?: VueTestIdTransformOptions,\n): (node: RootNode | TemplateChildNode) => void {\n const opts = resolveOptions(rawOpts)\n return createASTTransform(opts)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { ConstantTypes, NodeTypes } from '@vue/compiler-core'\nimport type {\n AttributeNode,\n DirectiveNode,\n ExpressionNode,\n RootNode,\n SimpleExpressionNode,\n TemplateChildNode,\n} from '@vue/compiler-core'\n\n// ──────────────────────── Types ────────────────────────\n\nexport interface VueTestIdTransformOptions {\n /**\n * 需要注入 testId 的组件标签列表(kebab-case)\n * @default ant-design-vue 常用表单组件\n */\n components?: string[]\n\n /**\n * 自定义 testId 生成函数\n * @param tagName 组件标签名\n * @param count 全局递增序号\n * @returns testId 字符串\n */\n generateId?: (tagName: string, count: number) => string\n\n /**\n * testId 属性名\n * @default 'data-testid'\n */\n attributeName?: string\n\n /**\n * 需要同时注入 id + testId 的组件(如弹窗、日期选择器等 teleport 组件)\n * @default ['a-date-picker', 'a-range-picker', 'a-time-picker', 'a-modal']\n */\n idComponents?: string[]\n\n /**\n * teleport 组件的 id 属性名\n * @default 'id'\n */\n idAttributeName?: string\n}\n\ntype ResolvedOptions = Required<VueTestIdTransformOptions>\n\nconst DEFAULT_COMPONENTS = [\n 'a-input', 'a-textarea', 'a-input-number',\n 'a-select', 'a-cascader', 'a-tree-select',\n 'a-radio-group', 'a-checkbox-group',\n 'a-date-picker', 'a-range-picker', 'a-time-picker',\n 'a-switch', 'a-slider', 'a-rate', 'a-upload',\n 'a-form-item',\n 'a-button',\n 'a-modal',\n 'a-menu', 'a-menu-item', 'a-sub-menu', 'a-menu-item-group',\n]\n\nconst DEFAULT_ID_COMPONENTS = [\n 'a-date-picker', 'a-range-picker', 'a-time-picker', 'a-modal', 'a-upload',\n]\n\n// ──────────────────────── Helpers ────────────────────────\n\nfunction resolveOptions(raw?: VueTestIdTransformOptions): ResolvedOptions {\n return {\n components: raw?.components ?? DEFAULT_COMPONENTS,\n generateId: raw?.generateId ?? ((tag, n) => `${tag}-${n}`),\n attributeName: raw?.attributeName ?? 'data-testid',\n idComponents: raw?.idComponents ?? DEFAULT_ID_COMPONENTS,\n idAttributeName: raw?.idAttributeName ?? 'id',\n }\n}\n\n/**\n * 确保 slot props 解构表达式中包含 index。\n * 兼容 SIMPLE_EXPRESSION (type=4) 和 COMPOUND_EXPRESSION (type=8)。\n */\nfunction ensureSlotIndex(exp: ExpressionNode | undefined): void {\n if (!exp) return\n\n if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {\n if (!/\\bindex\\b/.test(exp.content)) {\n exp.content = exp.content.replace(/\\s*\\}$/, ', index}')\n }\n } else if (exp.type === NodeTypes.COMPOUND_EXPRESSION) {\n const hasIndex = exp.children.some(c => {\n if (typeof c === 'string') return /\\bindex\\b/.test(c)\n if (typeof c === 'symbol') return false\n if (c.type === NodeTypes.TEXT || c.type === NodeTypes.SIMPLE_EXPRESSION) {\n return /\\bindex\\b/.test(c.content)\n }\n return false\n })\n if (hasIndex) return\n\n for (let i = exp.children.length - 1; i >= 0; i--) {\n const c = exp.children[i]\n if (typeof c === 'string') {\n exp.children[i] = c.replace(/}$/, ', index}')\n break\n }\n if (typeof c !== 'symbol' && c.type === NodeTypes.TEXT) {\n c.content = c.content.replace(/}$/, ', index}')\n break\n }\n }\n }\n}\n\nfunction hasExistingTestId(\n node: { props: (AttributeNode | DirectiveNode)[] },\n attrName: string,\n): boolean {\n return node.props.some(\n p =>\n (p.type === NodeTypes.ATTRIBUTE && p.name === attrName) ||\n (p.type === NodeTypes.DIRECTIVE &&\n p.name === 'bind' &&\n p.arg &&\n (p.arg as SimpleExpressionNode).type === NodeTypes.SIMPLE_EXPRESSION &&\n (p.arg as SimpleExpressionNode).content === attrName),\n )\n}\n\nfunction createStaticAttr(name: string, value: string, loc: RootNode['loc']): AttributeNode {\n return {\n type: NodeTypes.ATTRIBUTE,\n name,\n nameLoc: loc,\n value: { type: NodeTypes.TEXT, content: value, loc },\n loc,\n }\n}\n\nfunction createBindDirective(\n attrName: string,\n exprContent: string,\n loc: RootNode['loc'],\n): DirectiveNode {\n return {\n type: NodeTypes.DIRECTIVE,\n name: 'bind',\n arg: {\n type: NodeTypes.SIMPLE_EXPRESSION,\n content: attrName,\n isStatic: true,\n constType: ConstantTypes.CAN_STRINGIFY,\n loc,\n },\n exp: {\n type: NodeTypes.SIMPLE_EXPRESSION,\n content: exprContent,\n isStatic: false,\n constType: ConstantTypes.NOT_CONSTANT,\n loc,\n },\n modifiers: [],\n loc,\n }\n}\n\n// ──────────────────────── Core Transform ────────────────────────\n\n/**\n * 创建 Vue 编译器 nodeTransform,自动为模板元素注入 data-testid。\n *\n * 特性:\n * - 自动跳过手动标注了 data-testid 的元素\n * - 支持 <a-table> 内 #bodyCell 插槽的动态 testId(行+列唯一)\n * - 自动补全 bodyCell slot props 中的 index 解构\n * - teleport 组件(日期选择器、弹窗)同时注入 id + data-testid\n */\nfunction createASTTransform(opts: ResolvedOptions) {\n let count = 0\n let insideTableDepth = 0\n let insideBodyCellDepth = 0\n\n const componentSet = new Set(opts.components)\n const idComponentSet = new Set(opts.idComponents)\n\n return (node: RootNode | TemplateChildNode) => {\n if (node.type !== NodeTypes.ELEMENT) return\n\n const tagName = node.tag.toLowerCase()\n\n // ──────────── <a-table> 上下文 ────────────\n if (tagName === 'a-table') {\n insideTableDepth++\n return () => { insideTableDepth-- }\n }\n\n // ──────────── <template #bodyCell> 上下文 ────────────\n if (tagName === 'template' && insideTableDepth > 0) {\n const slotDir = node.props.find(\n p =>\n p.type === NodeTypes.DIRECTIVE &&\n p.name === 'slot' &&\n p.arg &&\n (p.arg as SimpleExpressionNode).type === NodeTypes.SIMPLE_EXPRESSION &&\n (p.arg as SimpleExpressionNode).content === 'bodyCell',\n )\n if (slotDir && slotDir.type === NodeTypes.DIRECTIVE) {\n ensureSlotIndex(slotDir.exp)\n insideBodyCellDepth++\n return () => { insideBodyCellDepth-- }\n }\n }\n\n // table 内部但非 bodyCell → 跳过\n if (insideTableDepth > 0 && insideBodyCellDepth === 0) return\n\n // 非目标组件 → 跳过\n if (!componentSet.has(tagName)) return\n\n // 已有 testId → 跳过\n if (hasExistingTestId(node, opts.attributeName)) return\n\n const testIdValue = opts.generateId(tagName, count++)\n\n // ── bodyCell 内:动态 bind(引用 column.dataIndex + index) ──\n if (insideBodyCellDepth > 0) {\n const expr =\n `\\`\\${column?.dataIndex || column?.key || 'cell'}-\\${index}-${testIdValue}\\``\n node.props.push(createBindDirective(opts.attributeName, expr, node.loc))\n return\n }\n\n // ── teleport 组件:id + data-testid 双注入 ──\n if (idComponentSet.has(tagName)) {\n node.props.push(createStaticAttr(opts.idAttributeName, testIdValue, node.loc))\n node.props.push(createStaticAttr(opts.attributeName, testIdValue, node.loc))\n return\n }\n\n // ── 普通组件:静态 data-testid ──\n node.props.push(createStaticAttr(opts.attributeName, testIdValue, node.loc))\n }\n}\n\n// ──────────────────────── Public API ────────────────────────\n\n/**\n * 创建 Vue 编译器 nodeTransform 函数。\n * 直接传给 `@vitejs/plugin-vue` 的 `template.compilerOptions.nodeTransforms`。\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import vue from '@vitejs/plugin-vue'\n * import { createVueTestIdTransform } from 'vite-plugin-vue-testid'\n *\n * export default defineConfig({\n * plugins: [\n * vue({\n * template: {\n * compilerOptions: {\n * nodeTransforms: [createVueTestIdTransform()]\n * }\n * }\n * })\n * ]\n * })\n * ```\n *\n * @example 自定义配置\n * ```ts\n * createVueTestIdTransform({\n * components: ['a-input', 'a-select', 'el-input'],\n * generateId: (tag, n) => `myapp-${tag}-${n}`,\n * attributeName: 'data-cy',\n * })\n * ```\n */\nexport function createVueTestIdTransform(\n rawOpts?: VueTestIdTransformOptions,\n): (node: RootNode | TemplateChildNode) => void {\n const opts = resolveOptions(rawOpts)\n return createASTTransform(opts)\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-vue-testid",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Vite plugin to auto-inject data-testid into Vue component templates for E2E testing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",