Skip to content

Commit 4014515

Browse files
committed
feat(resolve): resolveAlias
Signed-off-by: Lexus Drumgold <unicornware@flexdevelopment.llc>
1 parent 08b9124 commit 4014515

14 files changed

+409
-1
lines changed

.eslintrc.cjs

+9-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@
1111
const config = {
1212
root: true,
1313
extends: ['./.eslintrc.base.cjs'],
14-
overrides: [...require('./.eslintrc.base.cjs').overrides]
14+
overrides: [
15+
...require('./.eslintrc.base.cjs').overrides,
16+
{
17+
files: ['src/lib/resolve-alias.ts'],
18+
rules: {
19+
'@typescript-eslint/unbound-method': 0
20+
}
21+
}
22+
]
1523
}
1624

1725
module.exports = config

__mocks__/.gitkeep

Whitespace-only changes.

__mocks__/pathe.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @file Mocks - pathe
3+
* @module mocks/pathe
4+
* @see https://github.com/unjs/pathe
5+
*/
6+
7+
/**
8+
* [`pathe`][1] module type.
9+
*
10+
* [1]: https://github.com/unjs/pathe
11+
*/
12+
type Actual = typeof import('pathe')
13+
14+
/**
15+
* `pathe` module.
16+
*
17+
* @const {Actual} actual
18+
*/
19+
const actual: Actual = await vi.importActual<Actual>('pathe')
20+
21+
export const dirname = vi.fn(actual.dirname)
22+
export const relative = vi.fn(actual.relative)
23+
export const resolve = vi.fn(actual.resolve)

__mocks__/tsconfig-paths/index.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @file Mocks - tsconfig-paths
3+
* @module mocks/tsconfig-paths
4+
* @see https://github.com/dividab/tsconfig-paths
5+
*/
6+
7+
/**
8+
* [`tsconfig-paths`][1] module type.
9+
*
10+
* [1]: https://github.com/dividab/tsconfig-paths
11+
*/
12+
type Actual = typeof import('tsconfig-paths')
13+
14+
/**
15+
* `tsconfig-paths` module.
16+
*
17+
* @const {Actual} actual
18+
*/
19+
const actual: Actual = await vi.importActual<Actual>('tsconfig-paths')
20+
21+
export const createMatchPath = vi.fn(actual.createMatchPath)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @file Mocks - tsconfig-paths/lib/tsconfig-loader
3+
* @module mocks/tsconfig-paths/lib/tsconfig-loader
4+
* @see https://github.com/dividab/tsconfig-paths
5+
*/
6+
7+
/**
8+
* `tsconfig-paths/lib/tsconfig-loader` module type.
9+
*/
10+
type Actual = typeof import('tsconfig-paths/lib/tsconfig-loader')
11+
12+
/**
13+
* Mocked module path.
14+
*
15+
* @const {string} path
16+
*/
17+
const path: string = 'tsconfig-paths/lib/tsconfig-loader'
18+
19+
/**
20+
* `tsconfig-paths/lib/tsconfig-loader` module.
21+
*
22+
* @const {Actual} actual
23+
*/
24+
const actual: Actual = await vi.importActual<Actual>(path)
25+
26+
export const loadTsconfig = vi.fn(actual.loadTsconfig)

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@
6969
"test:cov": "yarn test --coverage",
7070
"test:watch": "vitest"
7171
},
72+
"dependencies": {
73+
"pathe": "0.3.8",
74+
"tsconfig-paths": "4.1.0"
75+
},
7276
"devDependencies": {
7377
"@commitlint/cli": "17.1.2",
7478
"@commitlint/config-conventional": "17.1.0",

src/constants.ts

+18
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,24 @@ export const EXPORT_STAR_REGEX: RegExp =
6363
export const REQUIRE_STATEMENT_REGEX: RegExp =
6464
/(?<!(?:\/\/|\*).*)(?:\bconst[ {]+(?<imports>[\w\t\n\r $*,/]+)[ =}]+)?(?<type>\brequire(?:\.resolve)?)\(["'](?<specifier>[\w./-]+)["']\)/gm
6565

66+
/**
67+
* Default resolvable file extensions.
68+
*
69+
* @const {ReadonlyArray<string>} RESOLVE_EXTENSIONS
70+
*/
71+
export const RESOLVE_EXTENSIONS: readonly string[] = Object.freeze([
72+
'.cjs',
73+
'.css',
74+
'.cts',
75+
'.js',
76+
'.json',
77+
'.jsx',
78+
'.mjs',
79+
'.mts',
80+
'.ts',
81+
'.tsx'
82+
])
83+
6684
/**
6785
* Static import statement regex.
6886
*

src/interfaces/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
export type { default as DynamicImport } from './import-dynamic'
77
export type { default as StaticImport } from './import-static'
8+
export type { default as AliasResolverOptions } from './options-alias-resolver'
89
export type { default as Statement } from './statement'
910
export type { default as ExportStatement } from './statement-export'
1011
export type { default as ImportStatement } from './statement-import'
+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @file Interfaces - AliasResolverOptions
3+
* @module mlly/interfaces/AliasResolverOptions
4+
*/
5+
6+
import type { OneOrMany } from '@flex-development/tutils'
7+
8+
/**
9+
* Path alias resolution options.
10+
*
11+
* @see https://github.com/dividab/tsconfig-paths
12+
*/
13+
interface AliasResolverOptions {
14+
/**
15+
* Base directory to resolve non-absolute module names.
16+
*
17+
* @see https://www.typescriptlang.org/tsconfig#baseUrl
18+
*
19+
* @default process.cwd()
20+
*/
21+
baseUrl?: string
22+
23+
/**
24+
* Module extensions to probe for.
25+
*
26+
* @default RESOLVE_EXTENSIONS
27+
*/
28+
extensions?: string[]
29+
30+
/**
31+
* Checks for the existence of a file at `path`.
32+
*
33+
* @param {string} path - Path to check
34+
* @return {boolean} `true` if file exists, `false` otherwise
35+
*/
36+
fileExists?(path: string): boolean
37+
38+
/**
39+
* `package.json` fields to check when resolving modules.
40+
*
41+
* A nested field can be selected by passing an array of field names.
42+
*
43+
* @default ['main', 'module']
44+
*/
45+
mainFields?: OneOrMany<string>[]
46+
47+
/**
48+
* Path mappings.
49+
*
50+
* **Note**: Should be relative to {@link baseUrl}.
51+
*
52+
* @see https://www.typescriptlang.org/tsconfig#paths
53+
*
54+
* @default {}
55+
*/
56+
paths?: Record<string, string[]>
57+
58+
/**
59+
* Synchronously returns the contents of `filename`.
60+
*
61+
* @param {string} filename - Filename
62+
* @return {string} Contents of `filename`
63+
*/
64+
readFile?(filename: string): string
65+
66+
/**
67+
* Absolute path to tsconfig file.
68+
*/
69+
tsconfig?: string
70+
71+
/**
72+
* Absolute path to file containing path alias.
73+
*/
74+
url?: string
75+
}
76+
77+
export type { AliasResolverOptions as default }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @file Functional Tests - resolveAlias
3+
* @module mlly/lib/tests/resolveAlias/functional
4+
*/
5+
6+
import type { Spy } from '#tests/interfaces'
7+
import path from 'node:path'
8+
import * as pathe from 'pathe'
9+
import { createMatchPath } from 'tsconfig-paths'
10+
import { loadTsconfig } from 'tsconfig-paths/lib/tsconfig-loader'
11+
import tsconfig from '../../../tsconfig.json' assert { type: 'json' }
12+
import testSubject from '../resolve-alias'
13+
14+
vi.mock('pathe')
15+
vi.mock('tsconfig-paths')
16+
vi.mock('tsconfig-paths/lib/tsconfig-loader')
17+
18+
describe('functional:lib/resolveAlias', () => {
19+
const specifier: string = '#src/lib/resolve-alias'
20+
const tscpath: string = path.resolve('tsconfig.json')
21+
22+
it('should load tsconfig file', () => {
23+
// Act
24+
;(loadTsconfig as unknown as Spy).mockReturnValueOnce(undefined)
25+
testSubject(specifier, { tsconfig: tscpath })
26+
27+
// Expect
28+
expect(loadTsconfig).toHaveBeenCalledOnce()
29+
expect(loadTsconfig).toHaveBeenCalledWith(tscpath, undefined, undefined)
30+
})
31+
32+
it('should use baseUrl and paths from tsconfig', () => {
33+
// Arrange
34+
const { baseUrl, paths } = tsconfig.compilerOptions
35+
36+
// Act
37+
;(loadTsconfig as unknown as Spy).mockReturnValueOnce(tsconfig)
38+
testSubject(specifier, { tsconfig: tscpath })
39+
40+
// Expect
41+
expect(pathe.dirname).toHaveBeenCalledOnce()
42+
expect(pathe.dirname).toHaveBeenCalledWith(tscpath)
43+
expect(pathe.resolve).toHaveBeenCalledOnce()
44+
expect(pathe.resolve).toHaveBeenCalledWith(path.dirname(tscpath), baseUrl)
45+
expect(createMatchPath).toHaveBeenCalledOnce()
46+
expect(createMatchPath).toHaveBeenCalledWith(
47+
path.resolve(path.dirname(tscpath), baseUrl),
48+
paths,
49+
['main', 'module'],
50+
true
51+
)
52+
})
53+
})
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* @file Unit Tests - resolveAlias
3+
* @module mlly/lib/tests/resolveAlias/unit
4+
*/
5+
6+
import type { AliasResolverOptions as Options } from '#src/interfaces'
7+
import path from 'node:path'
8+
import testSubject from '../resolve-alias'
9+
10+
describe('unit:lib/resolveAlias', () => {
11+
const paths: NonNullable<Options['paths']> = {
12+
'#fixtures/*': ['__fixtures__/*'],
13+
'#src/*': ['src/*'],
14+
'@flex-development/tutils/*': [
15+
'node_modules/@flex-development/tutils/dist/*'
16+
]
17+
}
18+
19+
it('should convert alias from unknown source to absolute specifier', () => {
20+
// Act
21+
const result = testSubject('#src/lib/detect-syntax', { paths })
22+
23+
// Expect
24+
expect(result).to.equal(path.join(process.cwd(), 'src/lib/detect-syntax'))
25+
})
26+
27+
it('should convert current directory alias to relative specifier', () => {
28+
// Arrange
29+
const specifier = '#src/constants'
30+
const url = path.resolve('src/index.ts')
31+
32+
// Act + Expect
33+
expect(testSubject(specifier, { paths, url })).to.equal('./constants')
34+
})
35+
36+
it('should convert module alias to relative specifier', () => {
37+
// Arrange
38+
const expected = '../../../__fixtures__/aggregate-error-ponyfill.cjs'
39+
const specifier = '#fixtures/aggregate-error-ponyfill.cjs'
40+
const url = path.resolve('src/lib/__tests__/detect-syntax.spec.ts')
41+
42+
// Act + Expect
43+
expect(testSubject(specifier, { paths, url })).to.equal(expected)
44+
})
45+
46+
it('should convert node_modules alias to bare specifier', () => {
47+
// Arrange
48+
const expected = '@flex-development/tutils/dist/guards/is-node-env'
49+
const specifier = '@flex-development/tutils/guards/is-node-env'
50+
51+
// Act + Expect
52+
expect(testSubject(specifier, { paths })).to.equal(expected)
53+
})
54+
55+
it('should convert parent directory alias to relative specifier', () => {
56+
// Arrange
57+
const url = path.resolve('src/interfaces/statement.ts')
58+
59+
// Act + Expect
60+
expect(testSubject('#src/types', { paths, url })).to.equal('../types')
61+
})
62+
63+
it('should return specifier if path match was not found', () => {
64+
// Arrange
65+
const specifier: string = '#src/lib/resolve-alias'
66+
67+
// Act
68+
const results = testSubject(specifier, { paths: {} })
69+
70+
// Expect
71+
expect(results).to.equal(specifier)
72+
})
73+
})

src/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export { default as findRequires } from './find-requires'
1111
export { default as findStaticImports } from './find-static-imports'
1212
export { default as hasCJSSyntax } from './has-cjs-syntax'
1313
export { default as hasESMSyntax } from './has-esm-syntax'
14+
export { default as resolveAlias } from './resolve-alias'

0 commit comments

Comments
 (0)