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:
parent
7f91594d0c
commit
55b368dc4a
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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. */
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue