eslint-import-resolver-typescript
Advanced tools
Comparing version
@@ -1,27 +0,13 @@ | ||
import { type NapiResolveOptions, ResolverFactory } from 'rspack-resolver'; | ||
export declare const defaultConditionNames: string[]; | ||
export declare const defaultExtensions: string[]; | ||
export declare const defaultExtensionAlias: { | ||
'.js': string[]; | ||
'.jsx': string[]; | ||
'.cjs': string[]; | ||
'.mjs': string[]; | ||
}; | ||
export declare const defaultMainFields: string[]; | ||
export declare const interfaceVersion = 2; | ||
export interface TsResolverOptions extends NapiResolveOptions { | ||
alwaysTryTypes?: boolean; | ||
project?: string[] | string; | ||
} | ||
export declare function resolve(source: string, file: string, options?: TsResolverOptions | null, resolver?: ResolverFactory | null): { | ||
found: boolean; | ||
path?: string | null; | ||
}; | ||
export declare function createTypeScriptImportResolver(options?: TsResolverOptions | null): { | ||
import type { ResolvedResult } from 'eslint-plugin-import-x/types.js'; | ||
import { ResolverFactory } from 'rspack-resolver'; | ||
import type { TypeScriptResolverOptions } from './types.js'; | ||
export * from './constants.js'; | ||
export * from './helpers.js'; | ||
export * from './normalize-options.js'; | ||
export type * from './types.js'; | ||
export declare const resolve: (source: string, file: string, options?: TypeScriptResolverOptions | null, resolver?: ResolverFactory | null) => ResolvedResult; | ||
export declare const createTypeScriptImportResolver: (options?: TypeScriptResolverOptions | null) => { | ||
interfaceVersion: number; | ||
name: string; | ||
resolve(source: string, file: string): { | ||
found: boolean; | ||
path?: string | null; | ||
}; | ||
resolve(source: string, file: string): ResolvedResult; | ||
}; |
426
lib/index.js
@@ -1,90 +0,36 @@ | ||
import fs from 'node:fs'; | ||
import module from 'node:module'; | ||
import path from 'node:path'; | ||
import isNodeCoreModule from '@nolyfill/is-core-module'; | ||
import debug from 'debug'; | ||
import { createPathsMatcher, getTsconfig } from 'get-tsconfig'; | ||
import { createFilesMatcher, parseTsconfig, } from 'get-tsconfig'; | ||
import { isBunModule } from 'is-bun-module'; | ||
import { ResolverFactory } from 'rspack-resolver'; | ||
import { stableHash } from 'stable-hash'; | ||
import { globSync, isDynamicPattern } from 'tinyglobby'; | ||
const IMPORTER_NAME = 'eslint-import-resolver-typescript'; | ||
const log = debug(IMPORTER_NAME); | ||
export const defaultConditionNames = [ | ||
'types', | ||
'import', | ||
'esm2020', | ||
'es2020', | ||
'es2015', | ||
'require', | ||
'node', | ||
'node-addons', | ||
'browser', | ||
'default', | ||
]; | ||
export const defaultExtensions = [ | ||
'.ts', | ||
'.tsx', | ||
'.d.ts', | ||
'.js', | ||
'.jsx', | ||
'.json', | ||
'.node', | ||
]; | ||
export const defaultExtensionAlias = { | ||
'.js': [ | ||
'.ts', | ||
'.tsx', | ||
'.d.ts', | ||
'.js', | ||
], | ||
'.jsx': ['.tsx', '.d.ts', '.jsx'], | ||
'.cjs': ['.cts', '.d.cts', '.cjs'], | ||
'.mjs': ['.mts', '.d.mts', '.mjs'], | ||
}; | ||
export const defaultMainFields = [ | ||
'types', | ||
'typings', | ||
'fesm2020', | ||
'fesm2015', | ||
'esm2020', | ||
'es2020', | ||
'module', | ||
'jsnext:main', | ||
'main', | ||
]; | ||
export const interfaceVersion = 2; | ||
const JS_EXT_PATTERN = /\.(?:[cm]js|jsx?)$/; | ||
const RELATIVE_PATH_PATTERN = /^\.{1,2}(?:\/.*)?$/; | ||
let previousOptionsHash; | ||
let optionsHash; | ||
let cachedOptions; | ||
let cachedCwd; | ||
let mappersCachedOptions; | ||
let mappers = []; | ||
let resolverCachedOptions; | ||
let cachedResolver; | ||
export function resolve(source, file, options, resolver) { | ||
if (!cachedOptions || | ||
previousOptionsHash !== (optionsHash = stableHash(options))) { | ||
previousOptionsHash = optionsHash; | ||
cachedOptions = { | ||
...options, | ||
conditionNames: options?.conditionNames ?? defaultConditionNames, | ||
extensions: options?.extensions ?? defaultExtensions, | ||
extensionAlias: options?.extensionAlias ?? defaultExtensionAlias, | ||
mainFields: options?.mainFields ?? defaultMainFields, | ||
import { IMPORT_RESOLVER_NAME, JS_EXT_PATTERN } from './constants.js'; | ||
import { mangleScopedPackage, removeQuerystring, sortProjectsByAffinity, } from './helpers.js'; | ||
import { log } from './logger.js'; | ||
import { normalizeOptions } from './normalize-options.js'; | ||
export * from './constants.js'; | ||
export * from './helpers.js'; | ||
export * from './normalize-options.js'; | ||
const resolverCache = new Map(); | ||
const tsconfigCache = new Map(); | ||
const matcherCache = new Map(); | ||
const oxcResolve = (source, file, resolver) => { | ||
const result = resolver.sync(path.dirname(file), source); | ||
if (result.path) { | ||
return { | ||
found: true, | ||
path: result.path, | ||
}; | ||
} | ||
if (!resolver) { | ||
if (!cachedResolver || resolverCachedOptions !== cachedOptions) { | ||
cachedResolver = new ResolverFactory(cachedOptions); | ||
resolverCachedOptions = cachedOptions; | ||
} | ||
resolver = cachedResolver; | ||
if (result.error) { | ||
log('oxc resolve error:', result.error); | ||
} | ||
log('looking for', source, 'in', file); | ||
source = removeQuerystring(source); | ||
if (isNodeCoreModule(source) || | ||
isBunModule(source, (process.versions.bun ?? 'latest'))) { | ||
return { | ||
found: false, | ||
}; | ||
}; | ||
export const resolve = (source, file, options, resolver) => { | ||
if (module.isBuiltin(source) || | ||
(process.versions.bun && | ||
isBunModule(source, process.versions.bun))) { | ||
log('matched core:', source); | ||
@@ -104,29 +50,74 @@ return { | ||
} | ||
initMappers(cachedOptions); | ||
let mappedPaths = getMappedPaths(source, file, cachedOptions.extensions, true); | ||
if (mappedPaths.length > 0) { | ||
log('matched ts path:', ...mappedPaths); | ||
source = removeQuerystring(source); | ||
options ||= {}; | ||
if (!resolver) { | ||
const optionsHash = stableHash(options); | ||
const cwd = process.cwd(); | ||
options = normalizeOptions(options, cwd); | ||
const cacheKey = `${optionsHash}:${cwd}`; | ||
let cached = resolverCache.get(cacheKey); | ||
if (!cached && !options.project) { | ||
resolverCache.set(cacheKey, (cached = new ResolverFactory(options))); | ||
} | ||
resolver = cached; | ||
} | ||
else { | ||
mappedPaths = [source]; | ||
} | ||
let foundNodePath; | ||
for (const mappedPath of mappedPaths) { | ||
try { | ||
const resolved = resolver.sync(path.dirname(path.resolve(file)), mappedPath); | ||
if (resolved.path) { | ||
foundNodePath = resolved.path; | ||
break; | ||
options ||= {}; | ||
createResolver: if (!resolver) { | ||
const project = options.project; | ||
for (const tsconfigPath of project) { | ||
const resolverCached = resolverCache.get(tsconfigPath); | ||
if (resolverCached) { | ||
resolver = resolverCached; | ||
break createResolver; | ||
} | ||
let tsconfigCached = tsconfigCache.get(tsconfigPath); | ||
if (!tsconfigCached) { | ||
tsconfigCache.set(tsconfigPath, (tsconfigCached = parseTsconfig(tsconfigPath))); | ||
} | ||
let matcherCached = matcherCache.get(tsconfigPath); | ||
if (!matcherCached) { | ||
matcherCache.set(tsconfigPath, (matcherCached = createFilesMatcher({ | ||
config: tsconfigCached, | ||
path: tsconfigPath, | ||
}))); | ||
} | ||
const tsconfig = matcherCached(file); | ||
if (!tsconfig) { | ||
log('tsconfig', tsconfigPath, 'does not match', file); | ||
continue; | ||
} | ||
log('matched tsconfig at:', tsconfigPath, 'for', file); | ||
options = { | ||
...options, | ||
tsconfig: { | ||
references: 'auto', | ||
...options.tsconfig, | ||
configFile: tsconfigPath, | ||
}, | ||
}; | ||
resolver = new ResolverFactory(options); | ||
resolverCache.set(tsconfigPath, resolver); | ||
break createResolver; | ||
} | ||
catch { | ||
log('failed to resolve with', mappedPath); | ||
log('no tsconfig matched', file, 'with', ...project, ', trying from the the nearest one'); | ||
for (const p of sortProjectsByAffinity(project, file)) { | ||
const resolved = resolve(source, file, { ...options, project: p }, resolver); | ||
if (resolved.found) { | ||
return resolved; | ||
} | ||
} | ||
} | ||
if ((JS_EXT_PATTERN.test(foundNodePath) || | ||
(cachedOptions.alwaysTryTypes && !foundNodePath)) && | ||
if (!resolver) { | ||
return { | ||
found: false, | ||
}; | ||
} | ||
const resolved = oxcResolve(source, file, resolver); | ||
const foundPath = resolved.path; | ||
if (((foundPath && JS_EXT_PATTERN.test(foundPath)) || | ||
(options.alwaysTryTypes !== false && !foundPath)) && | ||
!/^@types[/\\]/.test(source) && | ||
!path.isAbsolute(source) && | ||
!source.startsWith('.')) { | ||
const definitelyTyped = resolve('@types' + path.sep + mangleScopedPackage(source), file, options); | ||
const definitelyTyped = oxcResolve('@types/' + mangleScopedPackage(source), file, resolver); | ||
if (definitelyTyped.found) { | ||
@@ -136,25 +127,16 @@ return definitelyTyped; | ||
} | ||
if (foundNodePath) { | ||
log('matched node path:', foundNodePath); | ||
return { | ||
found: true, | ||
path: foundNodePath, | ||
}; | ||
if (foundPath) { | ||
log('matched path:', foundPath); | ||
} | ||
log("didn't find ", source); | ||
else { | ||
log("didn't find", source, 'with', options.tsconfig?.configFile || options.project); | ||
} | ||
return resolved; | ||
}; | ||
export const createTypeScriptImportResolver = (options) => { | ||
options = normalizeOptions(options); | ||
const resolver = options.project ? null : new ResolverFactory(options); | ||
return { | ||
found: false, | ||
}; | ||
} | ||
export function createTypeScriptImportResolver(options) { | ||
const resolver = new ResolverFactory({ | ||
...options, | ||
conditionNames: options?.conditionNames ?? defaultConditionNames, | ||
extensions: options?.extensions ?? defaultExtensions, | ||
extensionAlias: options?.extensionAlias ?? defaultExtensionAlias, | ||
mainFields: options?.mainFields ?? defaultMainFields, | ||
}); | ||
return { | ||
interfaceVersion: 3, | ||
name: IMPORTER_NAME, | ||
name: IMPORT_RESOLVER_NAME, | ||
resolve(source, file) { | ||
@@ -164,207 +146,3 @@ return resolve(source, file, options, resolver); | ||
}; | ||
} | ||
function removeQuerystring(id) { | ||
const querystringIndex = id.lastIndexOf('?'); | ||
if (querystringIndex !== -1) { | ||
return id.slice(0, querystringIndex); | ||
} | ||
return id; | ||
} | ||
const isFile = (path) => { | ||
try { | ||
return !!(path && fs.statSync(path, { throwIfNoEntry: false })?.isFile()); | ||
} | ||
catch { | ||
return false; | ||
} | ||
}; | ||
const isModule = (modulePath) => !!modulePath && isFile(path.resolve(modulePath, 'package.json')); | ||
function getMappedPaths(source, file, extensions = defaultExtensions, retry) { | ||
const originalExtensions = extensions; | ||
extensions = ['', ...extensions]; | ||
let paths = []; | ||
if (RELATIVE_PATH_PATTERN.test(source)) { | ||
const resolved = path.resolve(path.dirname(file), source); | ||
if (isFile(resolved)) { | ||
paths = [resolved]; | ||
} | ||
} | ||
else { | ||
let mapperFns = mappers | ||
.filter(({ files }) => files.has(file)) | ||
.map(({ mapperFn }) => mapperFn); | ||
if (mapperFns.length === 0) { | ||
mapperFns = mappers | ||
.map(mapper => ({ | ||
mapperFn: mapper.mapperFn, | ||
counter: equalChars(path.dirname(file), path.dirname(mapper.path)), | ||
})) | ||
.sort((a, b) => b.counter - a.counter) | ||
.map(({ mapperFn }) => mapperFn); | ||
} | ||
paths = mapperFns | ||
.map(mapperFn => mapperFn(source).map(item => [ | ||
...extensions.map(ext => `${item}${ext}`), | ||
...originalExtensions.map(ext => `${item}/index${ext}`), | ||
])) | ||
.flat(2) | ||
.map(toNativePathSeparator) | ||
.filter(mappedPath => { | ||
try { | ||
const stat = fs.statSync(mappedPath, { throwIfNoEntry: false }); | ||
if (stat === undefined) | ||
return false; | ||
if (stat.isFile()) | ||
return true; | ||
if (stat.isDirectory()) { | ||
return isModule(mappedPath); | ||
} | ||
} | ||
catch { | ||
return false; | ||
} | ||
return false; | ||
}); | ||
} | ||
if (retry && paths.length === 0) { | ||
const isJs = JS_EXT_PATTERN.test(source); | ||
if (isJs) { | ||
const jsExt = path.extname(source); | ||
const tsExt = jsExt.replace('js', 'ts'); | ||
const basename = source.replace(JS_EXT_PATTERN, ''); | ||
let resolved = getMappedPaths(basename + tsExt, file); | ||
if (resolved.length === 0 && jsExt === '.js') { | ||
const tsxExt = jsExt.replace('js', 'tsx'); | ||
resolved = getMappedPaths(basename + tsxExt, file); | ||
} | ||
if (resolved.length === 0) { | ||
resolved = getMappedPaths(basename + '.d' + (tsExt === '.tsx' ? '.ts' : tsExt), file); | ||
} | ||
if (resolved.length > 0) { | ||
return resolved; | ||
} | ||
} | ||
for (const ext of extensions) { | ||
const mappedPaths = isJs ? [] : getMappedPaths(source + ext, file); | ||
const resolved = mappedPaths.length > 0 | ||
? mappedPaths | ||
: getMappedPaths(source + `/index${ext}`, file); | ||
if (resolved.length > 0) { | ||
return resolved; | ||
} | ||
} | ||
} | ||
return paths; | ||
} | ||
function initMappers(options) { | ||
if (mappers.length > 0 && | ||
mappersCachedOptions === options && | ||
cachedCwd === process.cwd()) { | ||
return; | ||
} | ||
cachedCwd = process.cwd(); | ||
const configPaths = (typeof options.project === 'string' | ||
? [options.project] | ||
: | ||
Array.isArray(options.project) | ||
? options.project | ||
: [cachedCwd]) | ||
.map(config => replacePathSeparator(config, path.sep, path.posix.sep)); | ||
const defaultInclude = ['**/*']; | ||
const defaultIgnore = ['**/node_modules/**']; | ||
const projectPaths = [ | ||
...new Set([ | ||
...configPaths | ||
.filter(p => !isDynamicPattern(p)) | ||
.map(p => path.resolve(process.cwd(), p)), | ||
...globSync(configPaths.filter(path => isDynamicPattern(path)), { | ||
absolute: true, | ||
dot: true, | ||
expandDirectories: false, | ||
ignore: defaultIgnore, | ||
}), | ||
]), | ||
]; | ||
mappers = projectPaths | ||
.map(projectPath => { | ||
let tsconfigResult; | ||
if (isFile(projectPath)) { | ||
const { dir, base } = path.parse(projectPath); | ||
tsconfigResult = getTsconfig(dir, base); | ||
} | ||
else { | ||
tsconfigResult = getTsconfig(projectPath); | ||
} | ||
if (!tsconfigResult) { | ||
return; | ||
} | ||
const mapperFn = createPathsMatcher(tsconfigResult); | ||
if (!mapperFn) { | ||
return; | ||
} | ||
const files = tsconfigResult.config.files == null && | ||
tsconfigResult.config.include == null | ||
? | ||
globSync(defaultInclude, { | ||
absolute: true, | ||
cwd: path.dirname(tsconfigResult.path), | ||
dot: true, | ||
ignore: [ | ||
...(tsconfigResult.config.exclude ?? []), | ||
...defaultIgnore, | ||
], | ||
}) | ||
: [ | ||
...(tsconfigResult.config.files != null && | ||
tsconfigResult.config.files.length > 0 | ||
? tsconfigResult.config.files.map(file => path.normalize(path.resolve(path.dirname(tsconfigResult.path), file))) | ||
: []), | ||
...(tsconfigResult.config.include != null && | ||
tsconfigResult.config.include.length > 0 | ||
? globSync(tsconfigResult.config.include, { | ||
absolute: true, | ||
cwd: path.dirname(tsconfigResult.path), | ||
dot: true, | ||
ignore: [ | ||
...(tsconfigResult.config.exclude ?? []), | ||
...defaultIgnore, | ||
], | ||
}) | ||
: []), | ||
]; | ||
return { | ||
path: toNativePathSeparator(tsconfigResult.path), | ||
files: new Set(files.map(toNativePathSeparator)), | ||
mapperFn, | ||
}; | ||
}) | ||
.filter(Boolean); | ||
mappersCachedOptions = options; | ||
} | ||
function mangleScopedPackage(moduleName) { | ||
if (moduleName.startsWith('@')) { | ||
const replaceSlash = moduleName.replace(path.sep, '__'); | ||
if (replaceSlash !== moduleName) { | ||
return replaceSlash.slice(1); | ||
} | ||
} | ||
return moduleName; | ||
} | ||
function replacePathSeparator(p, from, to) { | ||
return from === to ? p : p.replaceAll(from, to); | ||
} | ||
function toNativePathSeparator(p) { | ||
return replacePathSeparator(p, path[process.platform === 'win32' ? 'posix' : 'win32'].sep, path[process.platform === 'win32' ? 'win32' : 'posix'].sep); | ||
} | ||
function equalChars(a, b) { | ||
if (a.length === 0 || b.length === 0) { | ||
return 0; | ||
} | ||
let i = 0; | ||
const length = Math.min(a.length, b.length); | ||
while (i < length && a.charAt(i) === b.charAt(i)) { | ||
i += 1; | ||
} | ||
return i; | ||
} | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "eslint-import-resolver-typescript", | ||
"version": "3.9.1", | ||
"version": "4.0.0", | ||
"type": "module", | ||
@@ -14,3 +14,3 @@ "description": "This plugin adds `TypeScript` support to `eslint-plugin-import`", | ||
"engines": { | ||
"node": "^14.18.0 || >=16.0.0" | ||
"node": "^16.17.0 || >=18.6.0" | ||
}, | ||
@@ -22,15 +22,10 @@ "main": "lib/index.cjs", | ||
"types": "./lib/index.d.ts", | ||
"es2020": "./lib/index.es2020.mjs", | ||
"fesm2020": "./lib/index.es2020.mjs", | ||
"import": "./lib/index.js", | ||
"require": "./lib/index.cjs" | ||
"require": "./lib/index.cjs", | ||
"default": "./lib/index.js" | ||
}, | ||
"./package.json": "./package.json" | ||
}, | ||
"es2020": "lib/index.es2020.mjs", | ||
"fesm2020": "lib/index.es2020.mjs", | ||
"types": "lib/index.d.ts", | ||
"files": [ | ||
"lib", | ||
"shim.d.ts", | ||
"!**/*.tsbuildinfo" | ||
@@ -59,7 +54,6 @@ ], | ||
"dependencies": { | ||
"@nolyfill/is-core-module": "1.0.39", | ||
"debug": "^4.4.0", | ||
"get-tsconfig": "^4.10.0", | ||
"is-bun-module": "^1.3.0", | ||
"rspack-resolver": "^1.1.0", | ||
"rspack-resolver": "^1.1.2", | ||
"stable-hash": "^0.0.5", | ||
@@ -66,0 +60,0 @@ "tinyglobby": "^0.2.12" |
@@ -28,4 +28,8 @@ # eslint-import-resolver-typescript | ||
- [Installation](#installation) | ||
- [`eslint-plugin-import`](#eslint-plugin-import) | ||
- [`eslint-plugin-import-x`](#eslint-plugin-import-x) | ||
- [Configuration](#configuration) | ||
- [Options from `enhanced-resolve`](#options-from-enhanced-resolve) | ||
- [`eslint.config.js`](#eslintconfigjs) | ||
- [`.eslintrc`](#eslintrc) | ||
- [Options from `rspack-resolver`](#options-from-rspack-resolver) | ||
- [`conditionNames`](#conditionnames) | ||
@@ -51,2 +55,4 @@ - [`extensions`](#extensions) | ||
### `eslint-plugin-import` | ||
```sh | ||
@@ -63,4 +69,15 @@ # npm | ||
**Important when using `eslint-plugin-import-x` and `npm`**: Use `npm i -D eslint-plugin-import@eslint-plugin-import-x@latest eslint-import-resolver-typescript`, or you will end up with both `eslint-plugin-import` and `eslint-plugin-import-x` in your node_modules. | ||
### `eslint-plugin-import-x` | ||
```sh | ||
# npm | ||
npm i -D eslint-plugin-import-x eslint-import-resolver-typescript | ||
# pnpm | ||
pnpm i -D eslint-plugin-import-x eslint-import-resolver-typescript | ||
# yarn | ||
yarn add -D eslint-plugin-import-x eslint-import-resolver-typescript | ||
``` | ||
## Configuration | ||
@@ -73,8 +90,6 @@ | ||
```js | ||
// eslint.config.js | ||
const { | ||
createTypeScriptImportResolver, | ||
} = require('eslint-import-resolver-typescript') | ||
// eslint.config.js, CommonJS is also supported | ||
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript' | ||
module.exports = [ | ||
export default [ | ||
{ | ||
@@ -107,2 +122,5 @@ settings: { | ||
], | ||
// use <root>/jsconfig.json | ||
project: 'jsconfig.json', | ||
}), | ||
@@ -118,4 +136,4 @@ ], | ||
```js | ||
// eslint.config.js | ||
module.exports = [ | ||
// eslint.config.js, CommonJS is also supported | ||
export default [ | ||
{ | ||
@@ -148,2 +166,5 @@ settings: { | ||
], | ||
// use <root>/jsconfig.json | ||
project: 'jsconfig.json', | ||
}, | ||
@@ -196,2 +217,5 @@ }, | ||
], | ||
// use <root>/jsconfig.json | ||
"project": "jsconfig.json", | ||
}, | ||
@@ -203,3 +227,3 @@ }, | ||
## Options from [`enhanced-resolve`][] | ||
## Options from [`rspack-resolver`][] | ||
@@ -288,3 +312,3 @@ ### `conditionNames` | ||
You can pass through other options of [`enhanced-resolve`][] directly | ||
You can pass through other options of [`rspack-resolver`][] directly | ||
@@ -306,2 +330,4 @@ ### Default options | ||
[](https://github.com/sponsors/JounQin) | ||
## Sponsors | ||
@@ -329,4 +355,4 @@ | ||
[`eslint-plugin-import-x`]: https://github.com/un-ts/eslint-plugin-import-x | ||
[`enhanced-resolve`]: https://github.com/webpack/enhanced-resolve | ||
[`rspack-resolver`]: https://github.com/unrs/rspack-resolver | ||
[`typescript`]: https://www.typescriptlang.org | ||
[isc]: https://opensource.org/licenses/ISC |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
9
-10%22
175%347
8.1%53101
-16.26%773
-32.72%1
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated