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:
@@ -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;
|
||||
Reference in New Issue
Block a user