import { existsSync } from 'node:fs' function normalizedFilename(context) { return (context.filename ?? context.getFilename?.() ?? '').replace(/\\/g, '/') } function importSource(node) { return typeof node.source?.value === 'string' ? node.source.value : null } function createProgramFilenameRule({ description, check }) { return { meta: { docs: { description } }, create(context) { const file = normalizedFilename(context) const message = check(file) if (!message) return {} return { Program(node) { context.report({ node, message }) } } } } } function createImportSourceRule({ description, applies = () => true, includeExports = false, includeDynamic = false, check }) { return { meta: { docs: { description } }, create(context) { const file = normalizedFilename(context) if (!applies(file)) return {} function reportIfRestricted(node) { const source = importSource(node) if (!source) return const message = check(source, file) if (!message) return context.report({ node, message }) } const visitors = { ImportDeclaration: reportIfRestricted } if (includeExports) { visitors.ExportAllDeclaration = reportIfRestricted visitors.ExportNamedDeclaration = reportIfRestricted } if (includeDynamic) visitors.ImportExpression = reportIfRestricted return visitors } } } function isUnknownTypeAnnotation(typeAnnotation) { return typeAnnotation?.type === 'TSUnknownKeyword' } const noInlineNamedTypes = { meta: { docs: { description: 'Disallow inline type literals that duplicate a named type' }, schema: [ { type: 'object', additionalProperties: { type: 'string' } } ] }, create(context) { const typesOption = context.options[0] if (!typesOption || typeof typesOption !== 'object') return {} const shapeToName = new Map() for (const [name, shape] of Object.entries(typesOption)) { shapeToName.set(shape, name) } return { TSTypeLiteral(node) { const props = node.members?.filter( (m) => m.type === 'TSPropertySignature' && m.key?.type === 'Identifier' ) if (!props || props.length < 2) return const shape = props .map((m) => { const typeNode = m.typeAnnotation?.typeAnnotation let typeName = 'unknown' if (typeNode) { switch (typeNode.type) { case 'TSNumberKeyword': typeName = 'number' break case 'TSStringKeyword': typeName = 'string' break case 'TSBooleanKeyword': typeName = 'boolean' break } } return `${m.key.name}:${typeName}` }) .sort() .join(',') const namedType = shapeToName.get(shape) if (namedType) { context.report({ node, message: `Use '${namedType}' instead of inline type literal. Import from '@open-pencil/core'.` }) } } } } } const noStructuredCloneSceneArrays = { meta: { docs: { description: 'Disallow structuredClone on fills/strokes/effects — use typed copy helpers from copy.ts' }, schema: [ { type: 'array', items: { type: 'string' }, description: 'Property names that should use typed copy helpers' } ] }, create(context) { const props = new Set( context.options[0] ?? [ 'fills', 'strokes', 'effects', 'styleRuns', 'fillGeometry', 'strokeGeometry' ] ) return { CallExpression(node) { if (node.callee?.type !== 'Identifier' || node.callee.name !== 'structuredClone') return if (node.arguments?.length !== 1) return const arg = node.arguments[0] if (arg.type === 'MemberExpression' && arg.property?.type === 'Identifier') { if (props.has(arg.property.name)) { context.report({ node, message: `Use the typed copy helper instead of structuredClone for '${arg.property.name}'. Import from '@open-pencil/core'.` }) } } } } } } const noVueStyleBlocks = { meta: { docs: { description: 'Disallow Vue component