Commit 5b4839fa by QuentinHsu Committed by GitHub

chore(web): add default frontend oxc tooling (#5585)

- add oxlint and oxfmt configuration for web/default quality checks

- replace ESLint and Prettier scripts with protected-header Oxc formatting

- update frontend workspace dependencies for oxlint and oxfmt
parent 0c806db9
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"insertFinalNewline": true,
"jsxSingleQuote": true,
"printWidth": 80,
"quoteProps": "as-needed",
"semi": false,
"singleQuote": true,
"sortImports": true,
"sortPackageJson": true,
"sortTailwindcss": true,
"tabWidth": 2,
"trailingComma": "es5",
"ignorePatterns": [
"node_modules",
"dist",
"build",
"coverage",
".tanstack",
".eslintrc.cjs",
"**/routeTree.gen.ts",
"src/routeTree.gen.ts"
]
}
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "unicorn", "oxc", "react", "import", "promise"],
"categories": {
"correctness": "error"
},
"env": {
"browser": true,
"builtin": true,
"es2024": true,
"node": true
},
"ignorePatterns": [
"node_modules",
"dist",
"build",
"coverage",
".tanstack",
".eslintrc.cjs",
"src/components/ui",
"src/routeTree.gen.ts"
],
"rules": {
"curly": "error",
"eqeqeq": [
"error",
"always",
{
"null": "ignore"
}
],
"import/first": "warn",
"import/newline-after-import": "warn",
"import/no-cycle": "warn",
"import/no-duplicates": [
"error",
{
"preferInline": true
}
],
"import/no-unassigned-import": "off",
"no-array-constructor": "error",
"no-console": "warn",
"no-duplicate-imports": "error",
"no-nested-ternary": "error",
"no-new-wrappers": "error",
"no-regex-spaces": "error",
"no-unsafe-finally": "warn",
"no-unsafe-negation": "error",
"no-unused-expressions": "warn",
"no-unused-labels": "error",
"no-unused-vars": [
"warn",
{
"args": "all",
"argsIgnorePattern": "^_",
"caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
"no-useless-catch": "warn",
"no-useless-computed-key": "error",
"no-useless-escape": "warn",
"no-useless-rename": "warn",
"no-var": "warn",
"object-shorthand": "warn",
"oxc/const-comparisons": "error",
"prefer-const": "warn",
"prefer-object-has-own": "warn",
"prefer-object-spread": "warn",
"prefer-template": "warn",
"promise/catch-or-return": "error",
"promise/no-callback-in-promise": "error",
"promise/no-new-statics": "error",
"promise/no-return-wrap": "warn",
"promise/valid-params": "warn",
"react/button-has-type": "error",
"react/checked-requires-onchange-or-readonly": "warn",
"react/exhaustive-deps": "error",
"react/iframe-missing-sandbox": "error",
"react/jsx-boolean-value": "warn",
"react/jsx-curly-brace-presence": "warn",
"react/jsx-fragments": "warn",
"react/jsx-key": "warn",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-target-blank": "warn",
"react/jsx-no-undef": "error",
"react/jsx-no-useless-fragment": "error",
"react/jsx-props-no-spread-multi": "warn",
"react/no-array-index-key": "error",
"react/no-danger": "warn",
"react/no-unknown-property": "warn",
"react/no-unstable-nested-components": [
"error",
{
"allowAsProps": true
}
],
"react/only-export-components": [
"error",
{
"allowConstantExport": true,
"allowExportNames": [
"getAuthSectionNavItems",
"getAuthSectionContent",
"getAuthSectionMeta",
"getBillingSectionNavItems",
"getBillingSectionContent",
"getBillingSectionMeta",
"getContentSectionNavItems",
"getContentSectionContent",
"getContentSectionMeta",
"getDashboardSectionNavItems",
"getModelsSectionNavItems",
"getModelsSectionContent",
"getModelsSectionMeta",
"getOperationsSectionNavItems",
"getOperationsSectionContent",
"getOperationsSectionMeta",
"getSecuritySectionNavItems",
"getSecuritySectionContent",
"getSecuritySectionMeta",
"getSiteSectionNavItems",
"getSiteSectionContent",
"getSiteSectionMeta",
"getUsageLogsSectionNavItems",
"isUsageLogsSectionId",
"useSuppressSettingsSectionHeader"
]
}
],
"react/react-in-jsx-scope": "off",
"react/rules-of-hooks": "warn",
"react/self-closing-comp": "warn",
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports",
"disallowTypeAnnotations": false
}
],
"typescript/no-duplicate-type-constituents": "error",
"typescript/no-explicit-any": "warn",
"typescript/no-extra-non-null-assertion": "error",
"typescript/no-import-type-side-effects": "error",
"typescript/no-non-null-assertion": "error",
"typescript/no-require-imports": "error",
"typescript/no-unnecessary-template-expression": "error",
"typescript/no-unnecessary-type-arguments": "error",
"typescript/no-unnecessary-type-assertion": "error",
"typescript/no-useless-empty-export": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/prefer-as-const": "error",
"typescript/prefer-namespace-keyword": "error",
"unicorn/no-single-promise-in-promise-methods": "error",
"unicorn/no-unnecessary-await": "error",
"unicorn/no-useless-fallback-in-spread": "warn",
"unicorn/no-useless-length-check": "warn",
"unicorn/no-useless-promise-resolve-reject": "warn",
"unicorn/no-useless-spread": "error",
"unicorn/prefer-add-event-listener": "warn",
"unicorn/prefer-array-find": "warn",
"unicorn/prefer-array-flat": "warn",
"unicorn/prefer-array-flat-map": "warn",
"unicorn/prefer-array-index-of": "warn",
"unicorn/prefer-array-some": "warn",
"unicorn/prefer-at": "error",
"unicorn/prefer-date-now": "warn",
"unicorn/prefer-includes": "warn",
"unicorn/prefer-keyboard-event-key": "warn",
"unicorn/prefer-modern-dom-apis": "warn",
"unicorn/prefer-module": "warn",
"unicorn/prefer-node-protocol": "warn",
"unicorn/prefer-number-properties": "warn",
"unicorn/prefer-object-from-entries": "warn",
"unicorn/prefer-optional-catch-binding": "error",
"unicorn/prefer-query-selector": "warn",
"unicorn/prefer-regexp-test": "warn",
"unicorn/prefer-set-has": "warn",
"unicorn/prefer-spread": "error",
"unicorn/prefer-string-replace-all": "error",
"unicorn/prefer-string-slice": "warn",
"unicorn/prefer-string-starts-ends-with": "warn",
"unicorn/prefer-string-trim-start-end": "warn",
"unicorn/prefer-structured-clone": "warn",
"unicorn/throw-new-error": "error"
},
"overrides": [
{
"files": ["src/routes/**/*.{ts,tsx}"],
"rules": {
"react/only-export-components": "off"
}
},
{
"files": ["**/scripts/**/*.{js,mjs,cjs}"],
"rules": {
"no-console": "off",
"prefer-node-protocol": "off"
}
}
]
}
# Ignore everything
/*
# Except these files & folders
!/src
!index.html
!package.json
!tailwind.config.js
!tsconfig.json
!tsconfig.node.json
!vite.config.ts
!.prettierrc
!README.md
!eslint.config.js
!postcss.config.js
# Ignore auto generated routeTree.gen.ts
/src/routeTree.gen.ts
\ No newline at end of file
{
"arrowParens": "always",
"semi": false,
"tabWidth": 2,
"printWidth": 80,
"singleQuote": true,
"jsxSingleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"endOfLine": "lf",
"plugins": [
"@trivago/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss"
],
"importOrder": [
"^path$",
"^vite$",
"^@vitejs/(.*)$",
"^react$",
"^react-dom/client$",
"^react/(.*)$",
"^globals$",
"^zod$",
"^axios$",
"^dayjs$",
"^react-hook-form$",
"^use-intl$",
"^@radix-ui/(.*)$",
"^@hookform/resolvers/zod$",
"^@tanstack/react-query$",
"^@tanstack/react-router$",
"^@tanstack/react-table$",
"<THIRD_PARTY_MODULES>",
"^@/assets/(.*)",
"^@/api/(.*)$",
"^@/stores/(.*)$",
"^@/lib/(.*)$",
"^@/utils/(.*)$",
"^@/constants/(.*)$",
"^@/context/(.*)$",
"^@/hooks/(.*)$",
"^@/components/layouts/(.*)$",
"^@/components/ui/(.*)$",
"^@/components/errors/(.*)$",
"^@/components/(.*)$",
"^@/features/(.*)$",
"^[./]"
]
}
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
| UI 与样式 | Base UI、Hugeicons、Tailwind CSS、clsx / class-variance-authority | | UI 与样式 | Base UI、Hugeicons、Tailwind CSS、clsx / class-variance-authority |
| 表单 | React Hook Form、Zod | | 表单 | React Hook Form、Zod |
| 图表 | @visactor/vchart、@visactor/react-vchart | | 图表 | @visactor/vchart、@visactor/react-vchart |
| 工具 | qrcode.react、prettier、eslint、vitest(可选)| | 工具 | qrcode.react、oxfmt、oxlint、vitest(可选)|
优先选用成熟、维护良好的开源库;仅在现有库无法满足或需特殊适配时自行实现,并评估可维护性与通用性。 优先选用成熟、维护良好的开源库;仅在现有库无法满足或需特殊适配时自行实现,并评估可维护性与通用性。
......
import globals from 'globals'
import js from '@eslint/js'
import pluginQuery from '@tanstack/eslint-plugin-query'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig } from 'eslint/config'
import tseslint from 'typescript-eslint'
export default defineConfig(
{ ignores: ['dist', 'src/components/ui'] },
{
extends: [
js.configs.recommended,
...tseslint.configs.recommended,
...pluginQuery.configs['flat/recommended'],
],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-hooks/incompatible-library': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'no-console': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/consistent-type-imports': [
'error',
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports',
disallowTypeAnnotations: false,
},
],
'no-duplicate-imports': 'error',
},
},
{
files: ['src/routes/**/*.{ts,tsx}'],
plugins: {
'react-refresh': reactRefresh,
},
rules: {
'react-refresh/only-export-components': 'off',
},
}
)
{ {
"name": "newapi-web", "name": "newapi-web",
"private": false,
"version": "1.0.0", "version": "1.0.0",
"private": false,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "rsbuild dev", "dev": "rsbuild dev",
"build": "rsbuild build", "build": "rsbuild build",
"build:check": "tsc -b && rsbuild build", "build:check": "tsc -b && rsbuild build",
"typecheck": "tsc -b", "typecheck": "tsc -b",
"lint": "eslint .", "lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint -c .oxlintrc.json . --fix",
"preview": "rsbuild preview", "preview": "rsbuild preview",
"format:check": "prettier --check .", "format:check": "node scripts/format-with-protected-headers.mjs --check",
"format": "prettier --write .", "format": "node scripts/format-with-protected-headers.mjs --write",
"copyright:check": "node scripts/add-copyright.mjs --check", "copyright:check": "node scripts/add-copyright.mjs --check",
"copyright": "node scripts/add-copyright.mjs", "copyright": "node scripts/add-copyright.mjs",
"i18n:sync": "node scripts/sync-i18n.mjs", "i18n:sync": "node scripts/sync-i18n.mjs",
...@@ -74,29 +75,21 @@ ...@@ -74,29 +75,21 @@
"zustand": "^5.0.13" "zustand": "^5.0.13"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1",
"@rsbuild/core": "^2.0.7", "@rsbuild/core": "^2.0.7",
"@rsbuild/plugin-react": "^2.0.0", "@rsbuild/plugin-react": "^2.0.0",
"@tanstack/eslint-plugin-query": "^5.100.14",
"@tanstack/react-query-devtools": "^5.100.14", "@tanstack/react-query-devtools": "^5.100.14",
"@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-router-devtools": "^1.167.0",
"@tanstack/router-plugin": "^1.168.11", "@tanstack/router-plugin": "^1.168.11",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.9.1", "@types/node": "^25.9.1",
"@types/react": "^19.2.15", "@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@xyflow/react": "^12.10.2", "@xyflow/react": "^12.10.2",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"eslint": "^10.4.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"knip": "^6.14.2", "knip": "^6.14.2",
"prettier": "catalog:", "oxfmt": "catalog:",
"prettier-plugin-tailwindcss": "^0.8.0", "oxlint": "catalog:",
"shadcn": "^4.8.0", "shadcn": "^4.8.0",
"typescript": "~6.0.3", "typescript": "~6.0.3"
"typescript-eslint": "^8.59.4"
}, },
"overrides": { "overrides": {
"brace-expansion": "2.1.1", "brace-expansion": "2.1.1",
......
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { spawnSync } from 'node:child_process'
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
import { join, relative } from 'node:path'
const mode = process.argv[2]
if (mode !== '--check' && mode !== '--write') {
console.error(
'Usage: node scripts/format-with-protected-headers.mjs --check|--write'
)
process.exit(2)
}
const root = process.cwd()
const excludedDirs = new Set([
'.git',
'.tanstack',
'build',
'coverage',
'dist',
'node_modules',
])
const headerExtensions = new Set([
'.cjs',
'.cts',
'.js',
'.jsx',
'.mjs',
'.mts',
'.ts',
'.tsx',
])
const protectedHeaderPattern =
/^\/\*\nCopyright \(C\)[\s\S]*?QuantumNous[\s\S]*?\*\/\n+/
function extensionOf(path) {
const index = path.lastIndexOf('.')
return index === -1 ? '' : path.slice(index)
}
function walk(dir, files = []) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!excludedDirs.has(entry.name)) {
walk(join(dir, entry.name), files)
}
continue
}
if (entry.isFile()) {
files.push(join(dir, entry.name))
}
}
return files
}
function snapshotFiles(files) {
const snapshot = new Map()
for (const file of files) {
snapshot.set(file, readFileSync(file))
}
return snapshot
}
function restoreSnapshot(snapshot) {
for (const [file, content] of snapshot) {
writeFileSync(file, content)
}
}
function stripProtectedHeaders(files) {
const headers = new Map()
for (const file of files) {
if (!headerExtensions.has(extensionOf(file))) {
continue
}
const content = readFileSync(file, 'utf8')
const match = content.match(protectedHeaderPattern)
if (!match) {
continue
}
headers.set(file, match[0])
writeFileSync(file, content.slice(match[0].length))
}
return headers
}
function restoreProtectedHeaders(headers) {
for (const [file, header] of headers) {
const content = readFileSync(file, 'utf8').replace(/^\n+/, '')
if (!content.startsWith(header)) {
writeFileSync(file, header + content)
}
}
}
function listChangedFiles(before, files) {
const changed = []
for (const file of files) {
const previous = before.get(file)
const current = readFileSync(file)
if (!previous || !previous.equals(current)) {
changed.push(relative(root, file))
}
}
return changed
}
const files = walk(root).filter(
(file) => statSync(file).size < 10 * 1024 * 1024
)
const before = mode === '--check' ? snapshotFiles(files) : null
let headers = new Map()
let exitCode = 0
try {
headers = stripProtectedHeaders(files)
const result = spawnSync(
'oxfmt',
['-c', '.oxfmtrc.json', '--ignore-path', '.gitignore', '--write', '.'],
{
cwd: root,
stdio: 'inherit',
}
)
exitCode = result.status ?? 1
restoreProtectedHeaders(headers)
if (mode === '--check' && exitCode === 0) {
const changed = listChangedFiles(before, files)
if (changed.length > 0) {
console.error('Format issues found in protected-header-safe check:')
for (const file of changed) {
console.error(file)
}
exitCode = 1
}
}
} finally {
if (mode === '--check' && before) {
restoreSnapshot(before)
} else {
restoreProtectedHeaders(headers)
}
}
process.exit(exitCode)
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
"axios": "^1.16.1", "axios": "^1.16.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"oxfmt": "^0.54.0",
"oxlint": "^1.69.0",
"prettier": "^3.8.3", "prettier": "^3.8.3",
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react-icons": "^5.6.0", "react-icons": "^5.6.0",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment