snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
80
common/shared/eslint/config.ts
Normal file
80
common/shared/eslint/config.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { Rules } from '@antfu/eslint-config';
|
||||
import type { ESLint, Linter } from 'eslint';
|
||||
import antfu from '@antfu/eslint-config';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import {
|
||||
facadePreset,
|
||||
headerPreset,
|
||||
indexEntryPreset,
|
||||
noBarrelImportPreset,
|
||||
penetratingPreset,
|
||||
specPreset,
|
||||
tailwindcssPreset,
|
||||
typescriptPreset,
|
||||
univerSourcePreset,
|
||||
} from './presets';
|
||||
import { baseRules } from './rules';
|
||||
|
||||
export interface IUniverEslintConfigOptions {
|
||||
ignores?: string[];
|
||||
header?: boolean;
|
||||
noFacadeImportsOutsideFacade?: {
|
||||
ignore?: string[];
|
||||
};
|
||||
rules?: Partial<Rules>;
|
||||
}
|
||||
|
||||
export const createUniverEslintConfig = (options: IUniverEslintConfigOptions = {}) => {
|
||||
const configs: Linter.Config[] = [
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}'],
|
||||
plugins: {
|
||||
'react-hooks': reactHooks as unknown as ESLint.Plugin,
|
||||
},
|
||||
rules: {
|
||||
...baseRules,
|
||||
...options.rules,
|
||||
},
|
||||
},
|
||||
penetratingPreset(),
|
||||
typescriptPreset(),
|
||||
indexEntryPreset(),
|
||||
univerSourcePreset({
|
||||
noFacadeImportsOutsideFacade: options.noFacadeImportsOutsideFacade,
|
||||
}),
|
||||
facadePreset(),
|
||||
noBarrelImportPreset(),
|
||||
tailwindcssPreset(),
|
||||
specPreset(),
|
||||
];
|
||||
|
||||
if (options.header ?? true) {
|
||||
configs.push(headerPreset());
|
||||
}
|
||||
|
||||
return antfu(
|
||||
{
|
||||
ignores: options.ignores,
|
||||
stylistic: {
|
||||
indent: 4,
|
||||
semi: true,
|
||||
},
|
||||
e18e: false,
|
||||
regexp: false,
|
||||
react: true,
|
||||
pnpm: false,
|
||||
yaml: {
|
||||
overrides: {
|
||||
'yaml/indent': ['error', 4, { indicatorValueIndent: 2 }],
|
||||
},
|
||||
},
|
||||
markdown: false,
|
||||
typescript: true,
|
||||
formatters: {
|
||||
css: true,
|
||||
html: true,
|
||||
},
|
||||
},
|
||||
...configs
|
||||
);
|
||||
};
|
||||
14
common/shared/eslint/index.ts
Normal file
14
common/shared/eslint/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export { createUniverEslintConfig } from './config';
|
||||
export type { IUniverEslintConfigOptions } from './config';
|
||||
export {
|
||||
facadePreset,
|
||||
headerPreset,
|
||||
indexEntryPreset,
|
||||
noBarrelImportPreset,
|
||||
penetratingPreset,
|
||||
specPreset,
|
||||
tailwindcssPreset,
|
||||
typescriptPreset,
|
||||
univerSourcePreset,
|
||||
} from './presets';
|
||||
export { baseRules } from './rules';
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import { describe, it } from 'vitest';
|
||||
import noExternalImportsInFacade from './no-external-imports-in-facade';
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
});
|
||||
|
||||
describe('no-external-imports-in-facade', () => {
|
||||
it('explains the facade package-entry migration path', () => {
|
||||
ruleTester.run('no-external-imports-in-facade', noExternalImportsInFacade, {
|
||||
valid: [
|
||||
{
|
||||
code: 'import { PublicCommand } from "@univerjs/example";',
|
||||
filename: '/repo/packages/example/src/facade/f-univer.ts',
|
||||
},
|
||||
{
|
||||
code: 'import { FThing } from "./f-thing";',
|
||||
filename: '/repo/packages/example/src/facade/f-univer.ts',
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'import { PrivateCommand } from "../commands/private.command";',
|
||||
filename: '/repo/packages/example/src/facade/f-univer.ts',
|
||||
errors: [
|
||||
{
|
||||
message: 'Facade files are published as a separate package entry. Do not import package-internal runtime modules through "../commands/private.command"; it can duplicate module singletons when consumers import both the package root and the facade entry. Move the needed symbol to the package root export if necessary, then import it through the package name. Type-only imports should also use a public package entry.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { Rule } from 'eslint';
|
||||
import path from 'node:path';
|
||||
|
||||
function getImportSourceValue(node: Rule.Node): string | null {
|
||||
if (!('source' in node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const source = (node as { source?: { value?: unknown } }).source;
|
||||
if (!source || typeof source.value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return source.value;
|
||||
}
|
||||
|
||||
function getRuleFilename(context: Rule.RuleContext): string {
|
||||
const filenameFromProperty = (context as { filename?: unknown }).filename;
|
||||
|
||||
if (typeof filenameFromProperty === 'string' && filenameFromProperty) {
|
||||
return filenameFromProperty;
|
||||
}
|
||||
|
||||
const getFilename = (context as { getFilename?: () => string }).getFilename;
|
||||
return typeof getFilename === 'function' ? getFilename.call(context) : '';
|
||||
}
|
||||
|
||||
const rule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow imports from outside facade directory in facade files',
|
||||
},
|
||||
messages: {
|
||||
noExternalImports: 'Facade files are published as a separate package entry. Do not import package-internal runtime modules through "{{importPath}}"; it can duplicate module singletons when consumers import both the package root and the facade entry. Move the needed symbol to the package root export if necessary, then import it through the package name. Type-only imports should also use a public package entry.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = getRuleFilename(context);
|
||||
const normalizedPath = filename.split(path.sep).join('/');
|
||||
const isFacadeFile = normalizedPath.includes('/facade/');
|
||||
|
||||
if (!isFacadeFile) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const currentDir = path.dirname(filename);
|
||||
|
||||
return {
|
||||
ImportDeclaration(node: Rule.Node) {
|
||||
const importPath = getImportSourceValue(node);
|
||||
|
||||
if (!importPath || !importPath.startsWith('.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(currentDir, importPath).split(path.sep).join('/');
|
||||
|
||||
if (!resolvedPath.includes('/facade/')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noExternalImports',
|
||||
data: { importPath },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
137
common/shared/eslint/plugins/no-facade-imports-outside-facade.ts
Normal file
137
common/shared/eslint/plugins/no-facade-imports-outside-facade.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import type { Rule } from 'eslint';
|
||||
import path from 'node:path';
|
||||
|
||||
interface IRuleOptions { ignore?: unknown[] }
|
||||
|
||||
function getImportSourceValue(node: Rule.Node): string | null {
|
||||
if (!('source' in node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const source = (node as { source?: { value?: unknown } }).source;
|
||||
if (!source || typeof source.value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return source.value;
|
||||
}
|
||||
|
||||
function normalizePath(filePath: string): string {
|
||||
return filePath.split(path.sep).join('/');
|
||||
}
|
||||
|
||||
function shouldIgnoreFile(normalizedFilePath: string, ignorePaths: readonly unknown[]): boolean {
|
||||
return ignorePaths.some((ignorePath: unknown) => {
|
||||
if (typeof ignorePath !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedIgnorePath = normalizePath(ignorePath).replace(/^\.\//, '');
|
||||
|
||||
if (!normalizedIgnorePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return normalizedFilePath === normalizedIgnorePath
|
||||
|| normalizedFilePath.endsWith(`/${normalizedIgnorePath}`)
|
||||
|| normalizedFilePath.includes(normalizedIgnorePath);
|
||||
});
|
||||
}
|
||||
|
||||
function getRuleFilename(context: Rule.RuleContext): string {
|
||||
const filenameFromProperty = (context as { filename?: unknown }).filename;
|
||||
|
||||
if (typeof filenameFromProperty === 'string' && filenameFromProperty) {
|
||||
return filenameFromProperty;
|
||||
}
|
||||
|
||||
const getFilename = (context as { getFilename?: () => string }).getFilename;
|
||||
return typeof getFilename === 'function' ? getFilename.call(context) : '';
|
||||
}
|
||||
|
||||
const rule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow imports containing facade in non-facade files',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignore: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
noFacadeImports: 'Imports containing "facade" are not allowed in non-facade files: "{{importPath}}"',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = getRuleFilename(context);
|
||||
const normalizedPath = normalizePath(filename);
|
||||
const [ruleOptions = {} as IRuleOptions] = context.options as [IRuleOptions?];
|
||||
const ignorePaths = Array.isArray(ruleOptions.ignore) ? ruleOptions.ignore : [];
|
||||
|
||||
if (shouldIgnoreFile(normalizedPath, ignorePaths)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const isInPackages = normalizedPath.includes('/packages/');
|
||||
const isInFacade = normalizedPath.includes('/facade/');
|
||||
|
||||
if (!isInPackages || isInFacade) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// get parent dir
|
||||
const parentDirMatch = normalizedPath.match(/\/([^/]+)\/packages\//);
|
||||
if (!parentDirMatch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node: Rule.Node) {
|
||||
const importPath = getImportSourceValue(node);
|
||||
|
||||
// Check if the import path contains 'facade'
|
||||
if (importPath?.includes('facade')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noFacadeImports',
|
||||
data: { importPath },
|
||||
});
|
||||
}
|
||||
},
|
||||
ExportNamedDeclaration(node: Rule.Node) {
|
||||
const exportPath = getImportSourceValue(node);
|
||||
if (exportPath?.includes('facade')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noFacadeImports',
|
||||
data: { importPath: exportPath },
|
||||
});
|
||||
}
|
||||
},
|
||||
ExportAllDeclaration(node: Rule.Node) {
|
||||
const exportPath = getImportSourceValue(node);
|
||||
if (exportPath?.includes('facade')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noFacadeImports',
|
||||
data: { importPath: exportPath },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { Rule, SourceCode } from 'eslint';
|
||||
import path from 'node:path';
|
||||
|
||||
type ImportOrExportSpecifier = Rule.Node & {
|
||||
importKind?: 'type' | 'value';
|
||||
exportKind?: 'type' | 'value';
|
||||
};
|
||||
|
||||
function normalizePath(filePath: string): string {
|
||||
return filePath.split(path.sep).join('/');
|
||||
}
|
||||
|
||||
function getRuleFilename(context: Rule.RuleContext): string {
|
||||
const filenameFromProperty = (context as { filename?: unknown }).filename;
|
||||
|
||||
if (typeof filenameFromProperty === 'string' && filenameFromProperty) {
|
||||
return filenameFromProperty;
|
||||
}
|
||||
|
||||
const getFilename = (context as { getFilename?: () => string }).getFilename;
|
||||
return typeof getFilename === 'function' ? getFilename.call(context) : '';
|
||||
}
|
||||
|
||||
function isSrcIndexFile(context: Rule.RuleContext): boolean {
|
||||
return normalizePath(getRuleFilename(context)).endsWith('/src/index.ts');
|
||||
}
|
||||
|
||||
function isInlineTypeSpecifier(specifier: ImportOrExportSpecifier, kind: 'import' | 'export'): boolean {
|
||||
return kind === 'import' ? specifier.importKind === 'type' : specifier.exportKind === 'type';
|
||||
}
|
||||
|
||||
function trimInlineTypeKeyword(specifierText: string): string {
|
||||
return specifierText.replace(/^type\s+/, '');
|
||||
}
|
||||
|
||||
function buildReplacement(
|
||||
sourceCode: SourceCode,
|
||||
node: Rule.Node,
|
||||
kind: 'import' | 'export'
|
||||
): string | null {
|
||||
const specifiers = (node as { specifiers?: ImportOrExportSpecifier[] }).specifiers;
|
||||
if (!specifiers?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typeSpecifiers = specifiers.filter((specifier) => isInlineTypeSpecifier(specifier, kind));
|
||||
const valueSpecifiers = specifiers.filter((specifier) => !isInlineTypeSpecifier(specifier, kind));
|
||||
if (typeSpecifiers.length === 0 || valueSpecifiers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeText = sourceCode.getText(node);
|
||||
const openingBraceIndex = nodeText.indexOf('{');
|
||||
const closingBraceIndex = nodeText.lastIndexOf('}');
|
||||
if (openingBraceIndex === -1 || closingBraceIndex === -1 || closingBraceIndex < openingBraceIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const suffixWithSemicolon = nodeText.slice(closingBraceIndex + 1);
|
||||
const hasSemicolon = suffixWithSemicolon.trimEnd().endsWith(';');
|
||||
const suffix = hasSemicolon ? suffixWithSemicolon.replace(/;\s*$/, '') : suffixWithSemicolon;
|
||||
const semicolon = hasSemicolon ? ';' : '';
|
||||
const valueSpecifierText = valueSpecifiers.map((specifier) => sourceCode.getText(specifier)).join(', ');
|
||||
const typeSpecifierText = typeSpecifiers
|
||||
.map((specifier) => trimInlineTypeKeyword(sourceCode.getText(specifier)))
|
||||
.join(', ');
|
||||
|
||||
return `${kind} { ${valueSpecifierText} }${suffix}${semicolon}\n${kind} type { ${typeSpecifierText} }${suffix}${semicolon}`;
|
||||
}
|
||||
|
||||
const rule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow mixing inline type specifiers with value specifiers in src/index.ts',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
noMixedExportSpecifiers: 'Do not mix type and value export specifiers in src/index.ts.',
|
||||
noMixedImportSpecifiers: 'Do not mix type and value import specifiers in src/index.ts.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
if (!isSrcIndexFile(context)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node: Rule.Node) {
|
||||
const replacement = buildReplacement(context.sourceCode, node, 'import');
|
||||
if (!replacement) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noMixedImportSpecifiers',
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node, replacement);
|
||||
},
|
||||
});
|
||||
},
|
||||
ExportNamedDeclaration(node: Rule.Node) {
|
||||
const replacement = buildReplacement(context.sourceCode, node, 'export');
|
||||
if (!replacement) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noMixedExportSpecifiers',
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node, replacement);
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
95
common/shared/eslint/plugins/no-self-package-imports.ts
Normal file
95
common/shared/eslint/plugins/no-self-package-imports.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { Rule } from 'eslint';
|
||||
import path from 'node:path';
|
||||
|
||||
function getImportSourceValue(node: Rule.Node): string | null {
|
||||
if (!('source' in node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const source = (node as { source?: { value?: unknown } }).source;
|
||||
if (!source || typeof source.value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return source.value;
|
||||
}
|
||||
|
||||
function getRuleFilename(context: Rule.RuleContext): string {
|
||||
const filenameFromProperty = (context as { filename?: unknown }).filename;
|
||||
|
||||
if (typeof filenameFromProperty === 'string' && filenameFromProperty) {
|
||||
return filenameFromProperty;
|
||||
}
|
||||
|
||||
const getFilename = (context as { getFilename?: () => string }).getFilename;
|
||||
return typeof getFilename === 'function' ? getFilename.call(context) : '';
|
||||
}
|
||||
|
||||
const rule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow self package imports in packages directory except facade',
|
||||
},
|
||||
messages: {
|
||||
noSelfImport: 'Package cannot import itself: "{{importPath}}" in {{packageName}}',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = getRuleFilename(context);
|
||||
const normalizedPath = filename.split(path.sep).join('/');
|
||||
|
||||
const isInPackages = normalizedPath.includes('/packages/');
|
||||
const isInFacade = normalizedPath.includes('/facade/');
|
||||
|
||||
if (!isInPackages || isInFacade) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// get parent dir
|
||||
const parentDirMatch = normalizedPath.match(/\/([^/]+)\/packages\//);
|
||||
if (!parentDirMatch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const parentDir = parentDirMatch[1];
|
||||
const packagePrefix = parentDir === 'univer'
|
||||
? '@univerjs/' :
|
||||
parentDir === 'univer-pro'
|
||||
? '@univerjs-pro/' :
|
||||
null;
|
||||
|
||||
if (!packagePrefix) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// get package name
|
||||
const packageMatch = normalizedPath.match(/\/packages\/([^/]+)/);
|
||||
if (!packageMatch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const packageName = packageMatch[1];
|
||||
const possiblePackageName = `${packagePrefix}${packageName}`;
|
||||
|
||||
return {
|
||||
ImportDeclaration(node: Rule.Node) {
|
||||
const importPath = getImportSourceValue(node);
|
||||
|
||||
if (importPath && importPath === possiblePackageName) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noSelfImport',
|
||||
data: {
|
||||
importPath,
|
||||
packageName: importPath,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
296
common/shared/eslint/presets.ts
Normal file
296
common/shared/eslint/presets.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
import type { ESLint, Linter } from 'eslint';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fixupPluginRules } from '@eslint/compat';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import eslintPluginBetterTailwindcss from 'eslint-plugin-better-tailwindcss';
|
||||
import header from 'eslint-plugin-header';
|
||||
import barrel from 'eslint-plugin-no-barrel-import';
|
||||
import penetrating from 'eslint-plugin-no-penetrating-import';
|
||||
import noExternalImportsInFacade from './plugins/no-external-imports-in-facade';
|
||||
import noFacadeImportsOutsideFacade from './plugins/no-facade-imports-outside-facade';
|
||||
import noMixedTypeSpecifiersInIndex from './plugins/no-mixed-type-specifiers-in-index';
|
||||
import noSelfPackageImports from './plugins/no-self-package-imports';
|
||||
|
||||
const univerPlugin = {
|
||||
rules: {
|
||||
'no-external-imports-in-facade': noExternalImportsInFacade,
|
||||
'no-self-package-imports': noSelfPackageImports,
|
||||
'no-facade-imports-outside-facade': noFacadeImportsOutsideFacade,
|
||||
'no-mixed-type-specifiers-in-index': noMixedTypeSpecifiersInIndex,
|
||||
},
|
||||
} satisfies ESLint.Plugin;
|
||||
|
||||
/**
|
||||
* TypeScript preset configuration for ESLint.
|
||||
* Applies TypeScript-specific rules and custom Univer rules.
|
||||
*
|
||||
* @returns ESLint configuration object for TypeScript files
|
||||
*/
|
||||
export const typescriptPreset = (): Linter.Config => {
|
||||
return {
|
||||
files: ['**/*.{ts,tsx,mts,cts}'],
|
||||
rules: {
|
||||
'ts/naming-convention': [
|
||||
'warn',
|
||||
// Interfaces' names should start with a capital 'I'.
|
||||
{
|
||||
selector: 'interface',
|
||||
format: ['PascalCase'],
|
||||
custom: {
|
||||
regex: '^I[A-Z0-9]',
|
||||
match: true,
|
||||
},
|
||||
},
|
||||
// Private fields of a class should start with an underscore '_'.
|
||||
{
|
||||
selector: ['classMethod', 'classProperty'],
|
||||
modifiers: ['private'],
|
||||
format: ['camelCase'],
|
||||
leadingUnderscore: 'require',
|
||||
},
|
||||
],
|
||||
// 'ts/consistent-type-exports': 'warn',
|
||||
},
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Index entry preset configuration for ESLint.
|
||||
* Keeps type-only imports and exports separate from value imports and exports.
|
||||
*
|
||||
* @returns ESLint configuration object for package entry files
|
||||
*/
|
||||
export const indexEntryPreset = (): Linter.Config => {
|
||||
return {
|
||||
files: ['**/src/index.ts'],
|
||||
rules: {
|
||||
'univer/no-mixed-type-specifiers-in-index': 'error',
|
||||
},
|
||||
plugins: {
|
||||
univer: univerPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Univer source code preset configuration.
|
||||
* Enforces package structure rules for source files.
|
||||
*
|
||||
* @returns ESLint configuration object for Univer source files
|
||||
*/
|
||||
export const univerSourcePreset = (options: {
|
||||
noFacadeImportsOutsideFacade?: {
|
||||
ignore?: string[];
|
||||
};
|
||||
} = {}): Linter.Config => {
|
||||
return {
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
ignores: [
|
||||
'**/__tests__/**/*',
|
||||
'**/*.spec.ts',
|
||||
'**/*.test.ts',
|
||||
],
|
||||
rules: {
|
||||
'univer/no-self-package-imports': 'error',
|
||||
'univer/no-facade-imports-outside-facade': ['error', {
|
||||
ignore: options.noFacadeImportsOutsideFacade?.ignore ?? [],
|
||||
}],
|
||||
},
|
||||
plugins: {
|
||||
univer: univerPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Facade preset configuration for ESLint.
|
||||
* Enforces strict typing and import rules for facade files.
|
||||
*
|
||||
* @returns ESLint configuration object for facade files
|
||||
*/
|
||||
export const facadePreset = (): Linter.Config => {
|
||||
return {
|
||||
files: ['**/src/facade/**/*.ts'],
|
||||
ignores: [
|
||||
'**/__tests__/**/*',
|
||||
'**/*.spec.ts',
|
||||
'**/*.test.ts',
|
||||
],
|
||||
rules: {
|
||||
'ts/explicit-function-return-type': 'error',
|
||||
'univer/no-external-imports-in-facade': 'error',
|
||||
},
|
||||
plugins: {
|
||||
univer: univerPlugin,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Tailwind CSS preset configuration for ESLint.
|
||||
* Enforces Tailwind CSS class usage and formatting rules.
|
||||
*
|
||||
* @returns ESLint configuration object for files using Tailwind CSS
|
||||
*/
|
||||
export const tailwindcssPreset = (): Linter.Config => {
|
||||
const isWindows = os.platform() === 'win32';
|
||||
const lineBreakStyle = isWindows ? 'windows' : 'unix';
|
||||
|
||||
return {
|
||||
files: ['**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'better-tailwindcss': eslintPluginBetterTailwindcss,
|
||||
},
|
||||
settings: {
|
||||
'better-tailwindcss': {
|
||||
tailwindConfig: path.resolve(__dirname, '../tailwind/tailwind.config.ts'),
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// enable all recommended rules to error
|
||||
...eslintPluginBetterTailwindcss.configs['recommended-error'].rules,
|
||||
'better-tailwindcss/enforce-consistent-line-wrapping': ['error', {
|
||||
printWidth: 120,
|
||||
group: 'newLine',
|
||||
lineBreakStyle,
|
||||
}],
|
||||
'better-tailwindcss/no-unknown-classes': 'off',
|
||||
'better-tailwindcss/no-conflicting-classes': 'off',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Test/spec files preset configuration for ESLint.
|
||||
* Relaxed rules for test files to allow more flexible testing patterns.
|
||||
*
|
||||
* @returns ESLint configuration object for test files
|
||||
*/
|
||||
export const specPreset = (): Linter.Config => {
|
||||
return {
|
||||
files: [
|
||||
'**/*.spec.ts',
|
||||
'**/__tests__/**/*.ts',
|
||||
],
|
||||
rules: {
|
||||
'ts/explicit-function-return-type': 'off',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Penetrating import preset configuration for ESLint.
|
||||
* Prevents direct imports from internal package directories.
|
||||
*
|
||||
* @returns ESLint configuration object for preventing penetrating imports
|
||||
*/
|
||||
export const penetratingPreset = (): Linter.Config => {
|
||||
return {
|
||||
// Not penetrating for source files
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
plugins: {
|
||||
penetrating: fixupPluginRules(penetrating),
|
||||
},
|
||||
ignores: [
|
||||
'**/__tests__/**/*',
|
||||
'**/__testing__/**/*',
|
||||
'examples/**/*',
|
||||
],
|
||||
rules: {
|
||||
'penetrating/no-penetrating-import': 2,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* No barrel import preset configuration for ESLint.
|
||||
* Prevents barrel imports and enforces code complexity limits.
|
||||
*
|
||||
* @returns ESLint configuration object for preventing barrel imports
|
||||
*/
|
||||
export const noBarrelImportPreset = (): Linter.Config => {
|
||||
return {
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
ignores: [
|
||||
'packages/engine-render/src/components/docs/**/*.ts',
|
||||
'**/*.tsx',
|
||||
'**/*.d.ts',
|
||||
'**/tsdown.config.ts',
|
||||
'**/*.spec.ts',
|
||||
'**/*.spec.tsx',
|
||||
'**/*.test.ts',
|
||||
'**/*.test.tsx',
|
||||
], // do not check test files
|
||||
plugins: {
|
||||
barrel: fixupPluginRules(barrel),
|
||||
},
|
||||
rules: {
|
||||
'barrel/no-barrel-import': 2,
|
||||
complexity: ['warn', { max: 20 }],
|
||||
'max-lines-per-function': ['warn', 80],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Header preset configuration for ESLint.
|
||||
* Enforces Apache License 2.0 header in package and preset source files.
|
||||
*
|
||||
* @returns ESLint configuration object for enforcing file headers
|
||||
*/
|
||||
export const headerPreset = (): Linter.Config => {
|
||||
header.rules.header.meta.schema = false;
|
||||
|
||||
return {
|
||||
files: ['{packages,presets}/**/*.{ts,tsx}'],
|
||||
ignores: [
|
||||
'**/*.d.ts',
|
||||
'**/vitest.config.ts',
|
||||
'**/vitest.workspace.ts',
|
||||
],
|
||||
plugins: {
|
||||
header: fixupPluginRules(header),
|
||||
},
|
||||
rules: {
|
||||
'header/header': [
|
||||
2,
|
||||
'block',
|
||||
[
|
||||
'*',
|
||||
' * Copyright 2023-present DreamNum Co., Ltd.',
|
||||
' *',
|
||||
' * Licensed under the Apache License, Version 2.0 (the "License");',
|
||||
' * you may not use this file except in compliance with the License.',
|
||||
' * You may obtain a copy of the License at',
|
||||
' *',
|
||||
' * http://www.apache.org/licenses/LICENSE-2.0',
|
||||
' *',
|
||||
' * Unless required by applicable law or agreed to in writing, software',
|
||||
' * distributed under the License is distributed on an "AS IS" BASIS,',
|
||||
' * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.',
|
||||
' * See the License for the specific language governing permissions and',
|
||||
' * limitations under the License.',
|
||||
' ',
|
||||
],
|
||||
2,
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
201
common/shared/eslint/rules.ts
Normal file
201
common/shared/eslint/rules.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import type { Rules } from '@antfu/eslint-config';
|
||||
|
||||
/**
|
||||
* Base ESLint rules configuration for Univer project.
|
||||
* These rules apply to all TypeScript and JavaScript files.
|
||||
*/
|
||||
const qualityRules: Partial<Rules> = {
|
||||
// Code style and formatting rules
|
||||
curly: ['error', 'multi-line'],
|
||||
'antfu/if-newline': 'off',
|
||||
'no-param-reassign': ['warn'],
|
||||
'eol-last': ['error', 'always'],
|
||||
'no-empty-function': 'off',
|
||||
};
|
||||
|
||||
const typescriptRules: Partial<Rules> = {
|
||||
// TypeScript specific rules
|
||||
'ts/no-explicit-any': 'warn',
|
||||
'ts/no-redeclare': 'off',
|
||||
'ts/method-signature-style': 'off',
|
||||
};
|
||||
|
||||
const styleRules: Partial<Rules> = {
|
||||
// Style and formatting rules
|
||||
'style/no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }],
|
||||
'style/brace-style': ['warn', '1tbs', { allowSingleLine: true }],
|
||||
'style/jsx-first-prop-new-line': ['warn', 'multiline'],
|
||||
'style/arrow-parens': ['error', 'always'],
|
||||
'style/spaced-comment': 'off',
|
||||
'style/indent-binary-ops': 'off',
|
||||
'style/operator-linebreak': 'off',
|
||||
'style/indent': ['error', 4, {
|
||||
ObjectExpression: 'first',
|
||||
SwitchCase: 1,
|
||||
ignoreComments: true,
|
||||
}],
|
||||
'style/quotes': ['warn', 'single', { avoidEscape: true }],
|
||||
'style/jsx-closing-tag-location': 'warn',
|
||||
'style/jsx-curly-newline': ['warn', { multiline: 'forbid', singleline: 'forbid' }],
|
||||
'style/jsx-wrap-multilines': 'warn',
|
||||
'style/quote-props': ['warn', 'as-needed'],
|
||||
'style/jsx-curly-brace-presence': 'warn',
|
||||
'style/multiline-ternary': 'warn',
|
||||
|
||||
// Comma and punctuation rules
|
||||
'style/comma-dangle': ['error', {
|
||||
arrays: 'always-multiline',
|
||||
objects: 'always-multiline',
|
||||
imports: 'always-multiline',
|
||||
exports: 'always-multiline',
|
||||
enums: 'always-multiline',
|
||||
functions: 'never',
|
||||
}],
|
||||
};
|
||||
|
||||
const filenameRules: Partial<Rules> = {
|
||||
// Filename conventions
|
||||
'unicorn/filename-case': [
|
||||
'error',
|
||||
{
|
||||
cases: {
|
||||
kebabCase: true,
|
||||
pascalCase: true,
|
||||
},
|
||||
ignore: [
|
||||
'^README-(\w+)?\\.md$',
|
||||
'^[a-z]{2}-[A-Z]{2}\.ts$',
|
||||
'^__tests__$',
|
||||
'^__testing__$',
|
||||
'^FUNDING.yml$',
|
||||
'^bug_report.yml$',
|
||||
'^bug_report.zh-CN.yml$',
|
||||
'^feature_request.yml$',
|
||||
'^feature_request.zh-CN.yml$',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const reactRules: Partial<Rules> = {
|
||||
// JSX and React rules
|
||||
'style/jsx-self-closing-comp': ['error', {
|
||||
component: true,
|
||||
html: true,
|
||||
}],
|
||||
'react-refresh/only-export-components': 'off',
|
||||
'react/no-unstable-context-value': 'warn',
|
||||
'react/no-unstable-default-props': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/no-create-ref': 'warn',
|
||||
'react/rules-of-hooks': 'off',
|
||||
'react/static-components': 'off',
|
||||
'react/dom-no-render': 'off',
|
||||
'react/unsupported-syntax': 'off',
|
||||
'react/jsx-no-key-after-spread': 'off',
|
||||
'react/use-memo': 'warn',
|
||||
'react-hooks/static-components': 'off',
|
||||
'react-hooks/preserve-manual-memoization': 'off',
|
||||
'react-hooks/set-state-in-effect': 'off',
|
||||
'react-hooks/refs': 'off',
|
||||
'react-hooks/immutability': 'warn',
|
||||
'react-hooks/use-memo': 'warn',
|
||||
'react-hooks/purity': 'warn',
|
||||
'react-hooks/globals': 'warn',
|
||||
};
|
||||
|
||||
const importRules: Partial<Rules> = {
|
||||
// Import and export rules
|
||||
'sort-imports': [
|
||||
'error',
|
||||
{
|
||||
allowSeparatedGroups: false,
|
||||
ignoreCase: true,
|
||||
ignoreDeclarationSort: true,
|
||||
ignoreMemberSort: false,
|
||||
memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
|
||||
},
|
||||
],
|
||||
'perfectionist/sort-imports': 'warn',
|
||||
'perfectionist/sort-named-imports': 'off',
|
||||
'perfectionist/sort-named-exports': 'warn',
|
||||
|
||||
// Code quality rules
|
||||
'accessor-pairs': 'warn',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
'jsdoc/tag-lines': 'off',
|
||||
|
||||
// Restricted imports for consistency
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'clsx',
|
||||
message: 'Please use `import { clsx } from \'@univerjs/design\'` instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const disabledDebatableRules: Partial<Rules> = {
|
||||
// IMPORTANT: To ensure compatibility, some features of React 19 will be disabled.
|
||||
'react/no-forward-ref': 'off',
|
||||
'react/no-context-provider': 'off',
|
||||
'react-dom/no-render': 'off',
|
||||
'react/no-use-context': 'off',
|
||||
|
||||
// Debatable rules - currently disabled
|
||||
'test/prefer-lowercase-title': 'off',
|
||||
'antfu/top-level-function': 'off',
|
||||
'unicorn/no-new-array': 'off',
|
||||
'unicorn/prefer-includes': 'off',
|
||||
'prefer-arrow-callback': 'off',
|
||||
'no-restricted-globals': 'off',
|
||||
'unicorn/prefer-string-starts-ends-with': 'warn',
|
||||
};
|
||||
|
||||
const migrationWarningRules: Partial<Rules> = {
|
||||
// Compatibility rules for legacy code - set to warn for gradual migration
|
||||
'unused-imports/no-unused-vars': 'warn',
|
||||
'ts/no-restricted-types': 'warn',
|
||||
'ts/no-wrapper-object-types': 'warn',
|
||||
'ts/no-empty-object-type': 'warn',
|
||||
'ts/no-unsafe-function-type': 'warn',
|
||||
'ts/no-unused-expressions': 'warn',
|
||||
'unicorn/prefer-dom-node-text-content': 'warn',
|
||||
'unicorn/prefer-number-properties': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'eslint-comments/no-unlimited-disable': 'off',
|
||||
'ts/prefer-ts-expect-error': 'off',
|
||||
'ts/ban-ts-comment': 'off',
|
||||
'ts/no-duplicate-enum-values': 'off',
|
||||
'no-cond-assign': 'warn',
|
||||
'ts/no-use-before-define': 'warn',
|
||||
'test/no-identical-title': 'warn',
|
||||
'ts/no-non-null-asserted-optional-chain': 'warn',
|
||||
'no-restricted-syntax': 'warn',
|
||||
'prefer-regex-literals': 'warn',
|
||||
'ts/no-this-alias': 'warn',
|
||||
'prefer-promise-reject-errors': 'warn',
|
||||
'no-new': 'warn',
|
||||
'unicorn/error-message': 'warn',
|
||||
'ts/prefer-literal-enum-member': 'warn',
|
||||
'no-control-regex': 'warn',
|
||||
'ts/no-import-type-side-effects': 'warn',
|
||||
'unicorn/number-literal-case': 'warn',
|
||||
'unicorn/prefer-type-error': 'warn',
|
||||
};
|
||||
|
||||
export const baseRules: Partial<Rules> = {
|
||||
...qualityRules,
|
||||
...typescriptRules,
|
||||
...styleRules,
|
||||
...filenameRules,
|
||||
...reactRules,
|
||||
...importRules,
|
||||
...disabledDebatableRules,
|
||||
...migrationWarningRules,
|
||||
};
|
||||
3
common/shared/eslint/third-party.d.ts
vendored
Normal file
3
common/shared/eslint/third-party.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module 'eslint-plugin-header';
|
||||
declare module 'eslint-plugin-no-barrel-import';
|
||||
declare module 'eslint-plugin-no-penetrating-import';
|
||||
Reference in New Issue
Block a user