mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-13 15:48:31 +08:00
chore(audits): drop dead ESLint directives and redundant check:dead-code; fix the PPTX blend-group call they hid (#8876)
* chore(audits): drop dead ESLint directives and the redundant check:dead-code script - Remove all 89 `eslint-disable` comments. Nothing runs ESLint (no eslint dependency or config in any workspace; Next 16 `next build` has no lint step), so they suppressed nothing. Reasons they carried are kept as plain `//` whys. - `check:comment-hygiene` now rejects `eslint-disable`/`eslint-enable` comments so they cannot return. - Remove `check:dead-code`: `check:unused-exports` already runs knip with the same config and a superset of its issue types, and `check:audits` skipped the alias. - Ignore unused exports in the generated `apps/docs/components/icons.tsx` (a verbatim copy of the sim icon set, whose export surface is ratcheted at the source) and shrink the unused-exports baseline by its 66 entries. * fix(pptx): stop calling the appended rect blend group as a function * docs(skills): name both ESLint directives the comment audit rejects * docs: name both ESLint directives in the comment rule * test(pptx): cover the rect gradient blend-group path
This commit is contained in:
@@ -33,7 +33,7 @@ This codebase's convention: **TSDoc for documentation; an inline `//` only for a
|
||||
- A `//` comment that explains a **non-obvious why**: a workaround for an upstream bug, an ordering constraint, a perf reason, a spec/edge-case the code can't self-document (`// first-match wins — matches the old find() semantics`).
|
||||
- Existing TSDoc `/** ... */` blocks on declarations — leave them (only tighten if verbose).
|
||||
- `// boundary-raw-fetch:`, `// double-cast-allowed:`, `// boundary-raw-json:`, `// untyped-response:`, `-- migration-safe:` (SQL migrations), `// rq-lint-allow:`, `// client-boundary-allow:` and any other `<kebab-tag>: <reason>` annotation a script under `scripts/` greps for, in line-comment or block-comment form (e.g. the `/** svg-path-precision-exception: ... */` directive on icon paths) — these are load-bearing, never touch them.
|
||||
- `// biome-ignore`, `// eslint-disable`, `// @ts-expect-error` and other tooling directives.
|
||||
- `// biome-ignore`, `// @ts-expect-error` and other tooling directives. (Nothing runs ESLint, so `eslint-disable`/`eslint-enable` directives are dead; delete them — `check:comment-hygiene` rejects both.)
|
||||
- `// TODO` / `// FIXME` that point at real, still-open work.
|
||||
|
||||
## Bias
|
||||
|
||||
@@ -6,7 +6,7 @@ This file (also `AGENTS.md`) holds the repo-wide rules. Area detail lives in `.c
|
||||
|
||||
- **Package manager**: `bun` and `bunx`, never `npm` and `npx`.
|
||||
- **Logging**: `createLogger` from `@sim/logger`; `logger.info` / `logger.warn` / `logger.error`, never `console.log`. Inside `withRouteHandler` the logger already carries the request ID — no manual `withMetadata({ requestId })`.
|
||||
- **Comments**: name things so the code explains itself. TSDoc documents exported APIs and non-obvious modules. An inline `//` is only for a terse, non-obvious *why*, or for a script-enforced `// <tag>: <reason>` annotation (`boundary-raw-fetch`, `double-cast-allowed`, `boundary-raw-json`, `untyped-response`, `rq-lint-allow`, `client-boundary-allow`, `utils-lint-allow`, …). History belongs in the commit message. No `====` separators or commented-out code (`check:comment-hygiene` enforces this). The `/you-might-not-need-a-comment` skill applies this to a diff.
|
||||
- **Comments**: name things so the code explains itself. TSDoc documents exported APIs and non-obvious modules. An inline `//` is only for a terse, non-obvious *why*, or for a script-enforced `// <tag>: <reason>` annotation (`boundary-raw-fetch`, `double-cast-allowed`, `boundary-raw-json`, `untyped-response`, `rq-lint-allow`, `client-boundary-allow`, `utils-lint-allow`, …). History belongs in the commit message. No `====` separators, commented-out code, or ESLint directives (`eslint-disable`/`eslint-enable`; nothing runs ESLint; `check:comment-hygiene` enforces all three). The `/you-might-not-need-a-comment` skill applies this to a diff.
|
||||
- **ID generation**: `generateId()` (UUID v4, the default) or `generateShortId(size?)` (URL-safe, 21 chars by default) from `@sim/utils/id` — never `crypto.randomUUID()`, `nanoid`, or `uuid`. Both use `crypto.getRandomValues()`, so they also work in non-secure (HTTP) browsers. For other randomness, `@sim/utils/random` (`randomInt`, `randomFloat`, `randomItem`, `generateRandomBytes`, `generateRandomHex`) — never `Math.random()` or `crypto.randomBytes()`.
|
||||
- **Common utilities**: use the shared helpers from the `@sim/utils/<module>` subpaths instead of inline implementations (`check:utils` bans most of the inline forms below):
|
||||
- `sleep(ms)` from `@sim/utils/helpers` — never `new Promise(resolve => setTimeout(resolve, ms))`
|
||||
|
||||
+118
-122
@@ -548,137 +548,133 @@ export default function ResumeExecutionPage({
|
||||
}
|
||||
}, [refetchExecutionDetail, selectedContextId])
|
||||
|
||||
const handleResume = useCallback(
|
||||
async () => {
|
||||
if (!selectedContextId || !selectedDetail) {
|
||||
setError('No pause point is selected. Refresh and try again.')
|
||||
return
|
||||
}
|
||||
setLoadingAction(true)
|
||||
setError(null)
|
||||
setMessage(null)
|
||||
let resumePayload: any
|
||||
try {
|
||||
if (isHumanMode && hasInputFormat) {
|
||||
const errors: Record<string, string> = {}
|
||||
const submission: Record<string, any> = {}
|
||||
for (const field of inputFormatFields) {
|
||||
const rawValue = formValues[field.name] ?? ''
|
||||
const hasValue =
|
||||
field.type === 'boolean'
|
||||
? rawValue === 'true' || rawValue === 'false'
|
||||
: rawValue.trim().length > 0 && rawValue !== '__unset__'
|
||||
if (!hasValue || rawValue === '__unset__') {
|
||||
if (field.required) errors[field.name] = 'This field is required.'
|
||||
continue
|
||||
}
|
||||
const { value, error: parseError } = parseFormValue(field, rawValue)
|
||||
if (parseError) {
|
||||
errors[field.name] = parseError
|
||||
continue
|
||||
}
|
||||
if (value !== undefined) submission[field.name] = value
|
||||
const handleResume = useCallback(async () => {
|
||||
if (!selectedContextId || !selectedDetail) {
|
||||
setError('No pause point is selected. Refresh and try again.')
|
||||
return
|
||||
}
|
||||
setLoadingAction(true)
|
||||
setError(null)
|
||||
setMessage(null)
|
||||
let resumePayload: any
|
||||
try {
|
||||
if (isHumanMode && hasInputFormat) {
|
||||
const errors: Record<string, string> = {}
|
||||
const submission: Record<string, any> = {}
|
||||
for (const field of inputFormatFields) {
|
||||
const rawValue = formValues[field.name] ?? ''
|
||||
const hasValue =
|
||||
field.type === 'boolean'
|
||||
? rawValue === 'true' || rawValue === 'false'
|
||||
: rawValue.trim().length > 0 && rawValue !== '__unset__'
|
||||
if (!hasValue || rawValue === '__unset__') {
|
||||
if (field.required) errors[field.name] = 'This field is required.'
|
||||
continue
|
||||
}
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setFormErrors(errors)
|
||||
setError('Fix the highlighted fields before resuming.')
|
||||
const { value, error: parseError } = parseFormValue(field, rawValue)
|
||||
if (parseError) {
|
||||
errors[field.name] = parseError
|
||||
continue
|
||||
}
|
||||
if (value !== undefined) submission[field.name] = value
|
||||
}
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setFormErrors(errors)
|
||||
setError('Fix the highlighted fields before resuming.')
|
||||
setLoadingAction(false)
|
||||
return
|
||||
}
|
||||
setFormErrors({})
|
||||
resumePayload = { submission }
|
||||
} else {
|
||||
let parsedInput: any
|
||||
if (resumeInput && resumeInput.trim().length > 0) {
|
||||
try {
|
||||
parsedInput = JSON.parse(resumeInput)
|
||||
} catch {
|
||||
setError('Resume input must be valid JSON.')
|
||||
setLoadingAction(false)
|
||||
return
|
||||
}
|
||||
setFormErrors({})
|
||||
resumePayload = { submission }
|
||||
} else {
|
||||
let parsedInput: any
|
||||
if (resumeInput && resumeInput.trim().length > 0) {
|
||||
try {
|
||||
parsedInput = JSON.parse(resumeInput)
|
||||
} catch {
|
||||
setError('Resume input must be valid JSON.')
|
||||
setLoadingAction(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
resumePayload = parsedInput
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Failed to prepare resume payload.')
|
||||
setLoadingAction(false)
|
||||
resumePayload = parsedInput
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Failed to prepare resume payload.')
|
||||
setLoadingAction(false)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { ok, payload } = await resumeMutation.mutateAsync({
|
||||
workflowId,
|
||||
executionId,
|
||||
contextId: selectedContextId,
|
||||
input: resumePayload,
|
||||
})
|
||||
if (!ok) {
|
||||
setError(payload.error || 'Failed to resume execution.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { ok, payload } = await resumeMutation.mutateAsync({
|
||||
workflowId,
|
||||
executionId,
|
||||
contextId: selectedContextId,
|
||||
input: resumePayload,
|
||||
})
|
||||
if (!ok) {
|
||||
setError(payload.error || 'Failed to resume execution.')
|
||||
return
|
||||
const nextStatus = payload.status === 'queued' ? 'queued' : 'resuming'
|
||||
const nextQueuePosition = payload.queuePosition ?? null
|
||||
const fallbackContextId =
|
||||
executionDetail?.pausePoints.find(
|
||||
(point) => point.contextId !== selectedContextId && point.resumeStatus === 'paused'
|
||||
)?.contextId ?? null
|
||||
queryClient.setQueryData<PausedExecutionDetail>(
|
||||
resumeKeys.execution(workflowId, executionId),
|
||||
(prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
pausePoints: prev.pausePoints.map((point) =>
|
||||
point.contextId === selectedContextId
|
||||
? { ...point, resumeStatus: nextStatus, queuePosition: nextQueuePosition }
|
||||
: point
|
||||
),
|
||||
}
|
||||
}
|
||||
const nextStatus = payload.status === 'queued' ? 'queued' : 'resuming'
|
||||
const nextQueuePosition = payload.queuePosition ?? null
|
||||
const fallbackContextId =
|
||||
executionDetail?.pausePoints.find(
|
||||
(point) => point.contextId !== selectedContextId && point.resumeStatus === 'paused'
|
||||
)?.contextId ?? null
|
||||
queryClient.setQueryData<PausedExecutionDetail>(
|
||||
resumeKeys.execution(workflowId, executionId),
|
||||
(prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
pausePoints: prev.pausePoints.map((point) =>
|
||||
point.contextId === selectedContextId
|
||||
? { ...point, resumeStatus: nextStatus, queuePosition: nextQueuePosition }
|
||||
: point
|
||||
),
|
||||
}
|
||||
)
|
||||
queryClient.setQueryData<PauseContextDetail | null>(
|
||||
resumeKeys.context(workflowId, executionId, selectedContextId),
|
||||
(prev) => {
|
||||
if (!prev || prev.pausePoint.contextId !== selectedContextId) return prev
|
||||
return {
|
||||
...prev,
|
||||
pausePoint: {
|
||||
...prev.pausePoint,
|
||||
resumeStatus: nextStatus,
|
||||
queuePosition: nextQueuePosition,
|
||||
},
|
||||
}
|
||||
)
|
||||
queryClient.setQueryData<PauseContextDetail | null>(
|
||||
resumeKeys.context(workflowId, executionId, selectedContextId),
|
||||
(prev) => {
|
||||
if (!prev || prev.pausePoint.contextId !== selectedContextId) return prev
|
||||
return {
|
||||
...prev,
|
||||
pausePoint: {
|
||||
...prev.pausePoint,
|
||||
resumeStatus: nextStatus,
|
||||
queuePosition: nextQueuePosition,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
setSelectedContextIdOverride((override) => {
|
||||
const currentContextId = override === undefined ? (defaultContextId ?? null) : override
|
||||
return currentContextId !== selectedContextId ? override : fallbackContextId
|
||||
})
|
||||
setMessage(
|
||||
payload.status === 'queued' ? 'Resume request queued.' : 'Resume started successfully.'
|
||||
)
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Unexpected error while resuming execution.')
|
||||
} finally {
|
||||
setLoadingAction(false)
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
workflowId,
|
||||
executionId,
|
||||
selectedContextId,
|
||||
isHumanMode,
|
||||
hasInputFormat,
|
||||
inputFormatFields,
|
||||
formValues,
|
||||
parseFormValue,
|
||||
resumeInput,
|
||||
selectedDetail,
|
||||
executionDetail,
|
||||
queryClient,
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
setSelectedContextIdOverride((override) => {
|
||||
const currentContextId = override === undefined ? (defaultContextId ?? null) : override
|
||||
return currentContextId !== selectedContextId ? override : fallbackContextId
|
||||
})
|
||||
setMessage(
|
||||
payload.status === 'queued' ? 'Resume request queued.' : 'Resume started successfully.'
|
||||
)
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Unexpected error while resuming execution.')
|
||||
} finally {
|
||||
setLoadingAction(false)
|
||||
}
|
||||
}, [
|
||||
workflowId,
|
||||
executionId,
|
||||
selectedContextId,
|
||||
isHumanMode,
|
||||
hasInputFormat,
|
||||
inputFormatFields,
|
||||
formValues,
|
||||
parseFormValue,
|
||||
resumeInput,
|
||||
selectedDetail,
|
||||
executionDetail,
|
||||
queryClient,
|
||||
])
|
||||
|
||||
const isFormComplete = useMemo(() => {
|
||||
if (!isHumanMode || !hasInputFormat) return true
|
||||
|
||||
@@ -61,7 +61,6 @@ export function useCsvTruncationImport(
|
||||
}
|
||||
)
|
||||
// importFile.mutate and router are stable references
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [workspaceId, file.id, file.key, file.name])
|
||||
|
||||
// Surface the cap as a warning toast with an import action, once per file.
|
||||
|
||||
-1
@@ -442,7 +442,6 @@ const TerminalView = memo(function TerminalView({
|
||||
}
|
||||
// Theme is applied by the effect below so the terminal is never torn down
|
||||
// (and its buffer never lost) for a repaint.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [terminalId, scopeId])
|
||||
|
||||
// Runs after the effect above, which is what installs these.
|
||||
|
||||
-3
@@ -363,7 +363,6 @@ export function ResourceTabs({
|
||||
// The chat owner handles optimistic state and its single ordered persistence queue.
|
||||
onAddResource(resource)
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[desktopScopeId, onAddResource, selectResource]
|
||||
)
|
||||
|
||||
@@ -477,7 +476,6 @@ export function ResourceTabs({
|
||||
anchorIdRef.current = null
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
activeId,
|
||||
chatId,
|
||||
@@ -574,7 +572,6 @@ export function ResourceTabs({
|
||||
).catch(() => toast.error('Could not reorder that terminal. Please try again.'))
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[chatId, desktopScopeId, resources, onReorderResources]
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ export function PromptEditor({
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus && !readOnly) editor.focusAtEnd()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only focus
|
||||
// mount-only focus
|
||||
}, [])
|
||||
|
||||
/**
|
||||
|
||||
@@ -207,7 +207,7 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
|
||||
textarea.setSelectionRange(caretText.length, caretText.length)
|
||||
}
|
||||
}
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps -- intentional mount-only restore
|
||||
}, []) // intentional mount-only restore
|
||||
|
||||
const isFirstSaveRef = useRef(true)
|
||||
const draftSaveTimerRef = useRef<number | null>(null)
|
||||
|
||||
@@ -298,7 +298,6 @@ function HomeContent({ chatId, userName, userId }: HomeProps) {
|
||||
// listing it would re-run this drain on every render. Omitted deliberately to
|
||||
// keep it one-shot — and harmless either way, since `consume` clears the entry
|
||||
// atomically and any re-run would find nothing.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- see above
|
||||
}, [chatId, workspaceId, prepareResourceViewForAgentTurn, sendMessage])
|
||||
|
||||
function resolveResourceFromContext(
|
||||
|
||||
@@ -525,7 +525,7 @@ function KnowledgeContent() {
|
||||
await deleteKnowledgeBase.mutateAsync({ knowledgeBaseId: id })
|
||||
logger.info(`Knowledge base deleted: ${id}`)
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation objects are unstable; mutateAsync is stable in v5
|
||||
// mutation objects are unstable; mutateAsync is stable in v5
|
||||
[knowledgeBases, userPermissions.canEdit, userPermissions.canAdmin]
|
||||
)
|
||||
|
||||
@@ -933,7 +933,6 @@ function KnowledgeContent() {
|
||||
logger.error('Failed to create folder', createError)
|
||||
toast.error(getErrorMessage(createError, 'Failed to create folder'))
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [workspaceId])
|
||||
|
||||
useRegisterGlobalCommands(() => [
|
||||
@@ -987,7 +986,6 @@ function KnowledgeContent() {
|
||||
logger.error('Failed to delete folder', deleteError)
|
||||
toast.error(getErrorMessage(deleteError, 'Failed to delete folder'))
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [workspaceId, openFolder, canDeleteFolder])
|
||||
|
||||
const descendantsByFolderId = useMemo(() => buildDescendantIndex(folders), [folders])
|
||||
@@ -998,7 +996,7 @@ function KnowledgeContent() {
|
||||
const mutation = pinnedBaseIds.has(kb.id) ? unpinItem : pinItem
|
||||
mutation.mutate({ workspaceId, resourceType: 'knowledge_base', resourceId: kb.id })
|
||||
closeRowContextMenu()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation objects are unstable; mutate is stable in v5
|
||||
// mutation objects are unstable; mutate is stable in v5
|
||||
}, [workspaceId, pinnedBaseIds, closeRowContextMenu])
|
||||
|
||||
const handleToggleFolderPin = useCallback(() => {
|
||||
@@ -1007,7 +1005,7 @@ function KnowledgeContent() {
|
||||
const mutation = pinnedFolderIds.has(folder.id) ? unpinItem : pinItem
|
||||
mutation.mutate({ workspaceId, resourceType: 'folder', resourceId: folder.id })
|
||||
closeFolderContextMenu()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation objects are unstable; mutate is stable in v5
|
||||
// mutation objects are unstable; mutate is stable in v5
|
||||
}, [workspaceId, pinnedFolderIds, closeFolderContextMenu])
|
||||
|
||||
/** Move targets for the folder under the cursor: itself and its subtree are unreachable. */
|
||||
@@ -1045,7 +1043,7 @@ function KnowledgeContent() {
|
||||
toast.error(getErrorMessage(moveError, 'Failed to move folder'))
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation objects are unstable; mutateAsync is stable in v5
|
||||
// mutation objects are unstable; mutateAsync is stable in v5
|
||||
[workspaceId]
|
||||
)
|
||||
|
||||
@@ -1111,7 +1109,7 @@ function KnowledgeContent() {
|
||||
}
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation objects are unstable; mutate is stable in v5
|
||||
// mutation objects are unstable; mutate is stable in v5
|
||||
[clearSelection]
|
||||
)
|
||||
|
||||
@@ -1157,7 +1155,7 @@ function KnowledgeContent() {
|
||||
// The mutation toasts the request failure itself; the modal stays open to allow a retry.
|
||||
logger.error('Failed to delete selected items', deleteError)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation objects are unstable; mutateAsync is stable in v5
|
||||
// mutation objects are unstable; mutateAsync is stable in v5
|
||||
}, [selectedKnowledgeBaseIds, selectedFolderIds, clearSelection, canDeleteSelection])
|
||||
|
||||
/**
|
||||
|
||||
@@ -345,7 +345,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
|
||||
if (scrollAreaRef.current) {
|
||||
scrollAreaRef.current.scrollTop = 0
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- stable nuqs setter; reset tab when switching logs
|
||||
// stable nuqs setter; reset tab when switching logs
|
||||
}, [log.id])
|
||||
|
||||
const isLikelyExecution = !!log.executionId && log.trigger !== 'mothership'
|
||||
|
||||
@@ -604,25 +604,20 @@ export default function Logs() {
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, 'Failed to stop run'))
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [contextMenuLog, userPermissions.canEdit])
|
||||
|
||||
const retryLog = useCallback(
|
||||
async (log: WorkflowLogRow | null) => {
|
||||
const workflowId = log?.workflow?.id || log?.workflowId
|
||||
const executionId = log?.executionId
|
||||
if (!workflowId || !executionId) return
|
||||
const retryLog = useCallback(async (log: WorkflowLogRow | null) => {
|
||||
const workflowId = log?.workflow?.id || log?.workflowId
|
||||
const executionId = log?.executionId
|
||||
if (!workflowId || !executionId) return
|
||||
|
||||
try {
|
||||
await retryExecution.mutateAsync({ workflowId, executionId })
|
||||
toast.success('Retry started')
|
||||
} catch {
|
||||
toast.error('Failed to retry execution')
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
)
|
||||
try {
|
||||
await retryExecution.mutateAsync({ workflowId, executionId })
|
||||
toast.success('Retry started')
|
||||
} catch {
|
||||
toast.error('Failed to retry execution')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleRetryExecution = useCallback(() => {
|
||||
retryLog(contextMenuLog)
|
||||
|
||||
@@ -230,6 +230,5 @@ export function useRegisterGlobalCommands(commands: GlobalCommand[] | (() => Glo
|
||||
}))
|
||||
const unregister = ctx.register(wrappedCommands)
|
||||
return unregister
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ export function MCP() {
|
||||
if (!refreshServerMutation.isSuccess && !refreshServerMutation.isError) return
|
||||
const timeout = window.setTimeout(() => refreshServerMutation.reset(), 3000)
|
||||
return () => window.clearTimeout(timeout)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation object is unstable; status flags are the triggers
|
||||
// mutation object is unstable; status flags are the triggers
|
||||
}, [refreshServerMutation.isSuccess, refreshServerMutation.isError])
|
||||
|
||||
const editingServer = editingServerId
|
||||
|
||||
@@ -939,7 +939,7 @@ function TablesContent() {
|
||||
moveTable.mutate({ tableId: activeTable.id, folderId })
|
||||
closeRowContextMenu()
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation objects are unstable; mutate is stable in v5
|
||||
// mutation objects are unstable; mutate is stable in v5
|
||||
[activeTable, closeRowContextMenu]
|
||||
)
|
||||
|
||||
@@ -954,7 +954,7 @@ function TablesContent() {
|
||||
}
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation objects are unstable; mutate is stable in v5
|
||||
// mutation objects are unstable; mutate is stable in v5
|
||||
[workspaceId]
|
||||
)
|
||||
|
||||
@@ -992,7 +992,7 @@ function TablesContent() {
|
||||
}
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation objects are unstable; mutate is stable in v5
|
||||
// mutation objects are unstable; mutate is stable in v5
|
||||
[clearSelection]
|
||||
)
|
||||
|
||||
@@ -1035,7 +1035,7 @@ function TablesContent() {
|
||||
// The mutation toasts the request failure itself; the modal stays open to allow a retry.
|
||||
logger.error('Failed to delete selected items:', err)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation objects are unstable; mutateAsync is stable in v5
|
||||
// mutation objects are unstable; mutateAsync is stable in v5
|
||||
}, [selectedTableIds, selectedFolderIds, clearSelection])
|
||||
|
||||
/**
|
||||
@@ -1126,7 +1126,7 @@ function TablesContent() {
|
||||
const mutation = pinned ? unpinItem : pinItem
|
||||
mutation.mutate({ workspaceId, resourceType: target.resourceType, resourceId: target.id })
|
||||
closeRowContextMenu()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mutation objects are unstable; mutate is stable in v5
|
||||
// mutation objects are unstable; mutate is stable in v5
|
||||
}, [
|
||||
workspaceId,
|
||||
contextMenuKind,
|
||||
|
||||
-1
@@ -188,7 +188,6 @@ export const KeyboardNavigationHandler: React.FC<KeyboardNavigationHandlerProps>
|
||||
if (!visible || visibleIndices.length === 0) return
|
||||
|
||||
setSelectedIndex(visibleIndices[0])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, isInFolder, currentFolder, nestedPathLength])
|
||||
|
||||
const handleKeyboardEvent = useEffectEvent((e: KeyboardEvent) => {
|
||||
|
||||
-1
@@ -65,7 +65,6 @@ export function useToolbarItemInteractions({
|
||||
dragPreviewRef.current = preview
|
||||
|
||||
// Force browser to render the element by triggering reflow
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
preview.offsetHeight
|
||||
|
||||
// Set the custom drag image with offset to center it on cursor
|
||||
|
||||
-1
@@ -81,7 +81,6 @@ export function useWebhookInfo(blockId: string, workflowId: string): UseWebhookI
|
||||
logger.error('Error reactivating webhook:', error)
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[workflowId, blockId]
|
||||
)
|
||||
|
||||
|
||||
@@ -40,6 +40,5 @@ export function useBlockDimensions({
|
||||
updateBlockLayoutMetrics(blockId, dimensions)
|
||||
updateNodeInternals(blockId)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [blockId, updateBlockLayoutMetrics, updateNodeInternals, ...dependencies])
|
||||
}
|
||||
|
||||
@@ -2720,7 +2720,6 @@ export function useWorkflowExecution() {
|
||||
releaseReconnectOwnership()
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeWorkflowId, hasHydrated, reconnectAttemptNonce])
|
||||
|
||||
return {
|
||||
|
||||
-4
@@ -126,7 +126,6 @@ export function useWorkspaceManagement({
|
||||
return false
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
)
|
||||
|
||||
@@ -167,7 +166,6 @@ export function useWorkspaceManagement({
|
||||
throw error
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[switchWorkspace]
|
||||
)
|
||||
|
||||
@@ -194,7 +192,6 @@ export function useWorkspaceManagement({
|
||||
logger.error('Error deleting workspace:', error)
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[switchWorkspace]
|
||||
)
|
||||
|
||||
@@ -228,7 +225,6 @@ export function useWorkspaceManagement({
|
||||
throw error
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[switchWorkspace, sessionUserId]
|
||||
)
|
||||
|
||||
|
||||
@@ -298,7 +298,6 @@ async function listWorkspaceProjects(
|
||||
const projects: AsanaProject[] = []
|
||||
let offset: string | undefined
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const result = await asanaGet<{ data: AsanaProject[]; next_page: { offset: string } | null }>(
|
||||
accessToken,
|
||||
|
||||
@@ -51,7 +51,6 @@ const {
|
||||
storageConfig,
|
||||
mockGetBlobContainerClient,
|
||||
} = vi.hoisted(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const actualPath = require('path') as typeof import('path')
|
||||
return {
|
||||
mockVerifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -34,7 +34,6 @@ interface Storage<T> {
|
||||
let storage: Storage<PermissionGroupConfigStore>
|
||||
|
||||
if (typeof globalThis.process !== 'undefined' && globalThis.process.versions?.node) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { AsyncLocalStorage } = require('node:async_hooks') as typeof import('node:async_hooks')
|
||||
storage = new AsyncLocalStorage<PermissionGroupConfigStore>()
|
||||
} else {
|
||||
|
||||
@@ -23,7 +23,6 @@ function getClient(): PostHog | null {
|
||||
return null
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { PostHog } = require('posthog-node') as typeof import('posthog-node')
|
||||
_client = new PostHog(key, {
|
||||
host: 'https://us.i.posthog.com',
|
||||
|
||||
@@ -1331,12 +1331,10 @@ function buildBarChartOption(
|
||||
cfg?.showVal
|
||||
? {
|
||||
show: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
position: mapBarLabelPosition(cfg.position, isStacked) as any,
|
||||
fontSize: cfg.fontSize ?? 9,
|
||||
...(cfg.color ? { color: cfg.color } : {}),
|
||||
...(cfg.bold === true ? { fontWeight: 'bold' } : {}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
formatter: (params: any) => {
|
||||
const rawVal = params?.value
|
||||
const val =
|
||||
@@ -1368,7 +1366,6 @@ function buildBarChartOption(
|
||||
return {
|
||||
value: v,
|
||||
label: buildLabel(merged),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any
|
||||
})
|
||||
|
||||
@@ -1617,7 +1614,6 @@ function buildPieChartOption(
|
||||
const explosions = firstSer ? parseExplosion(firstSer, firstSeries.categories.length) : undefined
|
||||
|
||||
const pieData = firstSeries.categories.map((cat, i) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const item: any = {
|
||||
name: cat || `Item ${i + 1}`,
|
||||
value: firstSeries.values[i] ?? 0,
|
||||
@@ -1635,11 +1631,9 @@ function buildPieChartOption(
|
||||
|
||||
// Build label formatter based on data label config; show value and percent when requested
|
||||
const fc = firstSeries.formatCode
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let labelFormatter: string | ((params: any) => string) = '{b}: {c} ({d}%)'
|
||||
if (sharedLabels) {
|
||||
if (sharedLabels.showVal && fc && fc.includes('%')) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
labelFormatter = (params: any) => {
|
||||
const parts: string[] = []
|
||||
if (sharedLabels!.showCatName) parts.push(params.name)
|
||||
@@ -2164,7 +2158,6 @@ function buildStockChartOption(
|
||||
seriesArr[1].values[i] ?? 0,
|
||||
seriesArr[2].values[i] ?? 0,
|
||||
]),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
renderItem: (_params: any, api: any) => {
|
||||
const xValue = api.value(0)
|
||||
const high = api.value(1)
|
||||
@@ -2488,7 +2481,6 @@ function buildChartPalette(chartXml: SafeXmlNode, ctx: RenderContext): string[]
|
||||
* font size was set on that element (i.e., value matches our hardcoded defaults).
|
||||
*/
|
||||
function applyDefaultFontSizes(option: echarts.EChartsOption, defaultFs: number): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const opt = option as any
|
||||
|
||||
// Title: our defaults are 12 or 14 — replace with the chart-space default
|
||||
@@ -2537,7 +2529,6 @@ function applyDefaultFontSizes(option: echarts.EChartsOption, defaultFs: number)
|
||||
}
|
||||
|
||||
function applyDefaultFontFamily(option: echarts.EChartsOption, fontFamily: string): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const opt = option as any
|
||||
|
||||
if (opt.title?.textStyle && !opt.title.textStyle.fontFamily) {
|
||||
@@ -2574,7 +2565,6 @@ function applyLegendGridMargins(
|
||||
chartNode: SafeXmlNode,
|
||||
defaultFs: number | undefined
|
||||
): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const opt = option as any
|
||||
if (!opt.grid || !opt.legend) return
|
||||
if (opt.legend.show === false) return
|
||||
@@ -2628,7 +2618,6 @@ function applyLegendGridMargins(
|
||||
* This post-processes the ECharts option to set axis max when not explicitly provided.
|
||||
*/
|
||||
function applyNiceAxisRange(option: echarts.EChartsOption): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const opt = option as any
|
||||
|
||||
// Only applies to cartesian charts with axes
|
||||
@@ -3152,9 +3141,7 @@ export function parseChartXml(chartXml: SafeXmlNode, ctx: RenderContext): ParseC
|
||||
}
|
||||
if (plotAreaBg && option.grid) {
|
||||
// Apply plot area background via grid (for cartesian charts)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(option.grid as any).backgroundColor = plotAreaBg
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(option.grid as any).show = true
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ export function createRenderContext(
|
||||
|
||||
const layout: LayoutData = presentation.layouts.get(layoutPath) || {
|
||||
placeholders: [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
spTree: {} as any,
|
||||
rels: new Map(),
|
||||
showMasterSp: true,
|
||||
@@ -53,7 +52,6 @@ export function createRenderContext(
|
||||
colorMap: new Map(),
|
||||
textStyles: {},
|
||||
placeholders: [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
spTree: {} as any,
|
||||
rels: new Map(),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseShapeNode } from '@/lib/pptx-renderer/model/nodes/shape-node'
|
||||
import { parseXml } from '@/lib/pptx-renderer/parser/xml-parser'
|
||||
import type { RenderContext } from '@/lib/pptx-renderer/renderer/render-context'
|
||||
import { renderShape } from '@/lib/pptx-renderer/renderer/shape-renderer'
|
||||
|
||||
const EMPTY_NODE = parseXml(
|
||||
'<p:spTree xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" />'
|
||||
)
|
||||
|
||||
function createContext(): RenderContext {
|
||||
const slide = {
|
||||
index: 0,
|
||||
nodes: [],
|
||||
layoutIndex: '',
|
||||
rels: new Map(),
|
||||
slidePath: 'ppt/slides/slide1.xml',
|
||||
showMasterSp: true,
|
||||
}
|
||||
|
||||
return {
|
||||
presentation: {
|
||||
width: 960,
|
||||
height: 540,
|
||||
slides: [slide],
|
||||
layouts: new Map(),
|
||||
masters: new Map(),
|
||||
themes: new Map(),
|
||||
slideToLayout: new Map(),
|
||||
layoutToMaster: new Map(),
|
||||
masterToTheme: new Map(),
|
||||
media: new Map(),
|
||||
charts: new Map(),
|
||||
isWps: false,
|
||||
},
|
||||
slide,
|
||||
theme: {
|
||||
colorScheme: new Map(),
|
||||
majorFont: { latin: 'Calibri', ea: '', cs: '' },
|
||||
minorFont: { latin: 'Calibri', ea: '', cs: '' },
|
||||
fillStyles: [],
|
||||
lineStyles: [],
|
||||
effectStyles: [],
|
||||
},
|
||||
master: {
|
||||
colorMap: new Map(),
|
||||
textStyles: {},
|
||||
placeholders: [],
|
||||
spTree: EMPTY_NODE,
|
||||
rels: new Map(),
|
||||
},
|
||||
layout: {
|
||||
placeholders: [],
|
||||
spTree: EMPTY_NODE,
|
||||
rels: new Map(),
|
||||
showMasterSp: true,
|
||||
},
|
||||
mediaUrlCache: new Map(),
|
||||
colorCache: new Map(),
|
||||
}
|
||||
}
|
||||
|
||||
describe('renderShape', () => {
|
||||
it('inserts the rect gradient blend group before the main path', () => {
|
||||
const node = parseShapeNode(
|
||||
parseXml(`
|
||||
<p:sp xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
|
||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
||||
<p:nvSpPr><p:cNvPr id="2" name="Rect" /><p:cNvSpPr /><p:nvPr /></p:nvSpPr>
|
||||
<p:spPr>
|
||||
<a:xfrm><a:off x="0" y="0" /><a:ext cx="914400" cy="914400" /></a:xfrm>
|
||||
<a:prstGeom prst="rect"><a:avLst /></a:prstGeom>
|
||||
<a:gradFill>
|
||||
<a:gsLst>
|
||||
<a:gs pos="0"><a:srgbClr val="FF0000" /></a:gs>
|
||||
<a:gs pos="100000"><a:srgbClr val="0000FF" /></a:gs>
|
||||
</a:gsLst>
|
||||
<a:path path="rect" />
|
||||
</a:gradFill>
|
||||
</p:spPr>
|
||||
</p:sp>
|
||||
`)
|
||||
)
|
||||
|
||||
const svg = renderShape(node, createContext()).querySelector('svg')
|
||||
|
||||
const blendGroup = svg?.querySelector(':scope > g[clip-path]')
|
||||
expect(blendGroup?.nextElementSibling?.localName).toBe('path')
|
||||
expect(blendGroup?.nextElementSibling?.getAttribute('fill')).toBe('none')
|
||||
})
|
||||
})
|
||||
@@ -246,7 +246,6 @@ function createArrowMarker(
|
||||
default:
|
||||
return null
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
;(marker as any)._markerId = id
|
||||
return marker
|
||||
@@ -650,7 +649,6 @@ export function renderShape(node: ShapeNodeData, ctx: RenderContext): HTMLElemen
|
||||
// Mark path as no-fill; the blend group handles it.
|
||||
// Tag the blend group so we can insert it before the main path later.
|
||||
path.setAttribute('fill', 'none')
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(path as any).__rectBlendGroup = blendGroup
|
||||
} else if (gradientFillData.type === 'radial') {
|
||||
const radialGrad = document.createElementNS(svgNs, 'radialGradient')
|
||||
@@ -855,7 +853,6 @@ export function renderShape(node: ShapeNodeData, ctx: RenderContext): HTMLElemen
|
||||
)
|
||||
if (marker) {
|
||||
defs.appendChild(marker)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
path.setAttribute('marker-start', `url(#${(marker as any)._markerId})`)
|
||||
}
|
||||
}
|
||||
@@ -870,20 +867,15 @@ export function renderShape(node: ShapeNodeData, ctx: RenderContext): HTMLElemen
|
||||
)
|
||||
if (marker) {
|
||||
defs.appendChild(marker)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
path.setAttribute('marker-end', `url(#${(marker as any)._markerId})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert rect blend group (two linear gradients + lighten) before the main path
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((path as any).__rectBlendGroup) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
svg.appendChild((path as any).__rectBlendGroup)(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
path as any
|
||||
).__rectBlendGroup = undefined
|
||||
svg.appendChild((path as any).__rectBlendGroup)
|
||||
;(path as any).__rectBlendGroup = undefined
|
||||
}
|
||||
|
||||
svg.appendChild(path)
|
||||
@@ -1442,7 +1434,6 @@ export function renderShape(node: ShapeNodeData, ctx: RenderContext): HTMLElemen
|
||||
const mask = `linear-gradient(to bottom, rgba(255,255,255,${stA.toFixed(3)}) ${stPos.toFixed(1)}%, rgba(255,255,255,${endA.toFixed(3)}) ${endPos.toFixed(1)}%)`
|
||||
const reflectValue = `below ${dist.toFixed(1)}px ${mask}`
|
||||
wrapper.style.setProperty('-webkit-box-reflect', reflectValue)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(wrapper.style as any).webkitBoxReflect = reflectValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,9 +812,7 @@ export function renderTextBody(
|
||||
// Gradient text fill: use background-clip to paint text with gradient
|
||||
if (runStyle.textGradientCss) {
|
||||
element.style.background = runStyle.textGradientCss
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(element.style as any).webkitBackgroundClip = 'text'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(element.style as any).backgroundClip = 'text'
|
||||
element.style.color = 'transparent'
|
||||
}
|
||||
@@ -826,36 +824,26 @@ export function renderTextBody(
|
||||
// Ghost text: no fill + gradient outline → show outline fading via mask
|
||||
const outlineColor = '#ffffff' // base stroke color (gradient applied via mask)
|
||||
element.style.color = 'transparent'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(element.style as any).webkitTextStrokeWidth = `${strokeW}px`
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(element.style as any).webkitTextStrokeColor = outlineColor
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(element.style as any).paintOrder = 'stroke fill'
|
||||
// Use mask-image to apply the gradient fade to the entire text element
|
||||
const maskGrad = runStyle.textOutlineGradientCss
|
||||
element.style.maskImage = maskGrad
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(element.style as any).webkitMaskImage = maskGrad
|
||||
} else if (runStyle.textNoFill && runStyle.textOutlineColor) {
|
||||
// Ghost text with solid outline
|
||||
element.style.color = 'transparent'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(element.style as any).webkitTextStrokeWidth = `${strokeW}px`
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(element.style as any).webkitTextStrokeColor = runStyle.textOutlineColor
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(element.style as any).paintOrder = 'stroke fill'
|
||||
} else if (runStyle.textNoFill) {
|
||||
// noFill with no outline — invisible text (but keep space)
|
||||
element.style.color = 'transparent'
|
||||
} else if (runStyle.textOutlineColor) {
|
||||
// Outline with normal fill
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(element.style as any).webkitTextStrokeWidth = `${strokeW}px`
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(element.style as any).webkitTextStrokeColor = runStyle.textOutlineColor
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(element.style as any).paintOrder = 'stroke fill'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ const table = {
|
||||
},
|
||||
],
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any
|
||||
|
||||
let lastHandle: {
|
||||
|
||||
@@ -49,7 +49,6 @@ function asAppBlocks<T>(blocks: T): Record<string, AppBlockState> {
|
||||
* These tests intentionally use old SubBlockTypes (textarea, select, messages-input, input)
|
||||
* to verify the migration logic converts them to new types.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function legacySubBlocks(subBlocks: Record<string, any>): any {
|
||||
return subBlocks
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
* Hoisted mock setup - vi.mock is hoisted, so we need to hoist the config too.
|
||||
*/
|
||||
const { mockBlockConfigs, createMockGetBlock } = vi.hoisted(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const mockBlockConfigs: Record<string, any> = {
|
||||
starter: {
|
||||
name: 'Starter',
|
||||
@@ -39,7 +38,6 @@ const { mockBlockConfigs, createMockGetBlock } = vi.hoisted(() => {
|
||||
tools: {
|
||||
access: ['anthropic_chat', 'openai_chat'],
|
||||
config: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tool: (params: Record<string, any>) => {
|
||||
const model = params.model || 'gpt-4o'
|
||||
if (model.includes('claude')) return 'anthropic'
|
||||
@@ -214,7 +212,6 @@ const { mockBlockConfigs, createMockGetBlock } = vi.hoisted(() => {
|
||||
},
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const createMockGetBlock = (extraConfigs: Record<string, any> = {}) => {
|
||||
const configs = { ...mockBlockConfigs, ...extraConfigs }
|
||||
return (type: string) => configs[type] || null
|
||||
|
||||
@@ -321,7 +321,6 @@ function generateBoundary(): string {
|
||||
* @see https://github.com/googleapis/google-api-nodejs-client/blob/main/samples/gmail/send.js
|
||||
*/
|
||||
export function encodeRfc2047(value: string): string {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
if (/^[\x00-\x7F]*$/.test(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import { processUrl } from '@/tools/http/utils'
|
||||
process.env.VITEST = 'true'
|
||||
|
||||
describe('HTTP Request Tool', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let tester: ToolTester<any, any>
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
+9
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@6/schema.json",
|
||||
// Plain `knip` (check:dead-code) gates reachability and dependency ownership.
|
||||
// Unused exports, types, and duplicates are ratcheted by check:unused-exports,
|
||||
// which reuses this config in the same knip pass.
|
||||
// Plain `knip` reports reachability and dependency ownership. check:unused-exports runs
|
||||
// knip with this config, gates these at zero, and ratchets unused exports, types, and
|
||||
// duplicates in the same pass.
|
||||
"include": ["files", "dependencies", "unlisted", "unresolved"],
|
||||
// On by default: every workspace's `exports` map serves only this monorepo, so an entry export
|
||||
// no workspace imports is dead. The workspaces below turn it off because their entries are
|
||||
@@ -93,7 +93,12 @@
|
||||
// ensure-pty-prebuilds.ts assembles these package names for universal builds.
|
||||
"ignoreDependencies": ["@lydell/node-pty-darwin-arm64", "@lydell/node-pty-darwin-x64"]
|
||||
},
|
||||
"apps/docs": { "includeEntryExports": false },
|
||||
"apps/docs": {
|
||||
"includeEntryExports": false,
|
||||
// scripts/generate-docs.ts copies this verbatim from apps/sim/components/icons.tsx, whose
|
||||
// export surface is already ratcheted there; docs consumes only the icons it maps.
|
||||
"ignoreIssues": { "components/icons.tsx": ["exports", "types", "duplicates"] }
|
||||
},
|
||||
"apps/pii": { "includeEntryExports": false },
|
||||
"packages/cli": { "includeEntryExports": false },
|
||||
"packages/db": {
|
||||
|
||||
+1
-2
@@ -130,8 +130,7 @@
|
||||
"type-check": "turbo run type-check",
|
||||
"release": "bun run scripts/create-single-release.ts",
|
||||
"test:scripts": "vitest run --config scripts/vitest.config.ts",
|
||||
"check:agent-cli-boundary": "bun run scripts/check-agent-cli-boundary.ts",
|
||||
"check:dead-code": "bun run node_modules/knip/bin/knip.js"
|
||||
"check:agent-cli-boundary": "bun run scripts/check-agent-cli-boundary.ts"
|
||||
},
|
||||
"overrides": {
|
||||
"react": "19.2.4",
|
||||
|
||||
@@ -57,7 +57,6 @@ let storage: Storage<RequestContext>
|
||||
|
||||
if (typeof globalThis.process !== 'undefined' && globalThis.process.versions?.node) {
|
||||
// Node.js — use real AsyncLocalStorage
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { AsyncLocalStorage } = require('node:async_hooks') as typeof import('node:async_hooks')
|
||||
storage = new AsyncLocalStorage<RequestContext>()
|
||||
} else {
|
||||
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
} from '../factories/block.factory'
|
||||
import type { Position } from '../types'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* Fluent builder for creating complex workflow states.
|
||||
*
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import type { BlockData, BlockOutput, Position } from '../types'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* Options for creating a mock block.
|
||||
* All fields are optional - sensible defaults are provided.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* Options for creating a mock workflow state.
|
||||
* Uses `any` for complex types to avoid conflicts with app types.
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
* Tests themselves provide type safety through their actual usage of app types.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export interface Position {
|
||||
x: number
|
||||
y: number
|
||||
|
||||
@@ -98,3 +98,28 @@ describe('commented-out-code', () => {
|
||||
expect(rules('// return <Panel open={open} />;', 'view.tsx')).toEqual(['commented-out-code@1'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('eslint-directive', () => {
|
||||
it('flags own-line, trailing, and block ESLint directives', () => {
|
||||
const source = [
|
||||
'// eslint-disable-next-line react-hooks/exhaustive-deps',
|
||||
'useEffect(() => {}, []) // eslint-disable-line react-hooks/exhaustive-deps',
|
||||
'/* eslint-disable @typescript-eslint/no-explicit-any */',
|
||||
'/* eslint-enable */',
|
||||
].join('\n')
|
||||
expect(rules(source)).toEqual([
|
||||
'eslint-directive@1',
|
||||
'eslint-directive@2',
|
||||
'eslint-directive@3',
|
||||
'eslint-directive@4',
|
||||
])
|
||||
})
|
||||
|
||||
it('ignores ESLint mentioned in prose and strings', () => {
|
||||
const source = [
|
||||
'// Ported from a file that used an eslint-disable comment',
|
||||
"const rule = '// eslint-disable-next-line'",
|
||||
].join('\n')
|
||||
expect(rules(source)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Fails on the two comment patterns that are never documentation:
|
||||
* Fails on the three comment patterns that are never documentation:
|
||||
*
|
||||
* - `banner`: a separator comment — `// ====`, `// --- Section ---`, `// ─── Title ───`,
|
||||
* `/* ------ v2 ------ *\/`. The repo convention is "no separators": structure lives in the
|
||||
@@ -8,10 +8,13 @@
|
||||
* - `commented-out-code`: a run of `//` lines whose text parses as TypeScript. Git is the
|
||||
* history; dead code left in a comment rots, misleads readers and agents into thinking it
|
||||
* is a live option, and survives every refactor of the code around it.
|
||||
* - `eslint-directive`: an `eslint-disable` / `eslint-enable` comment. The repo lints with Biome
|
||||
* and nothing runs ESLint, so the directive suppresses nothing; it only misleads a reader into
|
||||
* thinking a rule is enforced and deliberately waived here. Biome's form is `// biome-ignore`.
|
||||
*
|
||||
* Both are detected conservatively so a hit is almost always real. Comments are read from the
|
||||
* Babel token stream, so `//` inside strings and template literals is never inspected. A
|
||||
* commented-out run only counts when it parses with no recovery errors, contains code
|
||||
* Banners and commented-out code are detected conservatively so a hit is almost always real.
|
||||
* Comments are read from the Babel token stream, so `//` inside strings and template literals is
|
||||
* never inspected. A commented-out run only counts when it parses with no recovery errors, contains code
|
||||
* punctuation, and is not a lone label (`firstRow: bold`), literal, or `a = b` gloss; prose
|
||||
* lines inside a comment group split it into separate runs. Change-history phrasing
|
||||
* ("previously", "no longer", "used to") was measured and left out: most hits describe live
|
||||
@@ -20,8 +23,8 @@
|
||||
* A legitimate exception (a code sample that must stay a line comment) takes
|
||||
* `// comment-hygiene-allow: <reason>` anywhere in the same comment group.
|
||||
*
|
||||
* The tree was swept clean when this landed (236 banners, 9 commented-out blocks), so there is
|
||||
* no baseline: every hit fails.
|
||||
* The tree was swept clean when each rule landed (236 banners, 9 commented-out blocks, 89
|
||||
* ESLint directives), so there is no baseline: every hit fails.
|
||||
*
|
||||
* Run: `bun run check:comment-hygiene`
|
||||
*/
|
||||
@@ -38,7 +41,7 @@ const ALLOW = 'comment-hygiene-allow:'
|
||||
/** Vendored or generated sources whose comments are not ours to edit. */
|
||||
const EXCLUDED = [/\.d\.ts$/, /(^|\/)bundles\//, /(^|\/)node_modules\//]
|
||||
|
||||
export type Rule = 'banner' | 'commented-out-code'
|
||||
export type Rule = 'banner' | 'commented-out-code' | 'eslint-directive'
|
||||
|
||||
export interface Violation {
|
||||
rule: Rule
|
||||
@@ -49,6 +52,9 @@ export interface Violation {
|
||||
/** Comment text that opens with a run of separator characters. */
|
||||
const BANNER = /^(?:={3,}|-{3,}|─{3,}|━{3,}|\*{3,}|~{3,})/
|
||||
|
||||
/** Comment text that is an ESLint suppression directive. */
|
||||
const ESLINT_DIRECTIVE = /^eslint-(?:disable|enable)\b/
|
||||
|
||||
/** At least one token that ordinary prose does not contain. */
|
||||
const CODE_PUNCTUATION = /[;{}]|=>|\b(?:const|let|return|await|import|export)\s|\w\.\w+\(/
|
||||
|
||||
@@ -154,13 +160,14 @@ function firstCodeSpan(run: LineComment[], jsx: boolean): LineComment | undefine
|
||||
}
|
||||
|
||||
/**
|
||||
* A superset of every hit: a separator right after a comment opener, or a `//` line holding a
|
||||
* {@link CODE_PUNCTUATION} token. Files without one skip the parse, which dominates the run.
|
||||
* A superset of every hit: a separator or ESLint directive right after a comment opener, or a
|
||||
* `//` line holding a {@link CODE_PUNCTUATION} token. Files without one skip the parse, which
|
||||
* dominates the run.
|
||||
*/
|
||||
const MAY_VIOLATE =
|
||||
/\/[/*][*\s]*(?:={3}|-{3}|─{3}|━{3}|\*{3}|~{3})|\/\/[^\n]*(?:[;{}]|=>|\b(?:const|let|return|await|import|export)\b|\w\.\w+\()/
|
||||
/\/[/*][*\s]*(?:={3}|-{3}|─{3}|━{3}|\*{3}|~{3}|eslint-(?:disable|enable))|\/\/[^\n]*(?:[;{}]|=>|\b(?:const|let|return|await|import|export)\b|\w\.\w+\()/
|
||||
|
||||
/** Every banner and commented-out-code hit in one source file. */
|
||||
/** Every comment-hygiene hit in one source file. */
|
||||
export function findViolations(file: string, source: string): Violation[] {
|
||||
if (!MAY_VIOLATE.test(source)) return []
|
||||
const jsx = /\.[jt]sx$/.test(file)
|
||||
@@ -183,6 +190,12 @@ export function findViolations(file: string, source: string): Violation[] {
|
||||
|
||||
for (const comment of comments) {
|
||||
const line = comment.loc?.start.line ?? 0
|
||||
const directive = comment.value.replace(/^\*+/, '').trim()
|
||||
if (ESLINT_DIRECTIVE.test(directive)) {
|
||||
violations.push({ rule: 'eslint-directive', line, text: directive })
|
||||
previous = undefined
|
||||
continue
|
||||
}
|
||||
if (comment.type === 'CommentBlock') {
|
||||
const singleLine = comment.loc?.start.line === comment.loc?.end.line
|
||||
const text = comment.value.replace(/^\*+/, '').trim()
|
||||
@@ -253,6 +266,9 @@ const FIX: Record<Rule, string> = {
|
||||
'commented-out-code':
|
||||
'delete the commented-out code — git keeps the history. If it is a deliberate code sample, ' +
|
||||
'put it in a TSDoc `@example` or add `// comment-hygiene-allow: <reason>` to the group',
|
||||
'eslint-directive':
|
||||
'delete it — nothing runs ESLint. Keep any reason it carried as a plain `// ...` why; ' +
|
||||
'to waive a Biome rule, use `// biome-ignore <rule>: <reason>`',
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
@@ -270,7 +286,7 @@ function main(): void {
|
||||
}
|
||||
|
||||
if (hits.length === 0) {
|
||||
console.log('✓ comment hygiene (no banners or commented-out code)')
|
||||
console.log('✓ comment hygiene (no banners, commented-out code, or ESLint directives)')
|
||||
return
|
||||
}
|
||||
console.error(hits.join('\n'))
|
||||
|
||||
@@ -57,72 +57,6 @@
|
||||
"apps/desktop/src/main/window.ts#fitBoundsToWorkArea",
|
||||
"apps/desktop/src/main/window.ts#sanitizeBounds",
|
||||
"apps/desktop/src/main/windows.ts#registerPopupContents",
|
||||
"apps/docs/components/icons.tsx#AgentSkillsIcon",
|
||||
"apps/docs/components/icons.tsx#AnthropicIcon",
|
||||
"apps/docs/components/icons.tsx#AtlassianIcon",
|
||||
"apps/docs/components/icons.tsx#BasetenIcon",
|
||||
"apps/docs/components/icons.tsx#BedrockIcon",
|
||||
"apps/docs/components/icons.tsx#BigQueryIcon",
|
||||
"apps/docs/components/icons.tsx#CalendarIcon",
|
||||
"apps/docs/components/icons.tsx#CerebrasIcon",
|
||||
"apps/docs/components/icons.tsx#CohereIcon",
|
||||
"apps/docs/components/icons.tsx#CrewAIIcon",
|
||||
"apps/docs/components/icons.tsx#DeepseekIcon",
|
||||
"apps/docs/components/icons.tsx#DustIcon",
|
||||
"apps/docs/components/icons.tsx#FalIcon",
|
||||
"apps/docs/components/icons.tsx#FireworksIcon",
|
||||
"apps/docs/components/icons.tsx#FlowiseIcon",
|
||||
"apps/docs/components/icons.tsx#GeminiIcon",
|
||||
"apps/docs/components/icons.tsx#GithubOutlineIcon",
|
||||
"apps/docs/components/icons.tsx#GitlabIcon",
|
||||
"apps/docs/components/icons.tsx#GoogleChatIcon",
|
||||
"apps/docs/components/icons.tsx#GroqIcon",
|
||||
"apps/docs/components/icons.tsx#GumloopIcon",
|
||||
"apps/docs/components/icons.tsx#InputIcon",
|
||||
"apps/docs/components/icons.tsx#InstagramIcon",
|
||||
"apps/docs/components/icons.tsx#KieIcon",
|
||||
"apps/docs/components/icons.tsx#KimiIcon",
|
||||
"apps/docs/components/icons.tsx#LangChainIcon",
|
||||
"apps/docs/components/icons.tsx#LangflowIcon",
|
||||
"apps/docs/components/icons.tsx#LitellmIcon",
|
||||
"apps/docs/components/icons.tsx#LucidIcon",
|
||||
"apps/docs/components/icons.tsx#MailIcon",
|
||||
"apps/docs/components/icons.tsx#MakeIcon",
|
||||
"apps/docs/components/icons.tsx#MessagesIcon",
|
||||
"apps/docs/components/icons.tsx#MetaIcon",
|
||||
"apps/docs/components/icons.tsx#MicrosoftCopilotIcon",
|
||||
"apps/docs/components/icons.tsx#MicrosoftIcon",
|
||||
"apps/docs/components/icons.tsx#N8nIcon",
|
||||
"apps/docs/components/icons.tsx#NotificationsIcon",
|
||||
"apps/docs/components/icons.tsx#NvidiaIcon",
|
||||
"apps/docs/components/icons.tsx#OllamaIcon",
|
||||
"apps/docs/components/icons.tsx#OpenClawIcon",
|
||||
"apps/docs/components/icons.tsx#OpenRouterIcon",
|
||||
"apps/docs/components/icons.tsx#PipedreamIcon",
|
||||
"apps/docs/components/icons.tsx#RetoolIcon",
|
||||
"apps/docs/components/icons.tsx#SMSIcon",
|
||||
"apps/docs/components/icons.tsx#SOC2BadgeIcon",
|
||||
"apps/docs/components/icons.tsx#SSHIcon",
|
||||
"apps/docs/components/icons.tsx#SakanaIcon",
|
||||
"apps/docs/components/icons.tsx#ScheduleIcon",
|
||||
"apps/docs/components/icons.tsx#ServerIcon",
|
||||
"apps/docs/components/icons.tsx#SignalIcon",
|
||||
"apps/docs/components/icons.tsx#SimAutoIcon",
|
||||
"apps/docs/components/icons.tsx#SimDeploymentsIcon",
|
||||
"apps/docs/components/icons.tsx#SlackMonoIcon",
|
||||
"apps/docs/components/icons.tsx#SpotifyIcon",
|
||||
"apps/docs/components/icons.tsx#SshTerminalIcon",
|
||||
"apps/docs/components/icons.tsx#StackAIIcon",
|
||||
"apps/docs/components/icons.tsx#TinesIcon",
|
||||
"apps/docs/components/icons.tsx#TogetherIcon",
|
||||
"apps/docs/components/icons.tsx#TypeSafeIcon",
|
||||
"apps/docs/components/icons.tsx#VellumIcon",
|
||||
"apps/docs/components/icons.tsx#VertexIcon",
|
||||
"apps/docs/components/icons.tsx#VllmIcon",
|
||||
"apps/docs/components/icons.tsx#WorkatoIcon",
|
||||
"apps/docs/components/icons.tsx#ZaiIcon",
|
||||
"apps/docs/components/icons.tsx#ZapierIcon",
|
||||
"apps/docs/components/icons.tsx#xAIIcon",
|
||||
"apps/docs/components/workflow-preview/index.ts#PreviewBlock",
|
||||
"apps/docs/components/workflow-preview/index.ts#PreviewTool",
|
||||
"apps/docs/components/workflow-preview/index.ts#PreviewWorkflow",
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
* Runs knip once for the whole dead-code audit: zero tolerance for unreachable files and
|
||||
* dependency drift, and a shrink-only ratchet for unused exports.
|
||||
*
|
||||
* `knip.jsonc` scopes plain `knip` (`check:dead-code`) to files, dependencies, unlisted, and
|
||||
* unresolved, because thousands of pre-existing unused exports would otherwise drown the report.
|
||||
* That left exports unguarded: an agent could export a helper nothing imports, or keep a dead
|
||||
* export alive after deleting its last caller, and nothing objected. This script adds knip's
|
||||
* `exports`, `types`, and `duplicates` issues on top of the same pass:
|
||||
* `knip.jsonc` scopes plain `knip` to files, dependencies, unlisted, and unresolved, because
|
||||
* thousands of pre-existing unused exports would otherwise drown the report. That left exports
|
||||
* unguarded: an agent could export a helper nothing imports, or keep a dead export alive after
|
||||
* deleting its last caller, and nothing objected. This script adds knip's `exports`, `types`,
|
||||
* and `duplicates` issues on top of the same pass:
|
||||
*
|
||||
* - Every other issue knip reports must be empty: `files`, `unlisted`, `unresolved`, and every
|
||||
* dependency type the `dependencies` include expands to (`devDependencies`,
|
||||
@@ -23,8 +23,7 @@
|
||||
* files entries. An export whose only consumer knip cannot see (a path-based `import()`, or a
|
||||
* helper an audit names as the replacement) carries a `@public` TSDoc tag saying why.
|
||||
*
|
||||
* Knip is slow, so `run-audits.ts` runs this script and skips `check:dead-code`, which stays
|
||||
* available as the human-readable report.
|
||||
* Knip is slow, so this is the only audit that runs it; `check:audits` runs this script.
|
||||
*
|
||||
* Run: `bun run check:unused-exports`
|
||||
*/
|
||||
@@ -116,7 +115,7 @@ if (strict.length) {
|
||||
console.error(
|
||||
'\nDelete unreachable files, declare or remove dependencies, and fix unresolved imports. ' +
|
||||
'If knip cannot see a real entry point, add it to knip.jsonc with a comment saying why. ' +
|
||||
'Details: bun run check:dead-code'
|
||||
'Details: bun run node_modules/knip/bin/knip.js'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ const EXCLUDED: Record<string, string> = {
|
||||
'check:audits': 'this runner',
|
||||
'check:migrations': 'diffs against a git base ref (origin/staging by default)',
|
||||
'check:api-validation': 'superseded by the :strict variant, which this runner does run',
|
||||
'check:dead-code': 'check:unused-exports gates the same issues in its single knip pass',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user