@taiga-ui/eslint-plugin-experience-next 0.372.0 → 0.374.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.esm.js +260 -242
- package/package.json +1 -1
- package/rules/prefer-deep-imports.d.ts +13 -0
package/index.esm.js
CHANGED
|
@@ -911,11 +911,7 @@ var recommended = defineConfig([
|
|
|
911
911
|
'error',
|
|
912
912
|
{
|
|
913
913
|
currentProject: String.raw `(?<=projects/)([-\w]+)`,
|
|
914
|
-
ignoreImports: [
|
|
915
|
-
String.raw `\?raw`,
|
|
916
|
-
'@taiga-ui/testing/cypress',
|
|
917
|
-
'@taiga-ui/testing/setup-jest',
|
|
918
|
-
],
|
|
914
|
+
ignoreImports: [String.raw `\?raw`, '@taiga-ui/testing/cypress'],
|
|
919
915
|
},
|
|
920
916
|
],
|
|
921
917
|
'@taiga-ui/experience-next/no-deep-imports-to-indexed-packages': 'error',
|
|
@@ -1135,7 +1131,7 @@ function projectJsonExist(filename) {
|
|
|
1135
1131
|
}
|
|
1136
1132
|
|
|
1137
1133
|
const allPackageJSONs = globSync('**/package.json', {
|
|
1138
|
-
ignore: ['node_modules/**', 'dist/**'],
|
|
1134
|
+
ignore: ['**/node_modules/**', '**/dist/**'],
|
|
1139
1135
|
}).filter((path) => !readJSON(path).private);
|
|
1140
1136
|
const packageNames = allPackageJSONs.map((path) => readJSON(path).name).filter(Boolean);
|
|
1141
1137
|
const packageSourceGlobs = allPackageJSONs.map((p) => p.replaceAll(/\\+/g, '/').replace('package.json', '**/*.ts'));
|
|
@@ -1932,61 +1928,63 @@ function getClass(node) {
|
|
|
1932
1928
|
const MESSAGE_ID$1 = 'prefer-deep-imports';
|
|
1933
1929
|
const ERROR_MESSAGE = 'Import via root entry point is prohibited when nested entry points exist';
|
|
1934
1930
|
const createRule$3 = ESLintUtils.RuleCreator(() => ERROR_MESSAGE);
|
|
1935
|
-
const tsconfigCache = new Map();
|
|
1936
|
-
const moduleResolutionCache = new Map();
|
|
1937
|
-
const exportCheckCache = new Map();
|
|
1938
|
-
const nearestNgCache = new Map();
|
|
1939
|
-
const tsFileCache = new Map();
|
|
1940
1931
|
var preferDeepImports = createRule$3({
|
|
1941
1932
|
create(context, [options]) {
|
|
1942
|
-
const allowedPackages =
|
|
1943
|
-
const
|
|
1933
|
+
const allowedPackages = normalizeImportFilter(options.importFilter);
|
|
1934
|
+
const isStrictMode = options.strict ?? false;
|
|
1935
|
+
const parserServices = ESLintUtils.getParserServices(context);
|
|
1936
|
+
const program = parserServices.program;
|
|
1937
|
+
const typeChecker = program.getTypeChecker();
|
|
1944
1938
|
return {
|
|
1945
1939
|
ImportDeclaration(node) {
|
|
1946
|
-
const
|
|
1947
|
-
if (typeof
|
|
1940
|
+
const rawImportPath = node.source.value;
|
|
1941
|
+
if (typeof rawImportPath !== 'string') {
|
|
1948
1942
|
return;
|
|
1949
1943
|
}
|
|
1950
|
-
const
|
|
1951
|
-
|
|
1952
|
-
if (shortName && pkg instanceof RegExp) {
|
|
1953
|
-
return pkg.test(shortName);
|
|
1954
|
-
}
|
|
1955
|
-
return pkg === shortName;
|
|
1956
|
-
});
|
|
1957
|
-
if (!allowed) {
|
|
1944
|
+
const rootPackageName = getRootPackageName(rawImportPath);
|
|
1945
|
+
if (!rootPackageName) {
|
|
1958
1946
|
return;
|
|
1959
1947
|
}
|
|
1960
|
-
if (!
|
|
1948
|
+
if (!allowedPackages.includes(rootPackageName)) {
|
|
1961
1949
|
return;
|
|
1962
1950
|
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1951
|
+
if (!isStrictMode &&
|
|
1952
|
+
isAlreadyNestedImport(rawImportPath, rootPackageName)) {
|
|
1965
1953
|
return;
|
|
1966
1954
|
}
|
|
1967
|
-
const
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1955
|
+
const importedSymbols = extractNamedImportedSymbols(node);
|
|
1956
|
+
if (importedSymbols.length === 0) {
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
const currentFileName = context.getFilename();
|
|
1960
|
+
const rootEntryDirectory = resolveRootEntryDirectory(rawImportPath, currentFileName, program);
|
|
1961
|
+
if (!rootEntryDirectory) {
|
|
1962
|
+
context.report({
|
|
1971
1963
|
messageId: MESSAGE_ID$1,
|
|
1972
1964
|
node,
|
|
1973
1965
|
});
|
|
1966
|
+
return;
|
|
1974
1967
|
}
|
|
1975
|
-
const
|
|
1976
|
-
if (
|
|
1977
|
-
|
|
1968
|
+
const nestedEntryPointRelativePaths = findNestedEntryPointRelativePaths(rootEntryDirectory);
|
|
1969
|
+
if (nestedEntryPointRelativePaths.length === 0) {
|
|
1970
|
+
context.report({
|
|
1978
1971
|
messageId: MESSAGE_ID$1,
|
|
1979
1972
|
node,
|
|
1980
1973
|
});
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
const candidateEntryPointPaths = selectCandidateEntryPointsForMode(nestedEntryPointRelativePaths, isStrictMode);
|
|
1977
|
+
if (candidateEntryPointPaths.length === 0) {
|
|
1978
|
+
return;
|
|
1981
1979
|
}
|
|
1982
|
-
const
|
|
1983
|
-
if (
|
|
1980
|
+
const symbolToEntryPoint = mapSymbolsToEntryPointsUsingTypeChecker(importedSymbols, candidateEntryPointPaths, rootEntryDirectory, program, typeChecker);
|
|
1981
|
+
if (symbolToEntryPoint.size === 0) {
|
|
1984
1982
|
return;
|
|
1985
1983
|
}
|
|
1986
|
-
const
|
|
1984
|
+
const newImportBlock = buildRewrittenImports(node, rawImportPath, symbolToEntryPoint);
|
|
1987
1985
|
context.report({
|
|
1988
1986
|
fix(fixer) {
|
|
1989
|
-
return fixer.replaceTextRange(node.range,
|
|
1987
|
+
return fixer.replaceTextRange(node.range, newImportBlock);
|
|
1990
1988
|
},
|
|
1991
1989
|
messageId: MESSAGE_ID$1,
|
|
1992
1990
|
node,
|
|
@@ -2018,244 +2016,264 @@ var preferDeepImports = createRule$3({
|
|
|
2018
2016
|
},
|
|
2019
2017
|
name: 'prefer-deep-imports',
|
|
2020
2018
|
});
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2019
|
+
/**
|
|
2020
|
+
* Normalize "importFilter" option to a flat array of package names.
|
|
2021
|
+
* The rule expects concrete package names, not regular expression strings.
|
|
2022
|
+
*/
|
|
2023
|
+
function normalizeImportFilter(importFilter) {
|
|
2024
|
+
return Array.isArray(importFilter) ? importFilter : [importFilter];
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Extract the package root name from an import specifier.
|
|
2028
|
+
*
|
|
2029
|
+
* Examples:
|
|
2030
|
+
* "@taiga-ui/core" → "@taiga-ui/core"
|
|
2031
|
+
* "@taiga-ui/core/components" → "@taiga-ui/core"
|
|
2032
|
+
* "some-lib" → "some-lib"
|
|
2033
|
+
* "some-lib/utils" → "some-lib"
|
|
2034
|
+
*/
|
|
2035
|
+
function getRootPackageName(importPath) {
|
|
2036
|
+
if (importPath.startsWith('@')) {
|
|
2037
|
+
const segments = importPath.split('/');
|
|
2038
|
+
if (segments.length < 2) {
|
|
2039
|
+
return null;
|
|
2040
|
+
}
|
|
2041
|
+
return `${segments[0]}/${segments[1]}`;
|
|
2024
2042
|
}
|
|
2025
|
-
const
|
|
2026
|
-
return
|
|
2043
|
+
const parts = importPath.split('/');
|
|
2044
|
+
return parts[0] ?? null;
|
|
2027
2045
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2046
|
+
/**
|
|
2047
|
+
* Check whether the current import path is already nested below the root package.
|
|
2048
|
+
*
|
|
2049
|
+
* Example:
|
|
2050
|
+
* root = "@taiga-ui/core"
|
|
2051
|
+
* "@taiga-ui/core" → false (root import)
|
|
2052
|
+
* "@taiga-ui/core/components" → true
|
|
2053
|
+
* "@taiga-ui/core/components/x" → true
|
|
2054
|
+
*/
|
|
2055
|
+
function isAlreadyNestedImport(importPath, rootPackageName) {
|
|
2056
|
+
if (!importPath.startsWith(rootPackageName)) {
|
|
2057
|
+
return false;
|
|
2058
|
+
}
|
|
2059
|
+
const importSegments = importPath.split('/');
|
|
2060
|
+
const rootSegments = rootPackageName.split('/');
|
|
2061
|
+
return importSegments.length > rootSegments.length;
|
|
2030
2062
|
}
|
|
2031
|
-
|
|
2063
|
+
/**
|
|
2064
|
+
* Extract only named imported symbols:
|
|
2065
|
+
*
|
|
2066
|
+
* Examples:
|
|
2067
|
+
* import {A, B as C} from 'x'; → ['A', 'B']
|
|
2068
|
+
*
|
|
2069
|
+
* Namespace imports and default imports are ignored for this rule.
|
|
2070
|
+
*/
|
|
2071
|
+
function extractNamedImportedSymbols(node) {
|
|
2032
2072
|
return node.specifiers
|
|
2033
|
-
.filter((
|
|
2034
|
-
.map((
|
|
2035
|
-
?
|
|
2036
|
-
:
|
|
2073
|
+
.filter((specifier) => specifier.type === AST_NODE_TYPES$1.ImportSpecifier)
|
|
2074
|
+
.map((specifier) => specifier.imported.type === AST_NODE_TYPES$1.Identifier
|
|
2075
|
+
? specifier.imported.name
|
|
2076
|
+
: specifier.imported.value);
|
|
2037
2077
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
const
|
|
2044
|
-
|
|
2045
|
-
|
|
2078
|
+
/**
|
|
2079
|
+
* Resolve the physical directory of the module being imported.
|
|
2080
|
+
* We rely on the same module resolution that TypeScript uses for the program.
|
|
2081
|
+
*/
|
|
2082
|
+
function resolveRootEntryDirectory(importPath, fromFile, program) {
|
|
2083
|
+
const compilerOptions = program.getCompilerOptions();
|
|
2084
|
+
const resolution = ts.resolveModuleName(importPath, fromFile, compilerOptions, ts.sys).resolvedModule;
|
|
2085
|
+
if (!resolution) {
|
|
2046
2086
|
return null;
|
|
2047
2087
|
}
|
|
2048
|
-
|
|
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);
|
|
2053
|
-
}
|
|
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;
|
|
2088
|
+
return path.dirname(resolution.resolvedFileName);
|
|
2058
2089
|
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2090
|
+
/**
|
|
2091
|
+
* Find all nested entry points relative to the given root directory.
|
|
2092
|
+
*
|
|
2093
|
+
* A directory is considered a nested entry point if it contains either:
|
|
2094
|
+
* - "ng-package.json" (Angular library entry)
|
|
2095
|
+
* - "collection.json" (Angular schematics collection)
|
|
2096
|
+
*
|
|
2097
|
+
* Returned paths are relative to "rootEntryDirectory".
|
|
2098
|
+
*
|
|
2099
|
+
* Example:
|
|
2100
|
+
* rootEntryDirectory = ".../core/src"
|
|
2101
|
+
* found:
|
|
2102
|
+
* "utils/ng-package.json" → "utils"
|
|
2103
|
+
* "utils/dom/ng-package.json" → "utils/dom"
|
|
2104
|
+
* "schematics/collection.json" → "schematics"
|
|
2105
|
+
*/
|
|
2106
|
+
function findNestedEntryPointRelativePaths(rootEntryDirectory) {
|
|
2107
|
+
const ngPackageJsonFiles = globSync('**/ng-package.json', {
|
|
2108
|
+
absolute: false,
|
|
2109
|
+
cwd: rootEntryDirectory,
|
|
2110
|
+
});
|
|
2111
|
+
const collectionJsonFiles = globSync('**/collection.json', {
|
|
2112
|
+
absolute: false,
|
|
2113
|
+
cwd: rootEntryDirectory,
|
|
2114
|
+
});
|
|
2115
|
+
const directories = [];
|
|
2116
|
+
for (const file of ngPackageJsonFiles) {
|
|
2117
|
+
const normalized = file.replaceAll('\\', '/').replace(/\/ng-package\.json$/, '');
|
|
2118
|
+
if (normalized && normalized !== '.') {
|
|
2119
|
+
directories.push(normalized);
|
|
2066
2120
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2121
|
+
}
|
|
2122
|
+
for (const file of collectionJsonFiles) {
|
|
2123
|
+
const normalized = file.replaceAll('\\', '/').replace(/\/collection\.json$/, '');
|
|
2124
|
+
if (normalized && normalized !== '.') {
|
|
2125
|
+
directories.push(normalized);
|
|
2070
2126
|
}
|
|
2071
|
-
dir = parent;
|
|
2072
2127
|
}
|
|
2073
|
-
return
|
|
2128
|
+
return directories;
|
|
2074
2129
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2130
|
+
/**
|
|
2131
|
+
* For strict = false:
|
|
2132
|
+
* Only first-level nested directories are candidates.
|
|
2133
|
+
*
|
|
2134
|
+
* For strict = true:
|
|
2135
|
+
* All nested directories are candidates, sorted from deepest to shallowest
|
|
2136
|
+
* so that the deepest match wins.
|
|
2137
|
+
*/
|
|
2138
|
+
function selectCandidateEntryPointsForMode(allNestedRelativePaths, strict) {
|
|
2139
|
+
if (!strict) {
|
|
2140
|
+
return allNestedRelativePaths.filter((relativePath) => !relativePath.includes('/'));
|
|
2079
2141
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
return found;
|
|
2142
|
+
return [...allNestedRelativePaths].sort((a, b) => {
|
|
2143
|
+
const depthA = a.split('/').filter(Boolean).length;
|
|
2144
|
+
const depthB = b.split('/').filter(Boolean).length;
|
|
2145
|
+
return depthB - depthA;
|
|
2146
|
+
});
|
|
2086
2147
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2148
|
+
/**
|
|
2149
|
+
* Build a map from exported symbol name to nested entry point relative path.
|
|
2150
|
+
*
|
|
2151
|
+
* Implementation strategy:
|
|
2152
|
+
* 1. For each candidate nested entry point:
|
|
2153
|
+
* - Determine its entry file (using ng-package.json or collection.json).
|
|
2154
|
+
* - Ask TypeScript for the module symbol and its exports.
|
|
2155
|
+
* 2. For each imported symbol:
|
|
2156
|
+
* - Find the first entry point whose export table contains that symbol.
|
|
2157
|
+
* - strict = true: candidates were sorted deepest-first.
|
|
2158
|
+
* - strict = false: candidates contain only first-level nested entry points.
|
|
2159
|
+
*/
|
|
2160
|
+
function mapSymbolsToEntryPointsUsingTypeChecker(importedSymbols, candidateEntryPoints, rootEntryDirectory, program, typeChecker) {
|
|
2161
|
+
const symbolToEntryPoint = new Map();
|
|
2162
|
+
const exportTableByEntryPoint = new Map();
|
|
2163
|
+
for (const relativeEntryDir of candidateEntryPoints) {
|
|
2164
|
+
const entryFile = getEntryFileForNestedEntryPoint(rootEntryDirectory, relativeEntryDir);
|
|
2165
|
+
if (!entryFile) {
|
|
2166
|
+
continue;
|
|
2093
2167
|
}
|
|
2094
|
-
const
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
});
|
|
2102
|
-
tsFileCache.set(full, files);
|
|
2168
|
+
const sourceFile = program.getSourceFile(entryFile);
|
|
2169
|
+
if (!sourceFile) {
|
|
2170
|
+
continue;
|
|
2171
|
+
}
|
|
2172
|
+
const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
|
|
2173
|
+
if (!moduleSymbol) {
|
|
2174
|
+
continue;
|
|
2103
2175
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2176
|
+
const exports$1 = typeChecker.getExportsOfModule(moduleSymbol);
|
|
2177
|
+
const exportedNames = new Set(exports$1.map((symbol) => symbol.getName()));
|
|
2178
|
+
exportTableByEntryPoint.set(relativeEntryDir, exportedNames);
|
|
2179
|
+
}
|
|
2180
|
+
for (const importedSymbol of importedSymbols) {
|
|
2181
|
+
for (const relativeEntryDir of candidateEntryPoints) {
|
|
2182
|
+
const exportedNames = exportTableByEntryPoint.get(relativeEntryDir);
|
|
2183
|
+
if (!exportedNames) {
|
|
2107
2184
|
continue;
|
|
2108
2185
|
}
|
|
2109
|
-
|
|
2110
|
-
|
|
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);
|
|
2186
|
+
if (!exportedNames.has(importedSymbol)) {
|
|
2187
|
+
continue;
|
|
2125
2188
|
}
|
|
2189
|
+
symbolToEntryPoint.set(importedSymbol, relativeEntryDir);
|
|
2190
|
+
break;
|
|
2126
2191
|
}
|
|
2127
2192
|
}
|
|
2128
|
-
return
|
|
2193
|
+
return symbolToEntryPoint;
|
|
2129
2194
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
if (
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
return cache.get(key);
|
|
2153
|
-
}
|
|
2154
|
-
const res = containsReExport(file, content, symbol);
|
|
2155
|
-
cache.set(key, res);
|
|
2156
|
-
return res;
|
|
2157
|
-
}
|
|
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)) {
|
|
2173
|
-
return true;
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
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;
|
|
2195
|
+
/**
|
|
2196
|
+
* Determine the physical entry file for a nested entry point.
|
|
2197
|
+
*
|
|
2198
|
+
* Priority:
|
|
2199
|
+
* 1. If "ng-package.json" exists:
|
|
2200
|
+
* - use lib.entryFile if present
|
|
2201
|
+
* - otherwise fall back to "index.ts"
|
|
2202
|
+
* 2. Else if "collection.json" exists:
|
|
2203
|
+
* - treat this directory as a schematic collection package
|
|
2204
|
+
* - entry file is "index.ts" if it exists
|
|
2205
|
+
* 3. Otherwise: no entry file can be determined → return null
|
|
2206
|
+
*/
|
|
2207
|
+
function getEntryFileForNestedEntryPoint(rootEntryDirectory, relativeEntryDirectory) {
|
|
2208
|
+
const absoluteDirectory = path.join(rootEntryDirectory, relativeEntryDirectory);
|
|
2209
|
+
const ngPackageJsonPath = path.join(absoluteDirectory, 'ng-package.json');
|
|
2210
|
+
const collectionJsonPath = path.join(absoluteDirectory, 'collection.json');
|
|
2211
|
+
if (fs.existsSync(ngPackageJsonPath)) {
|
|
2212
|
+
try {
|
|
2213
|
+
const raw = fs.readFileSync(ngPackageJsonPath, 'utf8');
|
|
2214
|
+
const json = JSON.parse(raw);
|
|
2215
|
+
const entryRelative = json.lib?.entryFile ?? 'index.ts';
|
|
2216
|
+
return path.resolve(absoluteDirectory, entryRelative);
|
|
2182
2217
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
return
|
|
2218
|
+
catch {
|
|
2219
|
+
const fallback = path.resolve(absoluteDirectory, 'index.ts');
|
|
2220
|
+
return fs.existsSync(fallback) ? fallback : null;
|
|
2186
2221
|
}
|
|
2187
2222
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
const full = path.resolve(path.dirname(file), rel);
|
|
2192
|
-
return fs.existsSync(full) ? full : null;
|
|
2193
|
-
}
|
|
2194
|
-
function cachedNearestNg(file, root) {
|
|
2195
|
-
const key = `${file}|${root}`;
|
|
2196
|
-
if (nearestNgCache.has(key)) {
|
|
2197
|
-
return nearestNgCache.get(key);
|
|
2223
|
+
if (fs.existsSync(collectionJsonPath)) {
|
|
2224
|
+
const entryFile = path.resolve(absoluteDirectory, 'index.ts');
|
|
2225
|
+
return fs.existsSync(entryFile) ? entryFile : null;
|
|
2198
2226
|
}
|
|
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
2227
|
return null;
|
|
2214
2228
|
}
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2229
|
+
/**
|
|
2230
|
+
* Build the final text block with rewritten import declarations.
|
|
2231
|
+
*
|
|
2232
|
+
* Example:
|
|
2233
|
+
* original:
|
|
2234
|
+
* import {A, B as C, D} from '@taiga-ui/core';
|
|
2235
|
+
*
|
|
2236
|
+
* symbolMap (strict = true):
|
|
2237
|
+
* A -> "components/button"
|
|
2238
|
+
* B -> "components/button"
|
|
2239
|
+
* D -> "components/other"
|
|
2240
|
+
*
|
|
2241
|
+
* result:
|
|
2242
|
+
* import {A, B as C} from '@taiga-ui/core/components/button';
|
|
2243
|
+
* import {D} from '@taiga-ui/core/components/other';
|
|
2244
|
+
*/
|
|
2245
|
+
function buildRewrittenImports(node, baseImportPath, symbolToEntryPoint) {
|
|
2246
|
+
const isTypeOnlyImport = node.importKind === 'type';
|
|
2247
|
+
const groupedByTarget = new Map();
|
|
2248
|
+
for (const [symbolName, relativeEntryPath] of symbolToEntryPoint.entries()) {
|
|
2249
|
+
const targetSpecifier = `${baseImportPath}/${relativeEntryPath}`;
|
|
2250
|
+
if (!groupedByTarget.has(targetSpecifier)) {
|
|
2251
|
+
groupedByTarget.set(targetSpecifier, []);
|
|
2222
2252
|
}
|
|
2223
|
-
|
|
2253
|
+
groupedByTarget.get(targetSpecifier).push(symbolName);
|
|
2224
2254
|
}
|
|
2225
|
-
const
|
|
2226
|
-
for (const [
|
|
2227
|
-
const
|
|
2228
|
-
const
|
|
2229
|
-
(
|
|
2230
|
-
?
|
|
2231
|
-
:
|
|
2232
|
-
if (!
|
|
2233
|
-
return
|
|
2255
|
+
const importStatements = [];
|
|
2256
|
+
for (const [targetSpecifier, symbols] of groupedByTarget.entries()) {
|
|
2257
|
+
const parts = symbols.map((symbolName) => {
|
|
2258
|
+
const matchingSpecifier = node.specifiers.find((specifier) => specifier.type === AST_NODE_TYPES$1.ImportSpecifier &&
|
|
2259
|
+
(specifier.imported.type === AST_NODE_TYPES$1.Identifier
|
|
2260
|
+
? specifier.imported.name === symbolName
|
|
2261
|
+
: specifier.imported.value === symbolName));
|
|
2262
|
+
if (!matchingSpecifier) {
|
|
2263
|
+
return symbolName;
|
|
2234
2264
|
}
|
|
2235
|
-
const
|
|
2236
|
-
|
|
2265
|
+
const importedName = matchingSpecifier.imported.type === AST_NODE_TYPES$1.Identifier
|
|
2266
|
+
? matchingSpecifier.imported.name
|
|
2267
|
+
: matchingSpecifier.imported.value;
|
|
2268
|
+
const localName = matchingSpecifier.local.name;
|
|
2269
|
+
return importedName === localName
|
|
2270
|
+
? importedName
|
|
2271
|
+
: `${importedName} as ${localName}`;
|
|
2237
2272
|
});
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
return result.join('\n');
|
|
2241
|
-
}
|
|
2242
|
-
function safeReadFile(file) {
|
|
2243
|
-
try {
|
|
2244
|
-
return fs.readFileSync(file, 'utf8');
|
|
2273
|
+
const statement = `import ${isTypeOnlyImport ? 'type ' : ''}{${parts.join(', ')}} from '${targetSpecifier}';`;
|
|
2274
|
+
importStatements.push(statement);
|
|
2245
2275
|
}
|
|
2246
|
-
|
|
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
|
-
});
|
|
2276
|
+
return importStatements.join('\n');
|
|
2259
2277
|
}
|
|
2260
2278
|
|
|
2261
2279
|
function getImportedName(spec) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
2
|
type RuleOptions = [
|
|
3
3
|
{
|
|
4
|
+
/**
|
|
5
|
+
* List of package names for which this rule should be applied.
|
|
6
|
+
* Usually something like: ['@taiga-ui/core', '@taiga-ui/cdk', ...]
|
|
7
|
+
*/
|
|
4
8
|
importFilter: string[] | string;
|
|
9
|
+
/**
|
|
10
|
+
* strict = false:
|
|
11
|
+
* - Only rewrite imports from the root package path
|
|
12
|
+
* - Target only the first-level nested entry point (e.g. "@pkg/components")
|
|
13
|
+
*
|
|
14
|
+
* strict = true:
|
|
15
|
+
* - May rewrite imports from nested paths as well
|
|
16
|
+
* - Target the deepest nested entry point that actually exports the symbol
|
|
17
|
+
*/
|
|
5
18
|
strict?: boolean;
|
|
6
19
|
}
|
|
7
20
|
];
|