test(arch): guard panel UI conventions
This commit is contained in:
parent
3f08b80b5e
commit
cde9cd4610
387
lint/plugin.js
387
lint/plugin.js
|
|
@ -1,4 +1,5 @@
|
||||||
import { existsSync } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
|
import { parse as parseVueSfc } from 'vue/compiler-sfc'
|
||||||
|
|
||||||
function normalizedFilename(context) {
|
function normalizedFilename(context) {
|
||||||
return (context.filename ?? context.getFilename?.() ?? '').replace(/\\/g, '/')
|
return (context.filename ?? context.getFilename?.() ?? '').replace(/\\/g, '/')
|
||||||
|
|
@ -172,6 +173,97 @@ const noStructuredCloneSceneArrays = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vueSfcDescriptor(source, filename) {
|
||||||
|
return parseVueSfc(source, { filename }).descriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
function vueTemplateAst(source, filename) {
|
||||||
|
return vueSfcDescriptor(source, filename).template?.ast ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVueSourceFile(file) {
|
||||||
|
return file.endsWith('.vue') && (file.includes('/src/') || file.includes('/packages/vue/src/'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceLineCount(source) {
|
||||||
|
const normalized = source.endsWith('\n') ? source.slice(0, -1) : source
|
||||||
|
return normalized.split('\n').length
|
||||||
|
}
|
||||||
|
|
||||||
|
const VUE_ELEMENT_NODE = 1
|
||||||
|
const VUE_SIMPLE_EXPRESSION_NODE = 4
|
||||||
|
const VUE_INTERPOLATION_NODE = 5
|
||||||
|
const VUE_ATTRIBUTE_NODE = 6
|
||||||
|
const VUE_DIRECTIVE_NODE = 7
|
||||||
|
|
||||||
|
function walkVueTemplateAst(node, visitor) {
|
||||||
|
visitor(node)
|
||||||
|
for (const prop of node.props ?? []) walkVueTemplateAst(prop, visitor)
|
||||||
|
for (const child of node.children ?? []) walkVueTemplateAst(child, visitor)
|
||||||
|
if (node.type === VUE_INTERPOLATION_NODE && node.content) {
|
||||||
|
walkVueTemplateAst(node.content, visitor)
|
||||||
|
}
|
||||||
|
if (node.type === VUE_DIRECTIVE_NODE) {
|
||||||
|
if (node.arg) walkVueTemplateAst(node.arg, visitor)
|
||||||
|
if (node.exp) walkVueTemplateAst(node.exp, visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkExpressionAst(node, visitor) {
|
||||||
|
if (!node || typeof node !== 'object') return
|
||||||
|
visitor(node)
|
||||||
|
for (const value of Object.values(node)) {
|
||||||
|
if (!value || value === node.loc) continue
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) walkExpressionAst(item, visitor)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
walkExpressionAst(value, visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUiHelperName(name) {
|
||||||
|
const prefix = 'use'
|
||||||
|
const suffix = 'UI'
|
||||||
|
if (!name.startsWith(prefix) || !name.endsWith(suffix)) return false
|
||||||
|
const firstDomainChar = name.at(prefix.length)
|
||||||
|
return firstDomainChar !== undefined && firstDomainChar === firstDomainChar.toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasExpressionCall(expression, predicate) {
|
||||||
|
if (expression?.type !== VUE_SIMPLE_EXPRESSION_NODE || !expression.ast) return false
|
||||||
|
let found = false
|
||||||
|
walkExpressionAst(expression.ast, (node) => {
|
||||||
|
if (found || node.type !== 'CallExpression') return
|
||||||
|
if (node.callee?.type === 'Identifier' && predicate(node.callee.name)) found = true
|
||||||
|
})
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasUiHelperCall(expression) {
|
||||||
|
return hasExpressionCall(expression, isUiHelperName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStaticVueAttribute(node, name) {
|
||||||
|
return node.type === VUE_ATTRIBUTE_NODE && node.name === name
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVueBindDirective(node, name) {
|
||||||
|
return node.type === VUE_DIRECTIVE_NODE && node.name === 'bind' && node.arg?.content === name
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBoundStringLiteral(node, name) {
|
||||||
|
if (!isVueBindDirective(node, name)) return false
|
||||||
|
const expression = node.exp
|
||||||
|
if (expression?.type !== VUE_SIMPLE_EXPRESSION_NODE) return false
|
||||||
|
const ast = expression.ast
|
||||||
|
if (!ast) return false
|
||||||
|
if (ast.type === 'StringLiteral' || (ast.type === 'Literal' && typeof ast.value === 'string')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return ast.type === 'TemplateLiteral' && ast.expressions?.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
const noVueStyleBlocks = {
|
const noVueStyleBlocks = {
|
||||||
meta: {
|
meta: {
|
||||||
docs: {
|
docs: {
|
||||||
|
|
@ -185,14 +277,13 @@ const noVueStyleBlocks = {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Program(node) {
|
Program(node) {
|
||||||
const source = context.sourceCode.getText()
|
const descriptor = vueSfcDescriptor(context.sourceCode.getText(), file)
|
||||||
if (/<style\b/i.test(source)) {
|
if (descriptor.styles.length === 0) return
|
||||||
context.report({
|
context.report({
|
||||||
node,
|
node,
|
||||||
message:
|
message:
|
||||||
'Vue components must not use <style> blocks. Use Tailwind utilities or global app.css tokens.'
|
'Vue components must not use <style> blocks. Use Tailwind utilities or global app.css tokens.'
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,13 +302,20 @@ const noNativeTitleAttributesInVue = {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Program(node) {
|
Program(node) {
|
||||||
const source = context.sourceCode.getText()
|
const template = vueTemplateAst(context.sourceCode.getText(), file)
|
||||||
if (/\s:?title=/.test(source)) {
|
if (!template) return
|
||||||
context.report({
|
let hasTitleAttribute = false
|
||||||
node,
|
walkVueTemplateAst(template, (templateNode) => {
|
||||||
message: 'Use the shared tooltip UI instead of native title attributes.'
|
if (hasTitleAttribute) return
|
||||||
})
|
if (isStaticVueAttribute(templateNode, 'title') || isVueBindDirective(templateNode, 'title')) {
|
||||||
}
|
hasTitleAttribute = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!hasTitleAttribute) return
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: 'Use the shared tooltip UI instead of native title attributes.'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -236,13 +334,110 @@ const noHardcodedTipLabelsInVue = {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Program(node) {
|
Program(node) {
|
||||||
const source = context.sourceCode.getText()
|
const template = vueTemplateAst(context.sourceCode.getText(), file)
|
||||||
if (/<Tip\b[^>]*\slabel=["']/.test(source)) {
|
if (!template) return
|
||||||
context.report({
|
let hasHardcodedTipLabel = false
|
||||||
node,
|
walkVueTemplateAst(template, (templateNode) => {
|
||||||
message: 'Use a localized binding for Tip labels, not a hardcoded string.'
|
if (hasHardcodedTipLabel) return
|
||||||
})
|
if (templateNode.type !== VUE_ELEMENT_NODE || templateNode.tag !== 'Tip') return
|
||||||
}
|
hasHardcodedTipLabel = templateNode.props?.some(
|
||||||
|
(prop) => isStaticVueAttribute(prop, 'label') || isBoundStringLiteral(prop, 'label')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if (!hasHardcodedTipLabel) return
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: 'Use a localized binding for Tip labels, not a hardcoded string.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const noRawSvgInAppVueTemplates = {
|
||||||
|
meta: {
|
||||||
|
docs: {
|
||||||
|
description: 'Disallow raw SVG in app Vue templates — use Iconify/unplugin icons'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const file = normalizedFilename(context)
|
||||||
|
if (!isVueSourceFile(file)) return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Program(node) {
|
||||||
|
const template = vueTemplateAst(context.sourceCode.getText(), file)
|
||||||
|
if (!template) return
|
||||||
|
let hasRawSvg = false
|
||||||
|
walkVueTemplateAst(template, (templateNode) => {
|
||||||
|
if (templateNode.type === VUE_ELEMENT_NODE && templateNode.tag === 'svg') {
|
||||||
|
hasRawSvg = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!hasRawSvg) return
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: 'Use Iconify/unplugin icon components instead of raw <svg> in app templates.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const noUiHelperCallsInVueTemplates = {
|
||||||
|
meta: {
|
||||||
|
docs: {
|
||||||
|
description: 'Disallow use*UI() helper calls inside Vue templates'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const file = normalizedFilename(context)
|
||||||
|
if (!file.endsWith('.vue') || !file.includes('/src/components/')) return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Program(node) {
|
||||||
|
const template = vueTemplateAst(context.sourceCode.getText(), file)
|
||||||
|
if (!template) return
|
||||||
|
let hasTemplateUiHelperCall = false
|
||||||
|
walkVueTemplateAst(template, (templateNode) => {
|
||||||
|
if (!hasTemplateUiHelperCall && hasUiHelperCall(templateNode)) {
|
||||||
|
hasTemplateUiHelperCall = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!hasTemplateUiHelperCall) return
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: 'Hoist use*UI() calls out of templates or hide them inside shared UI components.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROPERTY_SECTION_LINE_ALLOWLIST = new Set([
|
||||||
|
'/src/components/properties/LayoutSection/SizeControls.vue'
|
||||||
|
])
|
||||||
|
|
||||||
|
const noLargePropertySectionComponents = {
|
||||||
|
meta: {
|
||||||
|
docs: {
|
||||||
|
description: 'Disallow oversized property-section Vue components'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const file = normalizedFilename(context)
|
||||||
|
if (!file.endsWith('.vue') || !file.includes('/src/components/properties/')) return {}
|
||||||
|
if ([...PROPERTY_SECTION_LINE_ALLOWLIST].some((suffix) => file.endsWith(suffix))) return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Program(node) {
|
||||||
|
const lineCount = sourceLineCount(context.sourceCode.getText())
|
||||||
|
if (lineCount <= 250) return
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message:
|
||||||
|
'Split property-section components over 250 lines into focused controls or document an explicit allowlist.'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -298,13 +493,18 @@ const noDynamicDataTestIdInVue = {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Program(node) {
|
Program(node) {
|
||||||
const source = context.sourceCode.getText()
|
const template = vueTemplateAst(context.sourceCode.getText(), file)
|
||||||
if (/\B:data-test-id\s*=|\bv-bind:data-test-id\s*=/.test(source)) {
|
if (!template) return
|
||||||
context.report({
|
let hasDynamicDataTestId = false
|
||||||
node,
|
walkVueTemplateAst(template, (templateNode) => {
|
||||||
message: 'Use v-test-id for dynamic/configurable test ids instead of :data-test-id.'
|
if (hasDynamicDataTestId) return
|
||||||
})
|
if (isVueBindDirective(templateNode, 'data-test-id')) hasDynamicDataTestId = true
|
||||||
}
|
})
|
||||||
|
if (!hasDynamicDataTestId) return
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: 'Use v-test-id for dynamic/configurable test ids instead of :data-test-id.'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -322,13 +522,23 @@ const noTestIdHelperBindInVue = {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Program(node) {
|
Program(node) {
|
||||||
const source = context.sourceCode.getText()
|
const template = vueTemplateAst(context.sourceCode.getText(), file)
|
||||||
if (/\bv-bind\s*=\s*"testId(?:Attr)?\(/.test(source)) {
|
if (!template) return
|
||||||
context.report({
|
let hasTestIdHelperBind = false
|
||||||
node,
|
walkVueTemplateAst(template, (templateNode) => {
|
||||||
message: 'Use v-test-id instead of v-bind="testId(...)" in Vue templates.'
|
if (hasTestIdHelperBind) return
|
||||||
})
|
if (templateNode.type !== VUE_DIRECTIVE_NODE || templateNode.name !== 'bind') return
|
||||||
}
|
if (templateNode.arg) return
|
||||||
|
hasTestIdHelperBind = hasExpressionCall(
|
||||||
|
templateNode.exp,
|
||||||
|
(name) => name === 'testId' || name === 'testIdAttr'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if (!hasTestIdHelperBind) return
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: 'Use v-test-id instead of v-bind="testId(...)" in Vue templates.'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -346,26 +556,35 @@ const noInvalidTestIdAttributes = {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Program(node) {
|
Program(node) {
|
||||||
const source = context.sourceCode.getText()
|
const template = vueTemplateAst(context.sourceCode.getText(), file)
|
||||||
const invalidAttr = source.match(/\bdata-testid\s*=/)
|
if (!template) return
|
||||||
if (invalidAttr) {
|
let invalidId = null
|
||||||
|
let hasInvalidSpelling = false
|
||||||
|
walkVueTemplateAst(template, (templateNode) => {
|
||||||
|
if (hasInvalidSpelling || invalidId !== null) return
|
||||||
|
if (
|
||||||
|
isStaticVueAttribute(templateNode, 'data-testid') ||
|
||||||
|
isVueBindDirective(templateNode, 'data-testid')
|
||||||
|
) {
|
||||||
|
hasInvalidSpelling = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isStaticVueAttribute(templateNode, 'data-test-id')) return
|
||||||
|
const id = templateNode.value?.content ?? ''
|
||||||
|
if (!TEST_ID_FORMAT.test(id)) invalidId = id
|
||||||
|
})
|
||||||
|
if (hasInvalidSpelling) {
|
||||||
context.report({
|
context.report({
|
||||||
node,
|
node,
|
||||||
message: 'Use data-test-id instead of data-testid.'
|
message: 'Use data-test-id instead of data-testid.'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (invalidId === null) return
|
||||||
const attrPattern = /\bdata-test-id\s*=\s*"([^"]+)"/g
|
context.report({
|
||||||
for (const match of source.matchAll(attrPattern)) {
|
node,
|
||||||
const id = match[1]
|
message: `Static data-test-id values must be kebab-case. Invalid id: "${invalidId}".`
|
||||||
if (TEST_ID_FORMAT.test(id)) continue
|
})
|
||||||
context.report({
|
|
||||||
node,
|
|
||||||
message: `Static data-test-id values must be kebab-case. Invalid id: "${id}".`
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -406,6 +625,22 @@ const noRawTestIdSelectorsInTests = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isGeneratedTestIdLiteral(value) {
|
||||||
|
if (typeof value !== 'string') return false
|
||||||
|
const toolbarValue = value.startsWith('mobile-toolbar-')
|
||||||
|
? value.slice('mobile-'.length)
|
||||||
|
: value
|
||||||
|
return (
|
||||||
|
toolbarValue.startsWith('toolbar-tool-') ||
|
||||||
|
toolbarValue.startsWith('toolbar-flyout-') ||
|
||||||
|
toolbarValue.startsWith('toolbar-flyout-item-') ||
|
||||||
|
value === 'variables-add-float' ||
|
||||||
|
value === 'variables-add-string' ||
|
||||||
|
value === 'variables-add-boolean' ||
|
||||||
|
value.startsWith('acp-permission-option-')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const noGeneratedTestIdLiterals = {
|
const noGeneratedTestIdLiterals = {
|
||||||
meta: {
|
meta: {
|
||||||
docs: {
|
docs: {
|
||||||
|
|
@ -418,18 +653,31 @@ const noGeneratedTestIdLiterals = {
|
||||||
if (file.endsWith('/tests/helpers/test-ids.ts')) return {}
|
if (file.endsWith('/tests/helpers/test-ids.ts')) return {}
|
||||||
if (!file.includes('/src/') && !file.includes('/tests/')) return {}
|
if (!file.includes('/src/') && !file.includes('/tests/')) return {}
|
||||||
|
|
||||||
|
function report(node) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message:
|
||||||
|
'Use shared generated test-id helpers instead of hand-written toolbar/variable/permission id literals.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Program(node) {
|
Program(node) {
|
||||||
const source = context.sourceCode.getText()
|
if (!file.endsWith('.vue')) return
|
||||||
const match = source.match(
|
const template = vueTemplateAst(context.sourceCode.getText(), file)
|
||||||
/["'`](?:mobile-)?toolbar-(?:tool|flyout|flyout-item)-|["'`]variables-add-(?:float|string|boolean)["'`]|["'`]acp-permission-option-/
|
if (!template) return
|
||||||
)
|
let hasGeneratedTemplateId = false
|
||||||
if (!match) return
|
walkVueTemplateAst(template, (templateNode) => {
|
||||||
context.report({
|
if (hasGeneratedTemplateId || !isStaticVueAttribute(templateNode, 'data-test-id')) return
|
||||||
node,
|
hasGeneratedTemplateId = isGeneratedTestIdLiteral(templateNode.value?.content)
|
||||||
message:
|
|
||||||
'Use shared generated test-id helpers instead of hand-written toolbar/variable/permission id literals.'
|
|
||||||
})
|
})
|
||||||
|
if (hasGeneratedTemplateId) report(node)
|
||||||
|
},
|
||||||
|
Literal(node) {
|
||||||
|
if (isGeneratedTestIdLiteral(node.value)) report(node)
|
||||||
|
},
|
||||||
|
TemplateElement(node) {
|
||||||
|
if (isGeneratedTestIdLiteral(node.value?.raw)) report(node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -631,14 +879,16 @@ const noHandRolledColor = {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
TemplateLiteral(node) {
|
TemplateLiteral(node) {
|
||||||
const raw = context.sourceCode.getText(node)
|
const hasHandRolledRgb = node.quasis?.some((quasi) => {
|
||||||
if (/rgba?\s*\(/.test(raw)) {
|
const raw = quasi.value?.raw
|
||||||
context.report({
|
return typeof raw === 'string' && (raw.includes('rgb(') || raw.includes('rgba('))
|
||||||
node,
|
})
|
||||||
message:
|
if (!hasHandRolledRgb) return
|
||||||
'Use colorToCSS() or colorToHex() from color.ts instead of hand-rolled rgba()/rgb() strings.'
|
context.report({
|
||||||
})
|
node,
|
||||||
}
|
message:
|
||||||
|
'Use colorToCSS() or colorToHex() from color.ts instead of hand-rolled rgba()/rgb() strings.'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1837,6 +2087,9 @@ const plugin = {
|
||||||
'no-vue-style-blocks': noVueStyleBlocks,
|
'no-vue-style-blocks': noVueStyleBlocks,
|
||||||
'no-native-title-attributes-in-vue': noNativeTitleAttributesInVue,
|
'no-native-title-attributes-in-vue': noNativeTitleAttributesInVue,
|
||||||
'no-hardcoded-tip-labels-in-vue': noHardcodedTipLabelsInVue,
|
'no-hardcoded-tip-labels-in-vue': noHardcodedTipLabelsInVue,
|
||||||
|
'no-raw-svg-in-app-vue-templates': noRawSvgInAppVueTemplates,
|
||||||
|
'no-ui-helper-calls-in-vue-templates': noUiHelperCallsInVueTemplates,
|
||||||
|
'no-large-property-section-components': noLargePropertySectionComponents,
|
||||||
'no-raw-test-id-string-props': noRawTestIdStringProps,
|
'no-raw-test-id-string-props': noRawTestIdStringProps,
|
||||||
'no-dynamic-data-test-id-in-vue': noDynamicDataTestIdInVue,
|
'no-dynamic-data-test-id-in-vue': noDynamicDataTestIdInVue,
|
||||||
'no-test-id-helper-bind-in-vue': noTestIdHelperBindInVue,
|
'no-test-id-helper-bind-in-vue': noTestIdHelperBindInVue,
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,9 @@
|
||||||
"open-pencil/no-vue-style-blocks": "error",
|
"open-pencil/no-vue-style-blocks": "error",
|
||||||
"open-pencil/no-native-title-attributes-in-vue": "error",
|
"open-pencil/no-native-title-attributes-in-vue": "error",
|
||||||
"open-pencil/no-hardcoded-tip-labels-in-vue": "error",
|
"open-pencil/no-hardcoded-tip-labels-in-vue": "error",
|
||||||
|
"open-pencil/no-raw-svg-in-app-vue-templates": "error",
|
||||||
|
"open-pencil/no-ui-helper-calls-in-vue-templates": "error",
|
||||||
|
"open-pencil/no-large-property-section-components": "error",
|
||||||
"open-pencil/no-raw-test-id-string-props": "error",
|
"open-pencil/no-raw-test-id-string-props": "error",
|
||||||
"open-pencil/no-dynamic-data-test-id-in-vue": "error",
|
"open-pencil/no-dynamic-data-test-id-in-vue": "error",
|
||||||
"open-pencil/no-test-id-helper-bind-in-vue": "error",
|
"open-pencil/no-test-id-helper-bind-in-vue": "error",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue