chore(knip): add mdx support clean unused code (#37882)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Stephen Zhou 2026-06-24 20:30:11 +08:00 committed by GitHub
parent 2eb564696e
commit 1b81ac033f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 189 additions and 234 deletions

View File

@ -109,6 +109,10 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
run: vp run knip:production
- name: Web production unused declarations check
if: steps.changed-files.outputs.any_changed == 'true'
run: vp run knip:production-unused-check
ts-common-style:
name: TS Common
runs-on: depot-ubuntu-24.04

View File

@ -3274,7 +3274,7 @@
},
"web/app/components/develop/code.tsx": {
"ts/no-explicit-any": {
"count": 7
"count": 6
}
},
"web/app/components/develop/doc.tsx": {
@ -3286,9 +3286,6 @@
"jsx-a11y/no-redundant-roles": {
"count": 1
},
"ts/no-empty-object-type": {
"count": 1
},
"ts/no-explicit-any": {
"count": 2
}
@ -7227,7 +7224,7 @@
"count": 1
},
"ts/no-explicit-any": {
"count": 13
"count": 9
}
},
"web/service/datasets.ts": {

View File

@ -1,6 +1,6 @@
import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Code, CodeGroup, Embed, Pre } from '../code'
import { CodeGroup, Embed } from '../code'
vi.mock('@/utils/clipboard', () => ({
writeTextToClipboard: vi.fn().mockResolvedValue(undefined),
@ -21,31 +21,6 @@ describe('code.tsx components', () => {
vi.restoreAllMocks()
})
describe('Code', () => {
it('should render children as a code element', () => {
render(<Code>const x = 1</Code>)
const codeElement = screen.getByText('const x = 1')
expect(codeElement.tagName).toBe('CODE')
})
it('should pass through additional props', () => {
render(<Code data-testid="custom-code" className="custom-class">snippet</Code>)
const codeElement = screen.getByTestId('custom-code')
expect(codeElement).toHaveClass('custom-class')
})
it('should render with complex children', () => {
render(
<Code>
<span>part1</span>
<span>part2</span>
</Code>,
)
expect(screen.getByText('part1')).toBeInTheDocument()
expect(screen.getByText('part2')).toBeInTheDocument()
})
})
describe('Embed', () => {
it('should render value prop as a span element', () => {
render(<Embed value="embedded content">ignored children</Embed>)
@ -277,28 +252,6 @@ describe('code.tsx components', () => {
})
})
describe('Pre', () => {
it('should wrap children in CodeGroup when outside CodeGroup context', () => {
render(
<Pre title="Pre Title">
<pre><code>code</code></pre>
</Pre>,
)
expect(screen.getByText('Pre Title')).toBeInTheDocument()
})
it('should return children directly when inside CodeGroup context', () => {
render(
<CodeGroup targetCode="outer code">
<Pre>
<code>inner code</code>
</Pre>
</CodeGroup>,
)
expect(screen.getByText('outer code')).toBeInTheDocument()
})
})
describe('CodePanelHeader (via CodeGroup)', () => {
it('should render when tag is provided', () => {
render(

View File

@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react'
import { Col, Heading, Properties, Property, PropertyInstruction, Row, SubProperty } from '../md'
import { Col, Heading, Properties, Property, Row, SubProperty } from '../md'
describe('md.tsx components', () => {
describe('Heading', () => {
@ -540,67 +540,6 @@ describe('md.tsx components', () => {
})
})
describe('PropertyInstruction', () => {
it('should render children', () => {
render(
<PropertyInstruction>
This is an instruction
</PropertyInstruction>,
)
expect(screen.getByText('This is an instruction')).toBeInTheDocument()
})
it('should render as li element', () => {
const { container } = render(
<PropertyInstruction>
Instruction text
</PropertyInstruction>,
)
expect(container.querySelector('li')).toBeInTheDocument()
})
it('should have m-0 class', () => {
const { container } = render(
<PropertyInstruction>
Instruction
</PropertyInstruction>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('m-0')
})
it('should have padding classes', () => {
const { container } = render(
<PropertyInstruction>
Instruction
</PropertyInstruction>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('px-0')
expect(li.className).toContain('py-4')
})
it('should have italic class', () => {
const { container } = render(
<PropertyInstruction>
Instruction
</PropertyInstruction>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('italic')
})
it('should have first:pt-0 class', () => {
const { container } = render(
<PropertyInstruction>
Instruction
</PropertyInstruction>,
)
const li = container.querySelector('li')!
expect(li.className).toContain('first:pt-0')
})
})
describe('integration tests', () => {
it('should render Property inside Properties', () => {
render(
@ -635,21 +574,5 @@ describe('md.tsx components', () => {
expect(screen.getByText('Left column')).toBeInTheDocument()
expect(screen.getByText('Right column')).toBeInTheDocument()
})
it('should render PropertyInstruction inside Properties', () => {
render(
<Properties anchor={false}>
<PropertyInstruction>
Note: All fields are required
</PropertyInstruction>
<Property name="required_field" type="string" anchor={false}>
A required field
</Property>
</Properties>,
)
expect(screen.getByText('Note: All fields are required')).toBeInTheDocument()
expect(screen.getByText('required_field')).toBeInTheDocument()
})
})
})

View File

@ -1,5 +1,5 @@
'use client'
import type { PropsWithChildren, ReactElement, ReactNode } from 'react'
import type { PropsWithChildren, ReactElement } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import {
Tabs,
@ -9,8 +9,6 @@ import {
} from '@langgenius/dify-ui/tabs'
import {
Children,
createContext,
use,
useEffect,
useRef,
useState,
@ -269,8 +267,6 @@ function useTabGroupProps(tabValues: string[]) {
}
}
const CodeGroupContext = createContext(false)
type CodeGroupProps = PropsWithChildren<{
/** Code example(s) to display */
targetCode?: string | CodeExample[]
@ -297,42 +293,20 @@ export function CodeGroup({ children, title, targetCode, ...props }: CodeGroupPr
</>
)
return (
<CodeGroupContext.Provider value={true}>
{hasTabs
? (
<Tabs
{...tabGroupProps}
className="not-prose my-6 overflow-hidden rounded-2xl bg-zinc-900 shadow-md dark:ring-1 dark:ring-white/10"
>
{content}
</Tabs>
)
: (
<div className="not-prose my-6 overflow-hidden rounded-2xl bg-zinc-900 shadow-md dark:ring-1 dark:ring-white/10">
{content}
</div>
)}
</CodeGroupContext.Provider>
)
}
type IChildProps = {
children: ReactNode
[key: string]: any
}
export function Code({ children, ...props }: IChildProps) {
return <code {...props}>{children}</code>
}
export function Pre({ children, ...props }: IChildrenProps) {
const isGrouped = use(CodeGroupContext)
if (isGrouped)
return children
return <CodeGroup {...props}>{children}</CodeGroup>
return hasTabs
? (
<Tabs
{...tabGroupProps}
className="not-prose my-6 overflow-hidden rounded-2xl bg-zinc-900 shadow-md dark:ring-1 dark:ring-white/10"
>
{content}
</Tabs>
)
: (
<div className="not-prose my-6 overflow-hidden rounded-2xl bg-zinc-900 shadow-md dark:ring-1 dark:ring-white/10">
{content}
</div>
)
}
export function Embed({ value, ...props }: IChildrenProps) {

View File

@ -1,5 +1,4 @@
'use client'
import type { PropsWithChildren } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
type IChildrenProps = {
@ -140,9 +139,3 @@ export function SubProperty({ name, type, children }: ISubProperty) {
</li>
)
}
export function PropertyInstruction({ children }: PropsWithChildren<{ }>) {
return (
<li className="m-0 px-0 py-4 italic first:pt-0">{children}</li>
)
}

View File

@ -139,7 +139,7 @@ async function mountedStore() {
},
})
store.set(queryClientAtom, queryClient)
const unsubscribe = store.sub(state.createReleaseFormValuesAtom, () => undefined)
const unsubscribe = store.sub(state.createReleaseFormIsSubmittingAtom, () => undefined)
return {
state,

View File

@ -126,7 +126,7 @@ async function mountedStore() {
},
})
store.set(queryClientAtom, queryClient)
const unsubscribe = store.sub(state.createReleaseFormValuesAtom, () => undefined)
const unsubscribe = store.sub(state.createReleaseFormIsSubmittingAtom, () => undefined)
return {
queryClient,
@ -232,20 +232,6 @@ describe('create release state', () => {
}
})
it('should keep default form values before editing', async () => {
const { state, store, unsubscribe } = await mountedStore()
expect(store.get(state.createReleaseFormValuesAtom)).toEqual({
dslFile: undefined,
releaseDescription: '',
releaseName: '',
releaseSourceMode: 'sourceApp',
sourceApp: undefined,
})
unsubscribe()
})
it('should validate release name only when submitting', async () => {
const { state, store, unsubscribe } = await mountedStore()

View File

@ -106,7 +106,6 @@ export const createReleaseFormAtom = atomWithForm({
const createReleaseFormAtoms = createFormAtoms(createReleaseFormAtom)
export const createReleaseFormValuesAtom = createReleaseFormAtoms.valuesAtom
export const createReleaseFormIsSubmittingAtom = createReleaseFormAtoms.isSubmittingAtom
export const createReleaseSourceModeFieldAtom = createReleaseFormAtoms.fieldAtom('releaseSourceMode')
export const createReleaseSourceAppFieldAtom = createReleaseFormAtoms.fieldAtom('sourceApp')

View File

@ -4,6 +4,9 @@ import type { KnipConfig } from 'knip'
* @see https://knip.dev/reference/configuration
*/
const config: KnipConfig = {
compilers: {
mdx: true,
},
entry: [
'scripts/**/*.{js,ts,mjs}',
'bin/**/*.{js,ts,mjs}',
@ -11,12 +14,12 @@ const config: KnipConfig = {
'dev-proxy.config.ts',
],
project: [
'**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts,css}!',
'**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts,css,mdx}!',
'!**/*.{bench,test,test-d,spec,spec-d}.?(c|m)[jt]s?(x)!',
'!**/*.test-utils.{ts,tsx}!',
'!**/__mocks__/**!',
'!**/__tests__/**!',
'!**/*.stories.{js,jsx,ts,tsx}!',
'!**/*.stories.{js,jsx,ts,tsx,mdx}!',
'!.storybook/**!',
'!context/provider-context-mock.tsx!',
'!eslint.constants.mjs!',

View File

@ -93,11 +93,6 @@ export type CreateApiKeyResponse = {
created_at: string
}
export type ValidateOpenAIKeyResponse = {
result: string
error?: string
}
export type AppVoicesListResponse = [{
name: string
value: string

View File

@ -35,6 +35,7 @@
"i18n:check": "tsx ./scripts/check-i18n.js",
"knip": "knip",
"knip:production": "knip --production --include files",
"knip:production-unused-check": "node ./scripts/check-production-unused-after-knip-fix.mjs",
"lint:tss": "tsslint --project tsconfig.json",
"preinstall": "npx only-allow pnpm",
"refactor-component": "node ./scripts/refactor-component.js",

View File

@ -0,0 +1,157 @@
import { spawn } from 'node:child_process'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(scriptDir, '../..')
const webDir = path.join(repoRoot, 'web')
function commandName(name) {
return process.platform === 'win32' ? `${name}.cmd` : name
}
function run(command, args, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: options.cwd,
env: process.env,
shell: false,
})
let stdout = ''
let stderr = ''
child.stdout.on('data', data => stdout += data)
child.stderr.on('data', data => stderr += data)
child.on('error', reject)
child.on('close', status => resolve({ status, stdout, stderr }))
})
}
function parseVpLintJson(stdout) {
const jsonStart = stdout.indexOf('{')
if (jsonStart === -1)
return { diagnostics: [] }
return JSON.parse(stdout.slice(jsonStart))
}
function relativeDiagnostic(diagnostic) {
const label = diagnostic.labels?.[0]
const diagnosticFile = label?.file ?? diagnostic.filename
const filePath = diagnosticFile
? path.relative(webDir, path.isAbsolute(diagnosticFile) ? diagnosticFile : path.join(webDir, diagnosticFile))
: '<unknown>'
const span = label?.span ? `:${label.span.line}:${label.span.column}` : ''
return `${filePath}${span} ${diagnostic.code ?? ''} ${diagnostic.message ?? ''}`.trim()
}
async function ensureCleanWorktree() {
const result = await run('git', ['status', '--porcelain=v1'], { cwd: repoRoot })
if (result.status !== 0) {
process.stderr.write(result.stderr)
throw new Error('Failed to check git status.')
}
if (!result.stdout.trim())
return
console.error('This check runs knip --fix and must start from a clean worktree.')
console.error('Commit or stash your changes first, then run it again.')
console.error(result.stdout)
process.exit(1)
}
async function restoreWorktree() {
const restoreResult = await run('git', ['restore', '--staged', '--worktree', '.'], { cwd: repoRoot })
if (restoreResult.status !== 0) {
process.stdout.write(restoreResult.stdout)
process.stderr.write(restoreResult.stderr)
throw new Error('Failed to restore tracked files after knip --fix.')
}
const cleanResult = await run('git', ['clean', '-fd', '--', 'web', '.eslintcache'], { cwd: repoRoot })
if (cleanResult.status !== 0) {
process.stdout.write(cleanResult.stdout)
process.stderr.write(cleanResult.stderr)
throw new Error('Failed to clean untracked files after knip --fix.')
}
}
const knip = path.join(webDir, 'node_modules', '.bin', commandName('knip'))
const vp = path.join(repoRoot, 'node_modules', '.bin', commandName('vp'))
let shouldRestore = false
let hasUnusedMessages = false
try {
await ensureCleanWorktree()
shouldRestore = true
console.log('Running knip --production --fix...')
const knipResult = await run(knip, ['--production', '--fix'], { cwd: webDir })
if (knipResult.status !== 0) {
process.stdout.write(knipResult.stdout)
process.stderr.write(knipResult.stderr)
throw new Error('knip --production --fix failed.')
}
console.log('Running Vite+ unused checks after knip --fix...')
const lintResult = await run(vp, [
'lint',
'-A',
'all',
'-D',
'no-unused-vars',
'--format',
'json',
'--ignore-pattern',
'public/**',
'--ignore-pattern',
'coverage/**',
'--ignore-pattern',
'.next/**',
'--ignore-pattern',
'**/__tests__/**',
'--ignore-pattern',
'**/*.spec.ts',
'--ignore-pattern',
'**/*.spec.tsx',
'--ignore-pattern',
'**/*.test.ts',
'--ignore-pattern',
'**/*.test.tsx',
'.',
], { cwd: webDir })
let lintOutput
try {
lintOutput = parseVpLintJson(lintResult.stdout)
}
catch {
process.stdout.write(lintResult.stdout)
process.stderr.write(lintResult.stderr)
throw new Error('Failed to parse Vite+ lint JSON output.')
}
const unusedMessages = (lintOutput.diagnostics ?? []).map(relativeDiagnostic)
if (unusedMessages.length > 0) {
hasUnusedMessages = true
console.error('Unused declarations remain after applying knip --production --fix.')
console.error('Remove these declarations; if they are only referenced by tests, remove the matching tests too.')
for (const message of unusedMessages)
console.error(message)
}
else {
console.log('No Vite+ unused declarations remain after knip --production --fix.')
}
}
finally {
if (shouldRestore) {
console.log('Restoring checkout after knip --fix...')
await restoreWorktree()
}
}
if (hasUnusedMessages)
process.exit(1)

View File

@ -2,13 +2,9 @@ import type {
DefaultModelResponse,
Model,
ModelItem,
ModelLoadBalancingConfig,
ModelParameterRule,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type {
ValidateOpenAIKeyResponse,
} from '@/models/app'
import type {
CommonResponse,
ICurrentWorkspace,
@ -104,13 +100,7 @@ export const invitationCheck = ({ url, params }: { url: string, params: { worksp
export const activateMember = ({ url, body }: { url: string, body: ActivateMemberBody }): Promise<LoginResponse> => {
return post<LoginResponse>(url, { body })
}
export type ModelProviderCredentials = {
credentials?: Record<string, string | undefined | boolean>
load_balancing: ModelLoadBalancingConfig
}
export const fetchModelProviderCredentials = (url: string): Promise<ModelProviderCredentials> => {
return get<ModelProviderCredentials>(url)
}
export const fetchModelProviderModelList = (url: string): Promise<{ data: ModelItem[] }> => {
return get<{ data: ModelItem[] }>(url)
}
@ -119,26 +109,6 @@ export const fetchModelList = (url: string): Promise<{ data: Model[] }> => {
return get<{ data: Model[] }>(url)
}
export const validateModelProvider = ({ url, body }: { url: string, body: any }): Promise<ValidateOpenAIKeyResponse> => {
return post<ValidateOpenAIKeyResponse>(url, { body })
}
export const validateModelLoadBalancingCredentials = ({ url, body }: { url: string, body: any }): Promise<ValidateOpenAIKeyResponse> => {
return post<ValidateOpenAIKeyResponse>(url, { body })
}
export const setModelProvider = ({ url, body }: { url: string, body: any }): Promise<CommonResponse> => {
return post<CommonResponse>(url, { body })
}
export const deleteModelProvider = ({ url, body }: { url: string, body?: any }): Promise<CommonResponse> => {
return del<CommonResponse>(url, { body })
}
export const getPayUrl = (url: string): Promise<{ url: string }> => {
return get<{ url: string }>(url)
}
export const fetchDefaultModal = (url: string): Promise<{ data: DefaultModelResponse }> => {
return get<{ data: DefaultModelResponse }>(url)
}