fix: complete acronym casing follow-ups

- Cover UI identifiers in the acronym guardrail
- Preserve FIG thumbnail and metadata values through archive parsing
- Use Vue-compatible acronym prop attributes
This commit is contained in:
Danila Poyarkov 2026-08-10 16:38:17 +03:00
parent 7f91594d0c
commit 55b368dc4a
15 changed files with 76 additions and 66 deletions

View file

@ -2110,7 +2110,7 @@ const noMixedCaseAcronymIdentifiers = {
} }
}, },
create(context) { create(context) {
const canonicalAcronym = /(?:Acp|Ai|Api|Cli|Cors|Css|Html|Ime|Json|Jsx|Mcp|Pdf|Png|Rgb|Rpc|Rtl|Svg|Url|Uri|Xml)/g const canonicalAcronym = /(?:Acp|Ai|Api|Cli|Cors|Css|Html|Ime|Json|Jsx|Mcp|Pdf|Png|Rgb|Rpc|Rtl|Svg|Ui|Url|Uri|Xml)/g
const ignoredImports = new Set([ const ignoredImports = new Set([
'@agentclientprotocol/sdk', '@agentclientprotocol/sdk',
'@tauri-apps/plugin-clipboard-manager', '@tauri-apps/plugin-clipboard-manager',

View file

@ -25,6 +25,8 @@ export interface FigParseResult {
figKiwiVersion: number figKiwiVersion: number
/** Deflated Kiwi schema bytes from the original file, retained for round-trip fidelity. */ /** Deflated Kiwi schema bytes from the original file, retained for round-trip fidelity. */
figSchemaDeflated: Uint8Array figSchemaDeflated: Uint8Array
thumbnailPNG: Uint8Array | null
metaJSON: string | null
} }
function isLikelyAsset(name: string): boolean { function isLikelyAsset(name: string): boolean {
@ -46,12 +48,7 @@ function findCanvasData(entries: Partial<Record<string, Uint8Array>>): Uint8Arra
/** Parse a complete zipped `.fig` file into its Figma protocol payload and binary resources. */ /** Parse a complete zipped `.fig` file into its Figma protocol payload and binary resources. */
export function parseFigBuffer(buffer: ArrayBuffer): FigParseResult { export function parseFigBuffer(buffer: ArrayBuffer): FigParseResult {
const archive = unzipSync(new Uint8Array(buffer), { const archive = unzipSync(new Uint8Array(buffer))
filter: (file) =>
file.name === 'canvas.fig' ||
file.name === 'canvas' ||
(file.name.startsWith('images/') && file.name !== 'images/')
})
const canvasData = findCanvasData(archive) const canvasData = findCanvasData(archive)
if (!canvasData) { if (!canvasData) {
throw new Error( throw new Error(
@ -60,11 +57,17 @@ export function parseFigBuffer(buffer: ArrayBuffer): FigParseResult {
} }
const decoded = decodeFigKiwiCanvas(canvasData) const decoded = decodeFigKiwiCanvas(canvasData)
const metaBytes = archive['meta.json']
const images = Object.entries(archive) const images = Object.entries(archive)
.filter(([name]) => name.startsWith('images/') && name !== 'images/') .filter(([name]) => name.startsWith('images/') && name !== 'images/')
.map(([name, data]) => [name.slice('images/'.length), data] as [string, Uint8Array]) .map(([name, data]) => [name.slice('images/'.length), data] as [string, Uint8Array])
return { ...decoded, images } return {
...decoded,
images,
thumbnailPNG: archive['thumbnail.png'] ?? null,
metaJSON: Object.hasOwn(archive, 'meta.json') ? new TextDecoder().decode(metaBytes) : null
}
} }
/** Assemble a complete zipped `.fig` archive from an encoded Kiwi message and resources. */ /** Assemble a complete zipped `.fig` archive from an encoded Kiwi message and resources. */

View file

@ -41,6 +41,8 @@ describe('@open-pencil/fig package API', () => {
}) })
it('parses complete .fig archives and image resources', () => { it('parses complete .fig archives and image resources', () => {
const thumbnailPNG = new Uint8Array([1])
const metaJSON = '{}'
const bytes = writeFigArchive({ const bytes = writeFigArchive({
schemaDeflated: deflateSync(getSchemaBytes()), schemaDeflated: deflateSync(getSchemaBytes()),
kiwiData: encodeMessage( kiwiData: encodeMessage(
@ -53,8 +55,8 @@ describe('@open-pencil/fig package API', () => {
} }
]) ])
), ),
thumbnailPNG: new Uint8Array([1]), thumbnailPNG,
metaJSON: '{}', metaJSON,
images: [{ name: 'images/hash', data: new Uint8Array([9, 8, 7]) }] images: [{ name: 'images/hash', data: new Uint8Array([9, 8, 7]) }]
}) })
const parsed = parseFigBuffer(bytes.buffer as ArrayBuffer) const parsed = parseFigBuffer(bytes.buffer as ArrayBuffer)
@ -62,6 +64,8 @@ describe('@open-pencil/fig package API', () => {
expect(parsed.nodeChanges).toHaveLength(1) expect(parsed.nodeChanges).toHaveLength(1)
expect(parsed.nodeChanges[0]?.type).toBe('DOCUMENT') expect(parsed.nodeChanges[0]?.type).toBe('DOCUMENT')
expect(parsed.images).toEqual([['hash', new Uint8Array([9, 8, 7])]]) expect(parsed.images).toEqual([['hash', new Uint8Array([9, 8, 7])]])
expect(parsed.thumbnailPNG).toEqual(thumbnailPNG)
expect(parsed.metaJSON).toBe(metaJSON)
}) })
it('rejects invalid fig-kiwi containers', () => { it('rejects invalid fig-kiwi containers', () => {

View file

@ -11,7 +11,7 @@ import { evictLocalFigCache } from '@/app/storage/cache-eviction'
import { getLocalCanvasStore } from '@/app/storage/local-store' import { getLocalCanvasStore } from '@/app/storage/local-store'
import { getOutbox } from '@/app/storage/sync/outbox' import { getOutbox } from '@/app/storage/sync/outbox'
import { setUploadProgress } from '@/app/storage/sync/progress' import { setUploadProgress } from '@/app/storage/sync/progress'
import { setPendingSyncCount, setSyncUi } from '@/app/storage/sync/status' import { setPendingSyncCount, setSyncUI } from '@/app/storage/sync/status'
import type { OutboxJob } from '@/app/storage/sync/types' import type { OutboxJob } from '@/app/storage/sync/types'
const MAX_ATTEMPTS = 8 const MAX_ATTEMPTS = 8
@ -141,17 +141,17 @@ async function pumpOnce(): Promise<void> {
setPendingSyncCount(jobs.length) setPendingSyncCount(jobs.length)
if (jobs.length === 0) { if (jobs.length === 0) {
if (isOnline()) setSyncUi('idle') if (isOnline()) setSyncUI('idle')
return return
} }
if (!isOnline()) { if (!isOnline()) {
setSyncUi('offline') setSyncUI('offline')
scheduleWake(5000) scheduleWake(5000)
return return
} }
setSyncUi('syncing') setSyncUI('syncing')
const now = Date.now() const now = Date.now()
// Single-flight globally for simplicity (large figs) // Single-flight globally for simplicity (large figs)
const job = jobs.find((j) => j.nextAttemptAt <= now) const job = jobs.find((j) => j.nextAttemptAt <= now)
@ -166,7 +166,7 @@ async function pumpOnce(): Promise<void> {
await outbox.remove(job.id) await outbox.remove(job.id)
const remaining = await outbox.list() const remaining = await outbox.list()
setPendingSyncCount(remaining.length) setPendingSyncCount(remaining.length)
if (remaining.length === 0) setSyncUi('idle') if (remaining.length === 0) setSyncUI('idle')
else scheduleWake(50) else scheduleWake(50)
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error) const message = error instanceof Error ? error.message : String(error)
@ -175,7 +175,7 @@ async function pumpOnce(): Promise<void> {
...job, ...job,
nextAttemptAt: Number.MAX_SAFE_INTEGER nextAttemptAt: Number.MAX_SAFE_INTEGER
}) })
setSyncUi('error', message) setSyncUI('error', message)
return return
} }
@ -191,7 +191,7 @@ async function pumpOnce(): Promise<void> {
syncStatus: 'error', syncStatus: 'error',
lastSyncError: message lastSyncError: message
}) })
setSyncUi('error', message.slice(0, 120)) setSyncUI('error', message.slice(0, 120))
} else { } else {
// Keep a record without touching syncStatus so the stale remote // Keep a record without touching syncStatus so the stale remote
// thumbnail is at least diagnosable. // thumbnail is at least diagnosable.
@ -202,7 +202,7 @@ async function pumpOnce(): Promise<void> {
const remaining = await outbox.list() const remaining = await outbox.list()
setPendingSyncCount(remaining.length) setPendingSyncCount(remaining.length)
if (remaining.length > 0) scheduleWake(1000) if (remaining.length > 0) scheduleWake(1000)
else setSyncUi('idle') else setSyncUI('idle')
} else { } else {
// Never discard a document mutation. Keep it durable until the user // Never discard a document mutation. Keep it durable until the user
// repairs credentials/permissions and explicitly wakes synchronization. // repairs credentials/permissions and explicitly wakes synchronization.
@ -247,11 +247,11 @@ function ensureOnlineListeners() {
if (onlineBound || !IS_BROWSER) return if (onlineBound || !IS_BROWSER) return
onlineBound = true onlineBound = true
window.addEventListener('online', () => { window.addEventListener('online', () => {
setSyncUi('syncing') setSyncUI('syncing')
void kickSyncEngine() void kickSyncEngine()
}) })
window.addEventListener('offline', () => { window.addEventListener('offline', () => {
setSyncUi('offline') setSyncUI('offline')
}) })
} }
@ -307,7 +307,7 @@ export async function resumeStorageSync(): Promise<void> {
const jobs = await outbox.list() const jobs = await outbox.list()
const now = Date.now() const now = Date.now()
await Promise.all(jobs.map((job) => outbox.update({ ...job, nextAttemptAt: now }))) await Promise.all(jobs.map((job) => outbox.update({ ...job, nextAttemptAt: now })))
if (jobs.length > 0) setSyncUi('syncing') if (jobs.length > 0) setSyncUI('syncing')
void kickSyncEngine() void kickSyncEngine()
} }
@ -316,5 +316,5 @@ export async function clearStorageLocalMirror(): Promise<void> {
await getLocalCanvasStore().clearAll() await getLocalCanvasStore().clearAll()
await getOutbox().clear() await getOutbox().clear()
setPendingSyncCount(0) setPendingSyncCount(0)
setSyncUi('idle') setSyncUI('idle')
} }

View file

@ -17,15 +17,15 @@ export { setUploadProgress, uploadProgressByCanvas } from './progress'
export { export {
pendingSyncCount, pendingSyncCount,
setPendingSyncCount, setPendingSyncCount,
setSyncUi, setSyncUI,
syncStatusLabel, syncStatusLabel,
syncUiDetail, syncUIDetail,
syncUiState syncUIState
} from './status' } from './status'
export { export {
makeJobId, makeJobId,
supersedePutCanvasJobs, supersedePutCanvasJobs,
type OutboxJob, type OutboxJob,
type OutboxJobType, type OutboxJobType,
type SyncUiState type SyncUIState
} from './types' } from './types'

View file

@ -1,28 +1,28 @@
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import type { SyncUiState } from '@/app/storage/sync/types' import type { SyncUIState } from '@/app/storage/sync/types'
/** Global subtle sync status for UI chips. */ /** Global subtle sync status for UI chips. */
export const syncUiState = ref<SyncUiState>('idle') export const syncUIState = ref<SyncUIState>('idle')
export const syncUiDetail = ref<string | null>(null) export const syncUIDetail = ref<string | null>(null)
export const pendingSyncCount = ref(0) export const pendingSyncCount = ref(0)
export const syncStatusLabel = computed(() => { export const syncStatusLabel = computed(() => {
switch (syncUiState.value) { switch (syncUIState.value) {
case 'syncing': case 'syncing':
return syncUiDetail.value ?? 'Syncing…' return syncUIDetail.value ?? 'Syncing…'
case 'offline': case 'offline':
return 'Offline · will sync' return 'Offline · will sync'
case 'error': case 'error':
return syncUiDetail.value ?? 'Sync failed' return syncUIDetail.value ?? 'Sync failed'
default: default:
return null return null
} }
}) })
export function setSyncUi(state: SyncUiState, detail: string | null = null) { export function setSyncUI(state: SyncUIState, detail: string | null = null) {
syncUiState.value = state syncUIState.value = state
syncUiDetail.value = detail syncUIDetail.value = detail
} }
export function setPendingSyncCount(count: number) { export function setPendingSyncCount(count: number) {

View file

@ -11,7 +11,7 @@ export type OutboxJob = {
nextAttemptAt: number nextAttemptAt: number
} }
export type SyncUiState = 'idle' | 'syncing' | 'offline' | 'error' export type SyncUIState = 'idle' | 'syncing' | 'offline' | 'error'
/** Pure helper: drop older putCanvas jobs for same canvas when a newer revision is enqueued. */ /** Pure helper: drop older putCanvas jobs for same canvas when a newer revision is enqueued. */
export function supersedePutCanvasJobs( export function supersedePutCanvasJobs(

View file

@ -54,7 +54,7 @@ const toolShortcuts: Record<Tool, string> = {
} }
const flyoutMenuCls = useMenuUI({ content: 'min-w-32' }) const flyoutMenuCls = useMenuUI({ content: 'min-w-32' })
const toolbarUi = { flyoutContent: flyoutMenuCls.content } const toolbarUI = { flyoutContent: flyoutMenuCls.content }
const { editActions, arrangeActions } = useToolbarActions({ store, getCommand, menu }) const { editActions, arrangeActions } = useToolbarActions({ store, getCommand, menu })
const { mobileCategory, slideDirection, hasPrev, hasNext, goPrev, goNext } = useToolbarState() const { mobileCategory, slideDirection, hasPrev, hasNext, goPrev, goNext } = useToolbarState()
@ -75,7 +75,7 @@ function onActionTap(item: ToolbarActionItem) {
:tool-icons="toolIcons" :tool-icons="toolIcons"
:tool-labels="toolLabels" :tool-labels="toolLabels"
:tool-shortcuts="toolShortcuts" :tool-shortcuts="toolShortcuts"
:ui="toolbarUi" :ui="toolbarUI"
@set-tool="actions.setTool" @set-tool="actions.setTool"
/> />
@ -87,7 +87,7 @@ function onActionTap(item: ToolbarActionItem) {
:tool-icons="toolIcons" :tool-icons="toolIcons"
:tool-labels="toolLabels" :tool-labels="toolLabels"
:tool-shortcuts="toolShortcuts" :tool-shortcuts="toolShortcuts"
:ui="toolbarUi" :ui="toolbarUI"
:mobile-category="mobileCategory" :mobile-category="mobileCategory"
:slide-direction="slideDirection" :slide-direction="slideDirection"
:has-prev="hasPrev" :has-prev="hasPrev"

View file

@ -376,8 +376,8 @@ void refreshKeyStatus()
:saved="hasExistingKey" :saved="hasExistingKey"
kind="api" kind="api"
:placeholder="hasExistingKey ? dialogs.keySavedReplace : providerDef.keyPlaceholder" :placeholder="hasExistingKey ? dialogs.keySavedReplace : providerDef.keyPlaceholder"
:key-url="providerDef.keyURL" :key-u-r-l="providerDef.keyURL"
:key-url-label="dialogs.getAPIKeyGeneric" :key-u-r-l-label="dialogs.getAPIKeyGeneric"
@clear="clearKey" @clear="clearKey"
/> />

View file

@ -44,8 +44,8 @@ async function clearUnsplashKey(): Promise<void> {
:saved="hasExistingPexelsKey" :saved="hasExistingPexelsKey"
kind="pexels" kind="pexels"
:placeholder="hasExistingPexelsKey ? dialogs.keySavedReplace : dialogs.stockPhotoToolOptional" :placeholder="hasExistingPexelsKey ? dialogs.keySavedReplace : dialogs.stockPhotoToolOptional"
key-url="https://www.pexels.com/api/" key-u-r-l="https://www.pexels.com/api/"
:key-url-label="dialogs.getPexelsAPIKey" :key-u-r-l-label="dialogs.getPexelsAPIKey"
@clear="clearPexelsKey" @clear="clearPexelsKey"
@change="savePexelsKey" @change="savePexelsKey"
/> />
@ -58,8 +58,8 @@ async function clearUnsplashKey(): Promise<void> {
:placeholder=" :placeholder="
hasExistingUnsplashKey ? dialogs.keySavedReplace : dialogs.pexelsAlternativeOptional hasExistingUnsplashKey ? dialogs.keySavedReplace : dialogs.pexelsAlternativeOptional
" "
key-url="https://unsplash.com/oauth/applications" key-u-r-l="https://unsplash.com/oauth/applications"
:key-url-label="dialogs.getUnsplashAccessKey" :key-u-r-l-label="dialogs.getUnsplashAccessKey"
@clear="clearUnsplashKey" @clear="clearUnsplashKey"
@change="saveUnsplashKey" @change="saveUnsplashKey"
/> />

View file

@ -72,8 +72,8 @@ onMounted(() => void refreshStatus())
:saved="keyStatus === 'configured'" :saved="keyStatus === 'configured'"
kind="api" kind="api"
:placeholder="keyStatus === 'configured' ? dialogs.keySavedReplace : provider.keyPlaceholder" :placeholder="keyStatus === 'configured' ? dialogs.keySavedReplace : provider.keyPlaceholder"
:key-url="provider.keyURL" :key-u-r-l="provider.keyURL"
:key-url-label="dialogs.getAPIKeyGeneric" :key-u-r-l-label="dialogs.getAPIKeyGeneric"
@change="saveCredential" @change="saveCredential"
@clear="clearCredential" @clear="clearCredential"
/> />

View file

@ -12,13 +12,13 @@ heavy('parse heavy .fig files', () => {
let material3: SceneGraph let material3: SceneGraph
let nuxtui: SceneGraph let nuxtui: SceneGraph
let material3Nodes: SceneNode[] let material3Nodes: SceneNode[]
let nuxtUiNodes: SceneNode[] let nuxtUINodes: SceneNode[]
beforeAll(async () => { beforeAll(async () => {
material3 = await parseFixture('material3.fig', { populate: 'none' }) material3 = await parseFixture('material3.fig', { populate: 'none' })
nuxtui = await parseFixture('nuxtui.fig', { populate: 'none' }) nuxtui = await parseFixture('nuxtui.fig', { populate: 'none' })
material3Nodes = collectAllNodes(material3) material3Nodes = collectAllNodes(material3)
nuxtUiNodes = collectAllNodes(nuxtui) nuxtUINodes = collectAllNodes(nuxtui)
}) })
test('material3.fig parses with pages and nodes', () => { test('material3.fig parses with pages and nodes', () => {
@ -30,7 +30,7 @@ heavy('parse heavy .fig files', () => {
test('nuxtui.fig parses with pages and nodes', () => { test('nuxtui.fig parses with pages and nodes', () => {
expect(nuxtui).toBeInstanceOf(SceneGraph) expect(nuxtui).toBeInstanceOf(SceneGraph)
expect(nuxtui.getPages().length).toBeGreaterThan(0) expect(nuxtui.getPages().length).toBeGreaterThan(0)
expect(nuxtUiNodes.length).toBeGreaterThan(0) expect(nuxtUINodes.length).toBeGreaterThan(0)
}) })
test('material3: contains COMPONENT nodes', () => { test('material3: contains COMPONENT nodes', () => {
@ -43,7 +43,7 @@ heavy('parse heavy .fig files', () => {
}) })
test('nuxtui: no unmapped node types', () => { test('nuxtui: no unmapped node types', () => {
const invalid = nuxtUiNodes.filter((n) => !VALID_NODE_TYPES.has(n.type)) const invalid = nuxtUINodes.filter((n) => !VALID_NODE_TYPES.has(n.type))
expect(invalid.map((n) => `${n.name}: ${n.type}`)).toEqual([]) expect(invalid.map((n) => `${n.name}: ${n.type}`)).toEqual([])
}) })
@ -66,7 +66,7 @@ heavy('parse heavy .fig files', () => {
}) })
test('nuxtui: fills have valid colors', () => { test('nuxtui: fills have valid colors', () => {
for (const n of nuxtUiNodes) { for (const n of nuxtUINodes) {
for (const fill of n.fills) { for (const fill of n.fills) {
if (fill.type === 'SOLID') { if (fill.type === 'SOLID') {
expect(fill.color.r).toBeGreaterThanOrEqual(0) expect(fill.color.r).toBeGreaterThanOrEqual(0)

View file

@ -112,7 +112,7 @@ export function dynamicClassDiagnostics(sourceRel: string, content: string) {
return diagnostics return diagnostics
} }
function containsUiHookCall(node: unknown, visited = new Set<ExpressionNode>()): boolean { function containsUIHookCall(node: unknown, visited = new Set<ExpressionNode>()): boolean {
if (!isExpressionNode(node) || visited.has(node)) return false if (!isExpressionNode(node) || visited.has(node)) return false
visited.add(node) visited.add(node)
if ( if (
@ -127,8 +127,8 @@ function containsUiHookCall(node: unknown, visited = new Set<ExpressionNode>()):
return Object.entries(node).some(([key, value]) => { return Object.entries(node).some(([key, value]) => {
if (key === 'loc' || key === 'start' || key === 'end') return false if (key === 'loc' || key === 'start' || key === 'end') return false
return Array.isArray(value) return Array.isArray(value)
? value.some((child) => containsUiHookCall(child, visited)) ? value.some((child) => containsUIHookCall(child, visited))
: containsUiHookCall(value, visited) : containsUIHookCall(value, visited)
}) })
} }
@ -145,7 +145,7 @@ export function vueTemplateGuardrailDiagnostics(sourceRel: string, content: stri
column: node.loc?.start?.column column: node.loc?.start?.column
}) })
} }
if (node.type === VUE_DIRECTIVE_NODE && containsUiHookCall(node.exp?.ast)) { if (node.type === VUE_DIRECTIVE_NODE && containsUIHookCall(node.exp?.ast)) {
diagnostics.push({ diagnostics.push({
message: 'Resolve use*UI() hooks once in script setup, not inside template expressions.', message: 'Resolve use*UI() hooks once in script setup, not inside template expressions.',
line: node.loc?.start?.line, line: node.loc?.start?.line,
@ -156,7 +156,7 @@ export function vueTemplateGuardrailDiagnostics(sourceRel: string, content: stri
return diagnostics return diagnostics
} }
export const noVueTemplateUiHooksOrSVG = createTextRule( export const noVueTemplateUIHooksOrSVG = createTextRule(
'open-pencil/no-vue-template-ui-hooks-or-svg', 'open-pencil/no-vue-template-ui-hooks-or-svg',
vueTemplateGuardrailDiagnostics vueTemplateGuardrailDiagnostics
) )

View file

@ -5,7 +5,7 @@ import { parse as parseVueSfc } from 'vue/compiler-sfc'
import { noCrossPackageReexportShims } from './cross-package-reexport-shims.ts' import { noCrossPackageReexportShims } from './cross-package-reexport-shims.ts'
import { import {
noDynamicTailwindStateClasses, noDynamicTailwindStateClasses,
noVueTemplateUiHooksOrSVG noVueTemplateUIHooksOrSVG
} from './dynamic-tailwind-classes.ts' } from './dynamic-tailwind-classes.ts'
import { import {
collectFolders, collectFolders,
@ -305,7 +305,7 @@ const noComponentsImportViews = createImportRule(
} }
) )
const noNonUiImportsInSharedUi = createImportRule( const noNonUIImportsInSharedUI = createImportRule(
'open-pencil/no-non-ui-imports-in-shared-ui', 'open-pencil/no-non-ui-imports-in-shared-ui',
(sourceRel, _specifier, resolved) => { (sourceRel, _specifier, resolved) => {
if (!sourceRel.startsWith('src/components/ui/')) return null if (!sourceRel.startsWith('src/components/ui/')) return null
@ -326,7 +326,7 @@ const noViewsImportedOutsideEntry = createImportRule(
} }
) )
const noAppImportsInSharedUi = createImportRule( const noAppImportsInSharedUI = createImportRule(
'open-pencil/no-app-imports-in-shared-ui', 'open-pencil/no-app-imports-in-shared-ui',
(sourceRel, _specifier, resolved) => { (sourceRel, _specifier, resolved) => {
if (!sourceRel.startsWith('src/components/ui/')) return null if (!sourceRel.startsWith('src/components/ui/')) return null
@ -476,7 +476,7 @@ const noShortcutTextInLabels = createTextRule(
} }
) )
const noUiImportsInCore = createImportRule( const noUIImportsInCore = createImportRule(
'open-pencil/no-ui-imports-in-core', 'open-pencil/no-ui-imports-in-core',
(sourceRel, specifier) => { (sourceRel, specifier) => {
if (!sourceRel.startsWith('packages/core/src/')) return null if (!sourceRel.startsWith('packages/core/src/')) return null
@ -514,15 +514,15 @@ export const openPencilArchitecturePlugin = {
noAppImportsComponentsOrViews, noAppImportsComponentsOrViews,
noComponentsImportViews, noComponentsImportViews,
noViewsImportedOutsideEntry, noViewsImportedOutsideEntry,
noNonUiImportsInSharedUi, noNonUIImportsInSharedUI,
noAppImportsInSharedUi, noAppImportsInSharedUI,
noPropertyPanelInternalsOutsidePanel, noPropertyPanelInternalsOutsidePanel,
noProductionTestIdsInSharedLayers, noProductionTestIdsInSharedLayers,
noDynamicTailwindStateClasses, noDynamicTailwindStateClasses,
noVueTemplateUiHooksOrSVG, noVueTemplateUIHooksOrSVG,
noNativeTitleAttributesInVue, noNativeTitleAttributesInVue,
noShortcutTextInLabels, noShortcutTextInLabels,
noHardcodedMacOSShortcutGlyphs, noHardcodedMacOSShortcutGlyphs,
noUiImportsInCore noUIImportsInCore
] ]
} }

View file

@ -35,10 +35,13 @@ afterEach(() => {
describe('acronym identifier casing', () => { describe('acronym identifier casing', () => {
test('rejects mixed-case first-party acronym identifiers', () => { test('rejects mixed-case first-party acronym identifiers', () => {
const result = lint('export class CloudCorsError {}\nexport function sendRpc() {}') const result = lint(
'export class CloudCorsError {}\nexport function sendRpc() {}\nexport type FontPickerUi = {}'
)
expect(result.exitCode).toBe(1) expect(result.exitCode).toBe(1)
expect(result.stdout.toString()).toContain('CloudCorsError') expect(result.stdout.toString()).toContain('CloudCorsError')
expect(result.stdout.toString()).toContain('sendRpc') expect(result.stdout.toString()).toContain('sendRpc')
expect(result.stdout.toString()).toContain('FontPickerUi')
}) })
test('accepts canonical first-party acronym identifiers', () => { test('accepts canonical first-party acronym identifiers', () => {