mirror of
https://github.com/langgenius/dify.git
synced 2026-11-10 21:35:45 +08:00
chore(web): guard handwritten requests with native lint rules (#44436)
This commit is contained in:
1 parent
8197cb6503
commit
201f46f19a
6 files changed
+1240
-217
No files matched your search
+710
-3
File diff suppressed because it is too large.
Load diff
@@ -156,6 +156,41 @@ Explain the concrete reason after `--`: which external contract, lifecycle, or r
|
||||
|
||||
`reportUnusedDisableDirectives` runs at `error` repository-wide. Remove an exception when the finding no longer exists. Keep both checks active: a described disable may still be unused, and a used disable may still lack a reason.
|
||||
|
||||
### Handwritten HTTP Calls
|
||||
|
||||
Web uses native Oxlint rules to discourage deprecated APIs and new handwritten requests:
|
||||
|
||||
- `typescript/no-deprecated` rejects references to deprecated APIs. Legacy base/fetch
|
||||
transport entry points carry `@deprecated`; generated `consoleClient`, `consoleQuery`,
|
||||
and marketplace clients remain allowed, including direct calls from UI components.
|
||||
- `no-restricted-globals` and `no-restricted-properties` reject direct global
|
||||
`fetch` / `XMLHttpRequest` references, including `window`, `globalThis`, `global`,
|
||||
and `self` property access.
|
||||
- `no-restricted-imports` rejects new value imports from common HTTP packages
|
||||
(including Ky, Axios, Undici and Node http/https). Type-only imports remain allowed.
|
||||
Existing legacy-service and localStorage import/access restrictions remain active.
|
||||
|
||||
`typescript/no-deprecated` applies throughout Web, including tests, mocks, stories,
|
||||
test infrastructure, and maintenance scripts. The raw transport and HTTP-client import
|
||||
restrictions apply only to production code. Repository-wide generated/vendor and
|
||||
declaration-file ignores still apply. There is no custom API rule or data-flow tracker.
|
||||
|
||||
Existing findings are recorded in `oxlint-suppressions.json`. These are counts of
|
||||
references/imports, not endpoints awaiting migration: they include transport internals,
|
||||
assets, special transports, and unrelated deprecated APIs reported by `no-deprecated`.
|
||||
Prune counts when removing existing findings; do not raise them for new work.
|
||||
For a necessary new special transport, use a scoped `oxlint-disable-next-line`
|
||||
for the applicable native rule with a concrete contract reason.
|
||||
|
||||
The simpler native checks intentionally accept gaps: for a deprecated arrow-function
|
||||
helper, current Oxlint reports `const saved = get` at assignment but does not propagate
|
||||
deprecation to later `saved()` calls. Adding calls through an existing alias,
|
||||
HTTP-client import, or XHR instance may therefore pass its existing baseline.
|
||||
Dynamic property access and indirect loading also require review. A per-file count
|
||||
cannot detect replacing one old use with another at the same count. Do not treat
|
||||
passing lint as proof that all requests use generated contracts. This rule set
|
||||
does not enforce Query ownership.
|
||||
|
||||
### Translation Function Types
|
||||
|
||||
`dify/require-i18n-namespace` requires translation hook calls to use non-empty
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { expect, it } from 'vite-plus/test'
|
||||
import { lintConfig } from './lint.config.ts'
|
||||
|
||||
type Diagnostic = { code?: string; filename: string; labels: { span: { line: number } }[] }
|
||||
const nativeRules = new Set([
|
||||
'typescript/no-deprecated',
|
||||
'no-restricted-globals',
|
||||
'no-restricted-properties',
|
||||
'no-restricted-imports',
|
||||
])
|
||||
|
||||
function fixture() {
|
||||
const require = createRequire(import.meta.url)
|
||||
const viteRequire = createRequire(require.resolve('vite-plus/package.json'))
|
||||
const oxlintBin = join(dirname(dirname(viteRequire.resolve('oxlint'))), 'bin', 'oxlint')
|
||||
const oxlintRequire = createRequire(oxlintBin)
|
||||
const tsgolintRequire = createRequire(oxlintRequire.resolve('oxlint-tsgolint/package.json'))
|
||||
const tsgolint = tsgolintRequire.resolve(
|
||||
`@oxlint-tsgolint/${process.platform}-${process.arch}/tsgolint${process.platform === 'win32' ? '.exe' : ''}`,
|
||||
)
|
||||
const directory = mkdtempSync(join(tmpdir(), 'dify-native-http-'))
|
||||
writeFileSync(
|
||||
join(directory, 'tsconfig.json'),
|
||||
JSON.stringify({
|
||||
compilerOptions: { target: 'ESNext', module: 'ESNext', lib: ['ESNext', 'DOM'], strict: true },
|
||||
include: ['web/**/*.ts', 'web/**/*.tsx'],
|
||||
}),
|
||||
)
|
||||
mkdirSync(join(directory, 'web/service'), { recursive: true })
|
||||
writeFileSync(
|
||||
join(directory, 'web/service/legacy.ts'),
|
||||
'/** @deprecated Use generated clients. */\nexport const get = (url: string) => Promise.resolve(url)\n',
|
||||
)
|
||||
const select = (rules: Record<string, unknown>) =>
|
||||
Object.fromEntries(Object.entries(rules).filter(([name]) => nativeRules.has(name)))
|
||||
writeFileSync(
|
||||
join(directory, '.oxlintrc.json'),
|
||||
JSON.stringify({
|
||||
categories: { correctness: 'off' },
|
||||
options: { typeAware: true },
|
||||
rules: select(lintConfig.rules),
|
||||
overrides: lintConfig.overrides.map((override) => ({
|
||||
...override,
|
||||
rules: select(override.rules),
|
||||
})),
|
||||
}),
|
||||
)
|
||||
return {
|
||||
close: () => rmSync(directory, { recursive: true, force: true }),
|
||||
lint(source: string, file = 'web/feature.ts', flags: string[] = []) {
|
||||
mkdirSync(dirname(join(directory, file)), { recursive: true })
|
||||
writeFileSync(join(directory, file), source)
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[oxlintBin, '--config', '.oxlintrc.json', '--format', 'json', ...flags, file],
|
||||
{
|
||||
cwd: directory,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, OXLINT_TSGOLINT_PATH: tsgolint },
|
||||
},
|
||||
)
|
||||
expect(result.error).toBeUndefined()
|
||||
expect([0, 1], result.stderr + result.stdout).toContain(result.status)
|
||||
return {
|
||||
status: result.status,
|
||||
diagnostics: JSON.parse(result.stdout).diagnostics as Diagnostic[],
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
it('rejects each direct deprecated helper call and allows generated clients and local functions', () => {
|
||||
const test = fixture()
|
||||
try {
|
||||
const result = test.lint(
|
||||
[
|
||||
"import { get as read } from './service/legacy'",
|
||||
"import * as legacy from './service/legacy'",
|
||||
"read('/one')",
|
||||
"legacy.get('/two')",
|
||||
'const consoleClient = { get: (url: string) => Promise.resolve(url) }',
|
||||
"consoleClient.get('/allowed')",
|
||||
'const fetch = (url: string) => url',
|
||||
"fetch('/local')",
|
||||
].join('\n'),
|
||||
)
|
||||
expect(result.diagnostics.map((d) => [d.code, d.labels[0]!.span.line])).toEqual([
|
||||
['typescript(no-deprecated)', 3],
|
||||
['typescript(no-deprecated)', 4],
|
||||
])
|
||||
} finally {
|
||||
test.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('blocks native requests and client imports without blocking type-only imports', () => {
|
||||
const test = fixture()
|
||||
try {
|
||||
const result = test.lint(
|
||||
[
|
||||
"import ky from 'ky'",
|
||||
"import type { Options } from 'ky'",
|
||||
"fetch('/one')",
|
||||
"window.fetch('/two')",
|
||||
"globalThis['fetch']('/three')",
|
||||
'new XMLHttpRequest()',
|
||||
'new self.XMLHttpRequest()',
|
||||
"import { request } from 'node:https'",
|
||||
].join('\n'),
|
||||
)
|
||||
expect(result.diagnostics.map((d) => d.labels[0]!.span.line).sort((a, b) => a - b)).toEqual([
|
||||
1, 3, 4, 5, 6, 7, 8,
|
||||
])
|
||||
} finally {
|
||||
test.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves storage and service import restrictions alongside transport restrictions', () => {
|
||||
const test = fixture()
|
||||
try {
|
||||
const result = test.lint(
|
||||
"import { get } from './base'\nlocalStorage.getItem('key')\nwindow.localStorage.getItem('key')\nfetch('/one')",
|
||||
'web/service/feature.ts',
|
||||
)
|
||||
expect(result.diagnostics.map((d) => d.labels[0]!.span.line).sort((a, b) => a - b)).toEqual([
|
||||
1, 2, 3, 4,
|
||||
])
|
||||
expect(test.lint("fetch('/one')", 'web/instrumentation-client.ts').diagnostics).toHaveLength(1)
|
||||
} finally {
|
||||
test.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('checks deprecations in all Web scopes while allowing test and script transports', () => {
|
||||
const test = fixture()
|
||||
try {
|
||||
for (const file of [
|
||||
'web/feature.spec.ts',
|
||||
'web/feature.test.tsx',
|
||||
'web/feature.stories.tsx',
|
||||
'web/__tests__/http.ts',
|
||||
'web/__mocks__/http.ts',
|
||||
'web/scripts/check.ts',
|
||||
'web/test/helper.ts',
|
||||
'web/vitest.setup.ts',
|
||||
'web/vitest.browser.setup.ts',
|
||||
]) {
|
||||
const source = [
|
||||
"import ky from 'ky'",
|
||||
"fetch('/allowed')",
|
||||
'/** @deprecated Use the replacement. */',
|
||||
'function legacy() {}',
|
||||
'legacy()',
|
||||
].join('\n')
|
||||
expect(
|
||||
test.lint(source, file).diagnostics.map((d) => [d.code, d.labels[0]!.span.line]),
|
||||
).toEqual([['typescript(no-deprecated)', 5]])
|
||||
}
|
||||
} finally {
|
||||
test.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects additional direct calls beyond the existing suppression count', () => {
|
||||
const test = fixture()
|
||||
try {
|
||||
const source = "import { get } from './service/legacy'\nget('/existing')\nfetch('/existing')"
|
||||
expect(test.lint(source, undefined, ['--suppress-all']).status).toBe(0)
|
||||
expect(test.lint(`${source}\nget('/new')\nfetch('/new')`).status).toBe(1)
|
||||
} finally {
|
||||
test.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('documents the accepted Oxlint gap for calls through an already baselined alias', () => {
|
||||
const test = fixture()
|
||||
try {
|
||||
const source = "import { get } from './service/legacy'\nconst saved = get\nsaved('/existing')"
|
||||
expect(test.lint(source).diagnostics.map((d) => d.labels[0]!.span.line)).toEqual([2])
|
||||
expect(test.lint(source, undefined, ['--suppress-all']).status).toBe(0)
|
||||
// Change this expectation and the documentation when Oxlint gains signature propagation.
|
||||
expect(test.lint(`${source}\nsaved('/new')`).diagnostics).toEqual([])
|
||||
} finally {
|
||||
test.close()
|
||||
}
|
||||
})
|
||||
+290
-214
@@ -10,6 +10,242 @@ const webPackageJson = path.resolve(rootDir, 'web/package.json')
|
||||
const difyUiPackageJson = path.resolve(rootDir, 'packages/dify-ui/package.json')
|
||||
const enableTailwindCanonicalClasses = process.env.TAILWIND_CANONICAL_CLASSES === 'true'
|
||||
|
||||
const handwrittenApiMessage =
|
||||
'Use generated consoleClient/consoleQuery APIs. New special transports need a scoped lint exception with a concrete reason.'
|
||||
const rawHttpGlobals = ['fetch', 'XMLHttpRequest'].map((name) => ({
|
||||
name,
|
||||
message: handwrittenApiMessage,
|
||||
}))
|
||||
const rawHttpProperties = ['window', 'globalThis', 'global', 'self'].flatMap((object) =>
|
||||
['fetch', 'XMLHttpRequest'].map((property) => ({
|
||||
object,
|
||||
property,
|
||||
message: handwrittenApiMessage,
|
||||
})),
|
||||
)
|
||||
const rawHttpImports = {
|
||||
group: [
|
||||
'axios',
|
||||
'ky',
|
||||
'ofetch',
|
||||
'node-fetch',
|
||||
'cross-fetch',
|
||||
'isomorphic-fetch',
|
||||
'undici',
|
||||
'superagent',
|
||||
'http',
|
||||
'https',
|
||||
'node:http',
|
||||
'node:https',
|
||||
].flatMap((name) => [name, `${name}/*`]),
|
||||
allowTypeImports: true,
|
||||
message: handwrittenApiMessage,
|
||||
}
|
||||
const handwrittenApiScope = {
|
||||
files: ['web/**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}'],
|
||||
excludeFiles: [
|
||||
'web/**/__tests__/**',
|
||||
'web/**/__mocks__/**',
|
||||
'web/test/**',
|
||||
'web/scripts/**',
|
||||
'web/**/*.spec.{js,cjs,mjs,jsx,ts,cts,mts,tsx}',
|
||||
'web/**/*.test.{js,cjs,mjs,jsx,ts,cts,mts,tsx}',
|
||||
'web/**/*.stories.{js,cjs,mjs,jsx,ts,cts,mts,tsx}',
|
||||
'web/**/*.story.{js,cjs,mjs,jsx,ts,cts,mts,tsx}',
|
||||
'web/vitest.setup.ts',
|
||||
'web/vitest.browser.setup.ts',
|
||||
],
|
||||
}
|
||||
|
||||
const webRestrictedImports = [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'next',
|
||||
message: 'Import Next APIs from the corresponding @/next module instead of next.',
|
||||
},
|
||||
{
|
||||
name: 'ahooks',
|
||||
importNames: ['useLocalStorageState'],
|
||||
message: 'Do not use ahooks useLocalStorageState. Use foxact storage hooks instead.',
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
group: ['next/image'],
|
||||
message: 'Do not import next/image. Use native img tags instead.',
|
||||
},
|
||||
{
|
||||
group: ['next/font', 'next/font/*'],
|
||||
message: 'Do not import next/font. Use the project font styles instead.',
|
||||
},
|
||||
{
|
||||
// next/dynamic must be imported directly for compiler-generated preload metadata.
|
||||
group: [
|
||||
'next/*',
|
||||
'!next/dynamic',
|
||||
'!next/font',
|
||||
'!next/font/*',
|
||||
'!next/image',
|
||||
'!next/image/*',
|
||||
],
|
||||
message: 'Import Next APIs from the corresponding @/next/* module instead of next/*.',
|
||||
},
|
||||
{
|
||||
group: ['@base-ui/react', '@base-ui/react/*'],
|
||||
message:
|
||||
'Do not import Base UI directly in web. Use @langgenius/dify-ui/* primitives instead.',
|
||||
},
|
||||
{
|
||||
group: ['@floating-ui/*'],
|
||||
message:
|
||||
'Do not import Floating UI directly in web. Use @langgenius/dify-ui/* primitives instead.',
|
||||
},
|
||||
{
|
||||
group: ['@/service/base', '@/service/base/*', '**/service/base', '**/service/base/*'],
|
||||
message:
|
||||
'Do not import legacy service/base fetch helpers. Use generated service clients or feature-specific service modules instead.',
|
||||
},
|
||||
{
|
||||
group: ['@/service/fetch', '@/service/fetch/*', '**/service/fetch', '**/service/fetch/*'],
|
||||
message:
|
||||
'Do not import low-level service/fetch helpers directly. Use generated service clients or feature-specific service modules instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies NonNullable<OxlintConfig['rules']>[string]
|
||||
|
||||
const serviceRestrictedImports = [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'next',
|
||||
message: 'Import Next APIs from the corresponding @/next module instead of next.',
|
||||
},
|
||||
{
|
||||
name: 'ahooks',
|
||||
importNames: ['useLocalStorageState'],
|
||||
message: 'Do not use ahooks useLocalStorageState. Use foxact storage hooks instead.',
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
group: ['next/image'],
|
||||
message: 'Do not import next/image. Use native img tags instead.',
|
||||
},
|
||||
{
|
||||
group: ['next/font', 'next/font/*'],
|
||||
message: 'Do not import next/font. Use the project font styles instead.',
|
||||
},
|
||||
{
|
||||
// next/dynamic must be imported directly for compiler-generated preload metadata.
|
||||
group: [
|
||||
'next/*',
|
||||
'!next/dynamic',
|
||||
'!next/font',
|
||||
'!next/font/*',
|
||||
'!next/image',
|
||||
'!next/image/*',
|
||||
],
|
||||
message: 'Import Next APIs from the corresponding @/next/* module instead of next/*.',
|
||||
},
|
||||
{
|
||||
group: ['@base-ui/react', '@base-ui/react/*'],
|
||||
message:
|
||||
'Do not import Base UI directly in web. Use @langgenius/dify-ui/* primitives instead.',
|
||||
},
|
||||
{
|
||||
group: ['@floating-ui/*'],
|
||||
message:
|
||||
'Do not import Floating UI directly in web. Use @langgenius/dify-ui/* primitives instead.',
|
||||
},
|
||||
{
|
||||
group: ['@/service/base', '@/service/base/*', '**/service/base', '**/service/base/*'],
|
||||
message:
|
||||
'Do not import legacy service/base fetch helpers. Use generated service clients or feature-specific service modules instead.',
|
||||
},
|
||||
{
|
||||
group: ['@/service/fetch', '@/service/fetch/*', '**/service/fetch', '**/service/fetch/*'],
|
||||
message:
|
||||
'Do not import low-level service/fetch helpers directly. Use generated service clients or feature-specific service modules instead.',
|
||||
},
|
||||
{
|
||||
group: ['./base', './base/*', '../base', '../base/*', '../../base', '../../base/*'],
|
||||
message:
|
||||
'Do not import legacy service/base fetch helpers. Use generated service clients or feature-specific service modules instead.',
|
||||
},
|
||||
{
|
||||
group: ['./fetch', './fetch/*', '../fetch', '../fetch/*', '../../fetch', '../../fetch/*'],
|
||||
message:
|
||||
'Do not import low-level service/fetch helpers directly. Use generated service clients or feature-specific service modules instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies NonNullable<OxlintConfig['rules']>[string]
|
||||
|
||||
const defaultRestrictedGlobals = [
|
||||
'error',
|
||||
{
|
||||
message: 'Use `globalThis` instead.',
|
||||
name: 'global',
|
||||
},
|
||||
{
|
||||
message: 'Use `globalThis` instead.',
|
||||
name: 'self',
|
||||
},
|
||||
] satisfies NonNullable<OxlintConfig['rules']>[string]
|
||||
|
||||
const storageRestrictedGlobals = [
|
||||
'error',
|
||||
{
|
||||
name: 'localStorage',
|
||||
message:
|
||||
'Do not use localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
|
||||
},
|
||||
] satisfies NonNullable<OxlintConfig['rules']>[string]
|
||||
|
||||
const defaultRestrictedProperties = [
|
||||
'error',
|
||||
{
|
||||
message: 'Use `Object.getPrototypeOf` or `Object.setPrototypeOf` instead.',
|
||||
property: '__proto__',
|
||||
},
|
||||
{
|
||||
message: 'Use `Object.defineProperty` instead.',
|
||||
property: '__defineGetter__',
|
||||
},
|
||||
{
|
||||
message: 'Use `Object.defineProperty` instead.',
|
||||
property: '__defineSetter__',
|
||||
},
|
||||
{
|
||||
message: 'Use `Object.getOwnPropertyDescriptor` instead.',
|
||||
property: '__lookupGetter__',
|
||||
},
|
||||
{
|
||||
message: 'Use `Object.getOwnPropertyDescriptor` instead.',
|
||||
property: '__lookupSetter__',
|
||||
},
|
||||
] satisfies NonNullable<OxlintConfig['rules']>[string]
|
||||
|
||||
const storageRestrictedProperties = [
|
||||
'error',
|
||||
{
|
||||
object: 'window',
|
||||
property: 'localStorage',
|
||||
message:
|
||||
'Do not use window.localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
|
||||
},
|
||||
{
|
||||
object: 'globalThis',
|
||||
property: 'localStorage',
|
||||
message:
|
||||
'Do not use globalThis.localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
|
||||
},
|
||||
] satisfies NonNullable<OxlintConfig['rules']>[string]
|
||||
|
||||
export const stylingContractRules = {
|
||||
'shadcn/no-restyle': [
|
||||
'error',
|
||||
@@ -387,40 +623,8 @@ export const lintConfig = {
|
||||
},
|
||||
],
|
||||
'no-regex-spaces': 'error',
|
||||
'no-restricted-globals': [
|
||||
'error',
|
||||
{
|
||||
message: 'Use `globalThis` instead.',
|
||||
name: 'global',
|
||||
},
|
||||
{
|
||||
message: 'Use `globalThis` instead.',
|
||||
name: 'self',
|
||||
},
|
||||
],
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
message: 'Use `Object.getPrototypeOf` or `Object.setPrototypeOf` instead.',
|
||||
property: '__proto__',
|
||||
},
|
||||
{
|
||||
message: 'Use `Object.defineProperty` instead.',
|
||||
property: '__defineGetter__',
|
||||
},
|
||||
{
|
||||
message: 'Use `Object.defineProperty` instead.',
|
||||
property: '__defineSetter__',
|
||||
},
|
||||
{
|
||||
message: 'Use `Object.getOwnPropertyDescriptor` instead.',
|
||||
property: '__lookupGetter__',
|
||||
},
|
||||
{
|
||||
message: 'Use `Object.getOwnPropertyDescriptor` instead.',
|
||||
property: '__lookupSetter__',
|
||||
},
|
||||
],
|
||||
'no-restricted-globals': defaultRestrictedGlobals,
|
||||
'no-restricted-properties': defaultRestrictedProperties,
|
||||
'no-self-assign': [
|
||||
'error',
|
||||
{
|
||||
@@ -940,168 +1144,13 @@ export const lintConfig = {
|
||||
{
|
||||
files: ['web/**/*.{ts,cts,mts}', 'web/**/*.tsx'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'next',
|
||||
message: 'Import Next APIs from the corresponding @/next module instead of next.',
|
||||
},
|
||||
{
|
||||
name: 'ahooks',
|
||||
importNames: ['useLocalStorageState'],
|
||||
message:
|
||||
'Do not use ahooks useLocalStorageState. Use foxact storage hooks instead.',
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
group: ['next/image'],
|
||||
message: 'Do not import next/image. Use native img tags instead.',
|
||||
},
|
||||
{
|
||||
group: ['next/font', 'next/font/*'],
|
||||
message: 'Do not import next/font. Use the project font styles instead.',
|
||||
},
|
||||
{
|
||||
// next/dynamic must be imported directly for compiler-generated preload metadata.
|
||||
group: [
|
||||
'next/*',
|
||||
'!next/dynamic',
|
||||
'!next/font',
|
||||
'!next/font/*',
|
||||
'!next/image',
|
||||
'!next/image/*',
|
||||
],
|
||||
message:
|
||||
'Import Next APIs from the corresponding @/next/* module instead of next/*.',
|
||||
},
|
||||
{
|
||||
group: ['@base-ui/react', '@base-ui/react/*'],
|
||||
message:
|
||||
'Do not import Base UI directly in web. Use @langgenius/dify-ui/* primitives instead.',
|
||||
},
|
||||
{
|
||||
group: ['@floating-ui/*'],
|
||||
message:
|
||||
'Do not import Floating UI directly in web. Use @langgenius/dify-ui/* primitives instead.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'@/service/base',
|
||||
'@/service/base/*',
|
||||
'**/service/base',
|
||||
'**/service/base/*',
|
||||
],
|
||||
message:
|
||||
'Do not import legacy service/base fetch helpers. Use generated service clients or feature-specific service modules instead.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'@/service/fetch',
|
||||
'@/service/fetch/*',
|
||||
'**/service/fetch',
|
||||
'**/service/fetch/*',
|
||||
],
|
||||
message:
|
||||
'Do not import low-level service/fetch helpers directly. Use generated service clients or feature-specific service modules instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-restricted-imports': webRestrictedImports,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['web/service/**/*.ts', 'web/service/**/*.tsx'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'next',
|
||||
message: 'Import Next APIs from the corresponding @/next module instead of next.',
|
||||
},
|
||||
{
|
||||
name: 'ahooks',
|
||||
importNames: ['useLocalStorageState'],
|
||||
message:
|
||||
'Do not use ahooks useLocalStorageState. Use foxact storage hooks instead.',
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
group: ['next/image'],
|
||||
message: 'Do not import next/image. Use native img tags instead.',
|
||||
},
|
||||
{
|
||||
group: ['next/font', 'next/font/*'],
|
||||
message: 'Do not import next/font. Use the project font styles instead.',
|
||||
},
|
||||
{
|
||||
// next/dynamic must be imported directly for compiler-generated preload metadata.
|
||||
group: [
|
||||
'next/*',
|
||||
'!next/dynamic',
|
||||
'!next/font',
|
||||
'!next/font/*',
|
||||
'!next/image',
|
||||
'!next/image/*',
|
||||
],
|
||||
message:
|
||||
'Import Next APIs from the corresponding @/next/* module instead of next/*.',
|
||||
},
|
||||
{
|
||||
group: ['@base-ui/react', '@base-ui/react/*'],
|
||||
message:
|
||||
'Do not import Base UI directly in web. Use @langgenius/dify-ui/* primitives instead.',
|
||||
},
|
||||
{
|
||||
group: ['@floating-ui/*'],
|
||||
message:
|
||||
'Do not import Floating UI directly in web. Use @langgenius/dify-ui/* primitives instead.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'@/service/base',
|
||||
'@/service/base/*',
|
||||
'**/service/base',
|
||||
'**/service/base/*',
|
||||
],
|
||||
message:
|
||||
'Do not import legacy service/base fetch helpers. Use generated service clients or feature-specific service modules instead.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'@/service/fetch',
|
||||
'@/service/fetch/*',
|
||||
'**/service/fetch',
|
||||
'**/service/fetch/*',
|
||||
],
|
||||
message:
|
||||
'Do not import low-level service/fetch helpers directly. Use generated service clients or feature-specific service modules instead.',
|
||||
},
|
||||
{
|
||||
group: ['./base', './base/*', '../base', '../base/*', '../../base', '../../base/*'],
|
||||
message:
|
||||
'Do not import legacy service/base fetch helpers. Use generated service clients or feature-specific service modules instead.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'./fetch',
|
||||
'./fetch/*',
|
||||
'../fetch',
|
||||
'../fetch/*',
|
||||
'../../fetch',
|
||||
'../../fetch/*',
|
||||
],
|
||||
message:
|
||||
'Do not import low-level service/fetch helpers directly. Use generated service clients or feature-specific service modules instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-restricted-imports': serviceRestrictedImports,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1116,29 +1165,8 @@ export const lintConfig = {
|
||||
'web/instrumentation-client.ts',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-globals': [
|
||||
'error',
|
||||
{
|
||||
name: 'localStorage',
|
||||
message:
|
||||
'Do not use localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
|
||||
},
|
||||
],
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
object: 'window',
|
||||
property: 'localStorage',
|
||||
message:
|
||||
'Do not use window.localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
|
||||
},
|
||||
{
|
||||
object: 'globalThis',
|
||||
property: 'localStorage',
|
||||
message:
|
||||
'Do not use globalThis.localStorage directly. Use a foxact storage boundary instead; prefer feature-owned createLocalStorageState for shared storage.',
|
||||
},
|
||||
],
|
||||
'no-restricted-globals': storageRestrictedGlobals,
|
||||
'no-restricted-properties': storageRestrictedProperties,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1621,5 +1649,53 @@ export const lintConfig = {
|
||||
'no-useless-assignment': 'error',
|
||||
},
|
||||
},
|
||||
// Apply after the existing Web overrides and preserve their unrelated restrictions.
|
||||
{
|
||||
files: ['web/**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}'],
|
||||
rules: {
|
||||
'typescript/no-deprecated': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
...handwrittenApiScope,
|
||||
rules: {
|
||||
'no-restricted-globals': [...defaultRestrictedGlobals, ...rawHttpGlobals],
|
||||
'no-restricted-properties': [...defaultRestrictedProperties, ...rawHttpProperties],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
...webRestrictedImports[1],
|
||||
patterns: [...webRestrictedImports[1].patterns, rawHttpImports],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
...handwrittenApiScope,
|
||||
files: ['web/**/*.{ts,cts,mts}', 'web/**/*.tsx'],
|
||||
excludeFiles: [
|
||||
...handwrittenApiScope.excludeFiles,
|
||||
'web/**/*.bench.{js,cjs,mjs,jsx,ts,cts,mts,tsx}',
|
||||
'web/**/*.benchmark.{js,cjs,mjs,jsx,ts,cts,mts,tsx}',
|
||||
'web/instrumentation-client.ts',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-globals': [...storageRestrictedGlobals, ...rawHttpGlobals],
|
||||
'no-restricted-properties': [...storageRestrictedProperties, ...rawHttpProperties],
|
||||
},
|
||||
},
|
||||
{
|
||||
...handwrittenApiScope,
|
||||
files: ['web/service/**/*.ts', 'web/service/**/*.tsx'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
...serviceRestrictedImports[1],
|
||||
patterns: [...serviceRestrictedImports[1].patterns, rawHttpImports],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies OxlintConfig
|
||||
@@ -630,6 +630,7 @@ type UploadResponse = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** @deprecated Use generated contract clients for new APIs. Special transports require a scoped lint exception. */
|
||||
export const upload = async (
|
||||
options: UploadOptions,
|
||||
isPublicAPI?: boolean,
|
||||
@@ -704,6 +705,7 @@ export const upload = async (
|
||||
})
|
||||
}
|
||||
|
||||
/** @deprecated Use generated contract clients for new APIs. Special transports require a scoped lint exception. */
|
||||
export const ssePost = async (
|
||||
url: string,
|
||||
fetchOptions: FetchOptionType,
|
||||
@@ -878,6 +880,7 @@ export const ssePost = async (
|
||||
})
|
||||
}
|
||||
|
||||
/** @deprecated Use generated contract clients for new APIs. Special transports require a scoped lint exception. */
|
||||
export const sseGet = async (
|
||||
url: string,
|
||||
fetchOptions: FetchOptionType,
|
||||
@@ -1064,6 +1067,7 @@ export type GeneratorStreamCallbacks = {
|
||||
* positional callbacks through that shared, high-blast-radius path isn't worth
|
||||
* it. This helper reuses the same cookie-auth + CSRF + abort setup but
|
||||
* only understands the generator's two events: ``plan`` then ``result``.
|
||||
* @deprecated Use generated contract clients for new APIs. Special transports require a scoped lint exception.
|
||||
*/
|
||||
export const sseGeneratorPost = (
|
||||
url: string,
|
||||
@@ -1154,6 +1158,7 @@ export const sseGeneratorPost = (
|
||||
}
|
||||
|
||||
// base request
|
||||
/** @deprecated Use generated contract clients for new APIs. Special transports require a scoped lint exception. */
|
||||
export const request = async <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
try {
|
||||
const otherOptionsForBaseFetch = otherOptions || {}
|
||||
@@ -1262,11 +1267,13 @@ export const get = <T>(url: string, options = {}, otherOptions?: IOtherOptions)
|
||||
}
|
||||
|
||||
// For public API
|
||||
/** @deprecated Use generated contract clients for new APIs. Special transports require a scoped lint exception. */
|
||||
export const getPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
return get<T>(url, options, { ...otherOptions, isPublicAPI: true })
|
||||
}
|
||||
|
||||
// For Marketplace API
|
||||
/** @deprecated Use generated contract clients for new APIs. Special transports require a scoped lint exception. */
|
||||
export const getMarketplace = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
return get<T>(url, options, { ...otherOptions, isMarketplaceAPI: true })
|
||||
}
|
||||
@@ -1282,10 +1289,12 @@ export const post = <T>(url: string, options = {}, otherOptions?: IOtherOptions)
|
||||
}
|
||||
|
||||
// For Marketplace API
|
||||
/** @deprecated Use generated contract clients for new APIs. Special transports require a scoped lint exception. */
|
||||
export const postMarketplace = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
return post<T>(url, options, { ...otherOptions, isMarketplaceAPI: true })
|
||||
}
|
||||
|
||||
/** @deprecated Use generated contract clients for new APIs. Special transports require a scoped lint exception. */
|
||||
export const postPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
return post<T>(url, options, { ...otherOptions, isPublicAPI: true })
|
||||
}
|
||||
@@ -1300,6 +1309,7 @@ export const del = <T>(url: string, options = {}, otherOptions?: IOtherOptions)
|
||||
return request<T>(url, Object.assign({}, options, { method: 'DELETE' }), otherOptions)
|
||||
}
|
||||
|
||||
/** @deprecated Use generated contract clients for new APIs. Special transports require a scoped lint exception. */
|
||||
export const delPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
return del<T>(url, options, { ...otherOptions, isPublicAPI: true })
|
||||
}
|
||||
@@ -1314,6 +1324,7 @@ export const patch = <T>(url: string, options = {}, otherOptions?: IOtherOptions
|
||||
return request<T>(url, Object.assign({}, options, { method: 'PATCH' }), otherOptions)
|
||||
}
|
||||
|
||||
/** @deprecated Use generated contract clients for new APIs. Special transports require a scoped lint exception. */
|
||||
export const patchPublic = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
return patch<T>(url, options, { ...otherOptions, isPublicAPI: true })
|
||||
}
|
||||
@@ -110,6 +110,7 @@ export const getBaseOptions = (): RequestInit => ({
|
||||
redirect: 'follow',
|
||||
})
|
||||
|
||||
/** @deprecated Use generated contract clients for new APIs. Special transports require a scoped lint exception. */
|
||||
async function base<T>(
|
||||
url: string,
|
||||
options: FetchOptionType = {},
|
||||
@@ -221,6 +222,7 @@ async function base<T>(
|
||||
* Includes credentials, Authorization (if available), and CSRF header
|
||||
* so the request is authenticated, matching the headers sent by the
|
||||
* standard `base()` fetch wrapper.
|
||||
* @deprecated New keepalive transports require a scoped lint exception.
|
||||
*/
|
||||
export function postWithKeepalive(url: string, body: Record<string, unknown>): void {
|
||||
const headers = new Headers({
|
||||
|
||||
Reference in new issue
Block a user