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