@taiga-ui/eslint-plugin-experience-next 0.369.0 → 0.370.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -28,7 +28,6 @@ declare const plugin: {
28
28
  'prefer-deep-imports': import("@typescript-eslint/utils/ts-eslint").RuleModule<"prefer-deep-imports", [{
29
29
  importFilter: string[] | string;
30
30
  strict?: boolean;
31
- mockPaths?: Record<string, string>;
32
31
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
33
32
  'short-tui-imports': import("@typescript-eslint/utils/ts-eslint").RuleModule<"replaceTuiImport", import("./rules/short-tui-imports").Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
34
33
  'standalone-imports-sort': import("@typescript-eslint/utils/ts-eslint").RuleModule<"incorrectOrder", [{
package/index.esm.js CHANGED
@@ -1930,37 +1930,63 @@ function getClass(node) {
1930
1930
  }
1931
1931
 
1932
1932
  const MESSAGE_ID$1 = 'prefer-deep-imports';
1933
- const ERROR_MESSAGE = 'Import via root or non-leaf entry points is prohibited for this package';
1933
+ const ERROR_MESSAGE = 'Import via root entry point is prohibited when nested entry points exist';
1934
+ const createRule$3 = ESLintUtils.RuleCreator(() => ERROR_MESSAGE);
1935
+ const tsconfigCache = new Map();
1934
1936
  const moduleResolutionCache = new Map();
1935
- const entryPointCache = new Map();
1936
- const entryRootCache = new Map();
1937
- const createRule$3 = ESLintUtils.RuleCreator((name) => name);
1937
+ const exportCheckCache = new Map();
1938
+ const nearestNgCache = new Map();
1939
+ const tsFileCache = new Map();
1938
1940
  var preferDeepImports = createRule$3({
1939
- create(context, [{ importFilter, mockPaths, strict = false }]) {
1940
- const selector = `ImportDeclaration[source.value=${getFilterRegExp(importFilter, strict)}]`;
1941
+ create(context, [options]) {
1942
+ const allowedPackages = normalizeFilter(options.importFilter);
1943
+ const strict = options.strict ?? false;
1941
1944
  return {
1942
- [selector](node) {
1943
- const specifier = node.source.value;
1944
- const resolvedFile = resolveAlias(specifier, context, mockPaths);
1945
- if (!resolvedFile) {
1945
+ ImportDeclaration(node) {
1946
+ const importPath = node.source.value;
1947
+ if (typeof importPath !== 'string') {
1946
1948
  return;
1947
1949
  }
1948
- const entryRoot = findEntryRoot(resolvedFile) ?? path.dirname(resolvedFile);
1949
- if (strict) {
1950
- const nested = isNestedEntryPoint(specifier);
1951
- const hasNested = checkNestedEntryPoints(entryRoot, mockPaths);
1952
- if (nested && !hasNested) {
1953
- return;
1950
+ const shortName = extractPackageName(importPath);
1951
+ const allowed = allowedPackages.some((pkg) => {
1952
+ if (shortName && pkg instanceof RegExp) {
1953
+ return pkg.test(shortName);
1954
1954
  }
1955
+ return pkg === shortName;
1956
+ });
1957
+ if (!allowed) {
1958
+ return;
1959
+ }
1960
+ if (!strict && isNestedImport(importPath)) {
1961
+ return;
1962
+ }
1963
+ const symbols = extractImportedSymbols(node);
1964
+ if (symbols.length === 0) {
1965
+ return;
1966
+ }
1967
+ const filename = context.filename;
1968
+ const rootEntry = resolveRootEntryPoint(importPath, filename);
1969
+ if (!rootEntry) {
1970
+ return context.report({
1971
+ messageId: MESSAGE_ID$1,
1972
+ node,
1973
+ });
1974
+ }
1975
+ const nested = findNestedEntryPoints(rootEntry);
1976
+ if (nested.length === 0) {
1977
+ return context.report({
1978
+ messageId: MESSAGE_ID$1,
1979
+ node,
1980
+ });
1955
1981
  }
1956
- const fixed = tryFixImport(node, entryRoot);
1957
- if (!fixed) {
1958
- context.report({ messageId: MESSAGE_ID$1, node });
1982
+ const symbolMap = mapSymbolsToEntryPoints(rootEntry, nested, symbols, strict);
1983
+ if (symbolMap.size === 0) {
1959
1984
  return;
1960
1985
  }
1986
+ const newImports = buildImports(node, importPath, symbolMap);
1961
1987
  context.report({
1962
1988
  fix(fixer) {
1963
- return fixer.replaceTextRange([node.range[0], node.range[1]], fixed);
1989
+ return fixer.replaceTextRange(node.range, newImports);
1964
1990
  },
1965
1991
  messageId: MESSAGE_ID$1,
1966
1992
  node,
@@ -1983,7 +2009,6 @@ var preferDeepImports = createRule$3({
1983
2009
  additionalProperties: false,
1984
2010
  properties: {
1985
2011
  importFilter: { type: ['string', 'array'] },
1986
- mockPaths: { type: 'object' },
1987
2012
  strict: { type: 'boolean' },
1988
2013
  },
1989
2014
  type: 'object',
@@ -1993,140 +2018,244 @@ var preferDeepImports = createRule$3({
1993
2018
  },
1994
2019
  name: 'prefer-deep-imports',
1995
2020
  });
1996
- function resolveAlias(specifier, context, mockPaths) {
1997
- if (mockPaths && specifier in mockPaths) {
1998
- return mockPaths[specifier] ?? null;
2021
+ function extractPackageName(importPath) {
2022
+ if (!importPath.startsWith('@')) {
2023
+ return null;
1999
2024
  }
2000
- if (moduleResolutionCache.has(specifier)) {
2001
- return moduleResolutionCache.get(specifier);
2025
+ const p = importPath.split('/');
2026
+ return p.length >= 2 ? `${p[0]}/${p[1]}` : null;
2027
+ }
2028
+ function isNestedImport(importPath) {
2029
+ return importPath.split('/').length > 2;
2030
+ }
2031
+ function extractImportedSymbols(node) {
2032
+ return node.specifiers
2033
+ .filter((s) => s.type === AST_NODE_TYPES$1.ImportSpecifier)
2034
+ .map((s) => s.imported.type === AST_NODE_TYPES$1.Identifier
2035
+ ? s.imported.name
2036
+ : s.imported.value);
2037
+ }
2038
+ function resolveRootEntryPoint(importPath, fromFile) {
2039
+ const key = `${importPath}|${fromFile}`;
2040
+ if (moduleResolutionCache.has(key)) {
2041
+ return moduleResolutionCache.get(key);
2002
2042
  }
2003
- let result;
2004
- try {
2005
- const services = ESLintUtils.getParserServices(context);
2006
- const program = services.program;
2007
- const compilerOptions = program.getCompilerOptions();
2008
- const resolved = ts.resolveModuleName(specifier, context.filename, compilerOptions, ts.sys);
2009
- result = resolved.resolvedModule?.resolvedFileName ?? null;
2043
+ const tsconfig = findNearestTsconfig(fromFile);
2044
+ if (!tsconfig) {
2045
+ moduleResolutionCache.set(key, null);
2046
+ return null;
2010
2047
  }
2011
- catch {
2012
- result = null;
2048
+ let parsed = tsconfigCache.get(tsconfig);
2049
+ if (!parsed) {
2050
+ const json = ts.readConfigFile(tsconfig, ts.sys.readFile).config;
2051
+ parsed = ts.parseJsonConfigFileContent(json, ts.sys, path.dirname(tsconfig));
2052
+ tsconfigCache.set(tsconfig, parsed);
2013
2053
  }
2014
- moduleResolutionCache.set(specifier, result);
2015
- return result;
2016
- }
2017
- function getImportedName$1(node) {
2018
- return node.type === AST_NODE_TYPES$1.Identifier ? node.name : node.value;
2054
+ const resolved = ts.resolveModuleName(importPath, fromFile, parsed.options, ts.sys).resolvedModule;
2055
+ const value = resolved ? path.dirname(resolved.resolvedFileName) : null;
2056
+ moduleResolutionCache.set(key, value);
2057
+ return value;
2019
2058
  }
2020
- function tryFixImport(node, entryRoot) {
2021
- const specifiers = node.specifiers;
2022
- const isTypeOnly = node.importKind === 'type';
2023
- const allTsFiles = globSync(`${entryRoot}/**/*.ts`, {
2024
- ignore: { ignored: (p) => /\.(spec|cy)\.ts$/.test(p.name) },
2025
- });
2026
- const sourceFiles = specifiers.map((sp) => {
2027
- if (sp.type !== AST_NODE_TYPES$1.ImportSpecifier) {
2028
- return null;
2059
+ function findNearestTsconfig(start) {
2060
+ let dir = path.dirname(start);
2061
+ const limit = process.cwd();
2062
+ while (dir.startsWith(limit)) {
2063
+ const candidate = path.join(dir, 'tsconfig.json');
2064
+ if (fs.existsSync(candidate)) {
2065
+ return candidate;
2029
2066
  }
2030
- const importedName = getImportedName$1(sp.imported);
2031
- const found = allTsFiles.find((filePath) => {
2032
- const content = fs.readFileSync(filePath, 'utf8');
2033
- const regExp = new RegExp(String.raw `(?<=export\s(default\s)?(abstract\s)?\w+\s)\b${importedName}\b`);
2034
- return regExp.test(content);
2035
- });
2036
- return found?.replaceAll(/\\+/g, '/');
2037
- });
2038
- if (sourceFiles.some((x) => !x)) {
2039
- return null;
2067
+ const parent = path.dirname(dir);
2068
+ if (parent === dir) {
2069
+ break;
2070
+ }
2071
+ dir = parent;
2040
2072
  }
2041
- const entryPoints = sourceFiles.map(findNearestEntryPoint);
2042
- if (entryPoints.some((x) => !x)) {
2043
- return null;
2073
+ return null;
2074
+ }
2075
+ function findNestedEntryPoints(root) {
2076
+ const cached = tsFileCache.get(`${root}|ng`);
2077
+ if (cached) {
2078
+ return cached;
2044
2079
  }
2045
- const newImports = specifiers.map((sp, i) => {
2046
- if (sp.type !== AST_NODE_TYPES$1.ImportSpecifier) {
2047
- return null;
2080
+ const found = globSync('**/ng-package.json', { absolute: false, cwd: root })
2081
+ .map((p) => p.replaceAll('\\', '/'))
2082
+ .map((p) => p.replace('/ng-package.json', ''))
2083
+ .filter((dir) => dir !== '.');
2084
+ tsFileCache.set(`${root}|ng`, found);
2085
+ return found;
2086
+ }
2087
+ function mapSymbolsToEntryPoints(root, nested, symbols, strict) {
2088
+ const result = new Map();
2089
+ const remaining = new Set(symbols);
2090
+ for (const np of nested) {
2091
+ if (remaining.size === 0) {
2092
+ break;
2093
+ }
2094
+ const full = path.join(root, np);
2095
+ let files = tsFileCache.get(full);
2096
+ if (!files) {
2097
+ files = globSync('**/*.ts', {
2098
+ absolute: true,
2099
+ cwd: full,
2100
+ ignore: ['**/*.spec.ts', '**/*.cy.ts'],
2101
+ });
2102
+ tsFileCache.set(full, files);
2103
+ }
2104
+ for (const file of files) {
2105
+ const content = safeReadFile(file);
2106
+ if (!content) {
2107
+ continue;
2108
+ }
2109
+ for (const symbol of Array.from(remaining)) {
2110
+ const has = cachedContainsDirectExport(file, content, symbol) ||
2111
+ cachedContainsReExport(file, content, symbol);
2112
+ if (!has) {
2113
+ continue;
2114
+ }
2115
+ const nearest = cachedNearestNg(file, root);
2116
+ if (!nearest) {
2117
+ continue;
2118
+ }
2119
+ let suffix = path.relative(root, nearest).replaceAll('\\', '/');
2120
+ if (!strict && suffix.includes('/')) {
2121
+ suffix = suffix.split('/')[0];
2122
+ }
2123
+ result.set(symbol, suffix);
2124
+ remaining.delete(symbol);
2125
+ }
2048
2126
  }
2049
- const imported = getImportedName$1(sp.imported);
2050
- const local = sp.local.name;
2051
- const mapped = imported === local ? imported : `${imported} as ${local}`;
2052
- return `import ${isTypeOnly ? 'type ' : ''}{${mapped}} from '${entryPoints[i]}';`;
2053
- });
2054
- if (newImports.some((x) => x === null)) {
2055
- return null;
2056
2127
  }
2057
- return newImports.join('\n');
2128
+ return result;
2058
2129
  }
2059
- function findEntryRoot(resolvedFile) {
2060
- if (entryRootCache.has(resolvedFile)) {
2061
- return entryRootCache.get(resolvedFile);
2130
+ function cachedContainsDirectExport(file, content, symbol) {
2131
+ let cache = exportCheckCache.get(file);
2132
+ if (!cache) {
2133
+ cache = new Map();
2134
+ exportCheckCache.set(file, cache);
2062
2135
  }
2063
- let dir = path.dirname(resolvedFile);
2064
- while (dir !== path.dirname(dir)) {
2065
- if (fs.existsSync(path.join(dir, 'ng-package.json'))) {
2066
- entryRootCache.set(resolvedFile, dir);
2067
- return dir;
2068
- }
2069
- dir = path.dirname(dir);
2136
+ const key = `direct:${symbol}`;
2137
+ if (cache.has(key)) {
2138
+ return cache.get(key);
2070
2139
  }
2071
- entryRootCache.set(resolvedFile, null);
2072
- return null;
2140
+ const res = containsDirectExport(content, symbol);
2141
+ cache.set(key, res);
2142
+ return res;
2073
2143
  }
2074
- function findNearestEntryPoint(filePath) {
2075
- if (!filePath) {
2076
- return '';
2144
+ function cachedContainsReExport(file, content, symbol) {
2145
+ let cache = exportCheckCache.get(file);
2146
+ if (!cache) {
2147
+ cache = new Map();
2148
+ exportCheckCache.set(file, cache);
2077
2149
  }
2078
- const parts = filePath.split('/');
2079
- for (let i = parts.length - 1; i >= 0; i--) {
2080
- const candidate = parts.slice(0, i).join('/');
2081
- if (fs.existsSync(`${candidate}/ng-package.json`)) {
2082
- return candidate;
2083
- }
2150
+ const key = `re:${symbol}`;
2151
+ if (cache.has(key)) {
2152
+ return cache.get(key);
2084
2153
  }
2085
- return '';
2154
+ const res = containsReExport(file, content, symbol);
2155
+ cache.set(key, res);
2156
+ return res;
2086
2157
  }
2087
- function checkNestedEntryPoints(entryRoot, mockPaths) {
2088
- if (mockPaths && Object.keys(mockPaths).length > 0) {
2089
- const normRoot = entryRoot.replaceAll('\\', '/').replace(/\/+$/, '');
2090
- const hasNestedMock = Object.values(mockPaths).some((resolved) => {
2091
- const dir = path.dirname(resolved).replaceAll('\\', '/');
2092
- return dir.startsWith(`${normRoot}/`) && dir !== normRoot;
2093
- });
2094
- if (hasNestedMock) {
2158
+ function containsDirectExport(content, symbol) {
2159
+ const re = new RegExp(String.raw `(?:export\s+(?:function|class|const|let|var)\s+${symbol}\b)|` +
2160
+ String.raw `(?:export\s*\{[^}]*\b${symbol}\b[^}]*\})`);
2161
+ return re.test(content);
2162
+ }
2163
+ function containsReExport(file, content, symbol) {
2164
+ const star = /export\s*\*\s*from\s*['"](.+)['"]/g;
2165
+ let m;
2166
+ while ((m = star.exec(content))) {
2167
+ const resolved = resolveReExport(file, m[1]);
2168
+ if (!resolved) {
2169
+ continue;
2170
+ }
2171
+ const nested = safeReadFile(resolved);
2172
+ if (nested && containsDirectExport(nested, symbol)) {
2095
2173
  return true;
2096
2174
  }
2097
2175
  }
2098
- if (entryPointCache.has(entryRoot)) {
2099
- return entryPointCache.get(entryRoot);
2176
+ const named = new RegExp(String.raw `export\s*\{[^}]*\b${symbol}\b[^}]*\}\s*from\s*['"](.+)['"]`);
2177
+ const nm = named.exec(content);
2178
+ if (nm) {
2179
+ const resolved = resolveReExport(file, nm[1]);
2180
+ if (!resolved) {
2181
+ return false;
2182
+ }
2183
+ const nested = safeReadFile(resolved);
2184
+ if (nested && containsDirectExport(nested, symbol)) {
2185
+ return true;
2186
+ }
2100
2187
  }
2101
- const nested = globSync(`${entryRoot}/**/ng-package.json`).map((path) => path.replaceAll(/\\+/g, '/'));
2102
- const hasNested = nested.some((path) => path !== `${entryRoot}/ng-package.json`);
2103
- entryPointCache.set(entryRoot, hasNested);
2104
- return hasNested;
2188
+ return false;
2105
2189
  }
2106
- function isNestedEntryPoint(specifier) {
2107
- const segments = specifier.split('/');
2108
- return specifier.startsWith('@') ? segments.length > 2 : segments.length > 1;
2190
+ function resolveReExport(file, rel) {
2191
+ const full = path.resolve(path.dirname(file), rel);
2192
+ return fs.existsSync(full) ? full : null;
2109
2193
  }
2110
- function getFilterRegExp(filter, strict) {
2111
- if (typeof filter === 'string' && filter.startsWith('/')) {
2112
- return strict ? extendRegExp(filter) : filter;
2194
+ function cachedNearestNg(file, root) {
2195
+ const key = `${file}|${root}`;
2196
+ if (nearestNgCache.has(key)) {
2197
+ return nearestNgCache.get(key);
2113
2198
  }
2114
- const packages = typeof filter === 'string' ? [filter] : filter;
2115
- const [npmScope] = packages[0]?.split('/') ?? [];
2116
- const names = packages.map((p) => p.split('/')[1]).filter(Boolean);
2117
- const base = String.raw `/^${npmScope}\u002F(${names.join('|')})$/`;
2118
- return strict ? extendRegExp(base) : base;
2199
+ let dir = path.dirname(file);
2200
+ while (dir.startsWith(root)) {
2201
+ const candidate = path.join(dir, 'ng-package.json');
2202
+ if (fs.existsSync(candidate)) {
2203
+ nearestNgCache.set(key, dir);
2204
+ return dir;
2205
+ }
2206
+ const parent = path.dirname(dir);
2207
+ if (parent === dir) {
2208
+ break;
2209
+ }
2210
+ dir = parent;
2211
+ }
2212
+ nearestNgCache.set(key, null);
2213
+ return null;
2119
2214
  }
2120
- function extendRegExp(regExp) {
2121
- const trimmed = regExp.trim();
2122
- const slash = trimmed.lastIndexOf('/');
2123
- if (!trimmed.startsWith('/') || slash <= 1) {
2124
- return regExp;
2215
+ function buildImports(node, baseImport, symbolMap) {
2216
+ const isTypeOnly = node.importKind === 'type';
2217
+ const groups = new Map();
2218
+ for (const [symbol, suffix] of symbolMap.entries()) {
2219
+ const target = suffix ? `${baseImport}/${suffix}` : baseImport;
2220
+ if (!groups.has(target)) {
2221
+ groups.set(target, []);
2222
+ }
2223
+ groups.get(target).push(symbol);
2224
+ }
2225
+ const result = [];
2226
+ for (const [target, symbols] of groups.entries()) {
2227
+ const importParts = symbols.map((symbol) => {
2228
+ const sp = node.specifiers.find((s) => s.type === AST_NODE_TYPES$1.ImportSpecifier &&
2229
+ (s.imported.type === AST_NODE_TYPES$1.Identifier
2230
+ ? s.imported.name === symbol
2231
+ : s.imported.value === symbol));
2232
+ if (!sp) {
2233
+ return symbol;
2234
+ }
2235
+ const local = sp.local.name;
2236
+ return symbol === local ? symbol : `${symbol} as ${local}`;
2237
+ });
2238
+ result.push(`import ${isTypeOnly ? 'type ' : ''}{${importParts.join(', ')}} from '${target}';`);
2125
2239
  }
2126
- const flags = trimmed.slice(slash + 1);
2127
- const pattern = trimmed.slice(1, slash);
2128
- const strictPattern = String.raw `${pattern.replace(/\$$/, '')}(?:\u002F.*)?$`;
2129
- return `/${strictPattern}/${flags}`;
2240
+ return result.join('\n');
2241
+ }
2242
+ function safeReadFile(file) {
2243
+ try {
2244
+ return fs.readFileSync(file, 'utf8');
2245
+ }
2246
+ catch {
2247
+ return null;
2248
+ }
2249
+ }
2250
+ function normalizeFilter(filter) {
2251
+ const arr = Array.isArray(filter) ? filter : [filter];
2252
+ return arr.map((item) => {
2253
+ if (typeof item === 'string' && item.startsWith('/') && item.endsWith('/')) {
2254
+ const body = item.slice(1, -1);
2255
+ return new RegExp(body);
2256
+ }
2257
+ return item;
2258
+ });
2130
2259
  }
2131
2260
 
2132
2261
  function getImportedName(spec) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.369.0",
3
+ "version": "0.370.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,10 +1,9 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
- type Options = [
2
+ type RuleOptions = [
3
3
  {
4
4
  importFilter: string[] | string;
5
5
  strict?: boolean;
6
- mockPaths?: Record<string, string>;
7
6
  }
8
7
  ];
9
- declare const _default: ESLintUtils.RuleModule<"prefer-deep-imports", Options, unknown, ESLintUtils.RuleListener>;
8
+ declare const _default: ESLintUtils.RuleModule<"prefer-deep-imports", RuleOptions, unknown, ESLintUtils.RuleListener>;
10
9
  export default _default;