Skip to content
This repository has been archived by the owner on Aug 16, 2022. It is now read-only.

Commit

Permalink
fix: ignore directory indexes (#2)
Browse files Browse the repository at this point in the history
* feat(typescript): `TrextNodePath`

* refactor(plugins): consolidate `Trextel` transformation logic

* fix(plugins): ignore directory indexes
  • Loading branch information
unicornware authored Oct 13, 2021
1 parent a14fd44 commit 3313025
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 62 deletions.
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const TREXT_OPTIONS: TrextOptions<'js', 'mjs'> = {
to: 'mjs'
}

trext('esm', TREXT_OPTIONS)
trext('esm/', TREXT_OPTIONS)
.then(results => console.info(inspect(results, false, null)))
.catch(error => console.error(inspect(error, false, null)))
```
Expand Down Expand Up @@ -174,6 +174,52 @@ trext('esm/', TREXT_OPTIONS)
.catch(error => console.error(inspect(error, false, null)))
```

#### Ignoring Directory Indexes

Directory entry points are a common way of exposing a series of modules from a
single `index.*` file. Directory index syntax allows developers to `import` and
`require` those entries without including `/index.*` in the module specifier:

```typescript
/**
* @file Package Entry Point
* @module trext
*/

export { default as TREXT_DEFAULTS } from './config/defaults.config'
export * from './interfaces'
export { default as Trext, trext, trextFile } from './plugins/trext.plugin'
export * from './types'
```

Specifiers `'./interfaces'` and `'./types'` use directory index syntax and
**will be ignored** when encountered by [`Trextel`][6].

By default, index lookups are performed in the `process.cwd()/src` directory.
Set `src` to change the lookup location:

```typescript
import type { TrextOptions } from '@flex-development/trext'
import { trext } from '@flex-development/trext'
import { inspect } from 'util'

/**
* @file Examples - Ignoring Directory Indexes
* @module docs/examples/src
*/

const TREXT_OPTIONS: TrextOptions<'js', 'cjs'> = {
from: 'js',
pattern: /.js$/,
src: 'lib',
to: 'cjs'
}

trext('cjs/', TREXT_OPTIONS)
.then(results => console.info(inspect(results, false, null)))
.catch(error => console.error(inspect(error, false, null)))
```

## Built With

- [@babel/core][1] - Babel compiler core
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/basic-usage.example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ const TREXT_OPTIONS: TrextOptions<'js', 'mjs'> = {
to: 'mjs'
}

trext('esm', TREXT_OPTIONS)
trext('esm/', TREXT_OPTIONS)
.then(results => console.info(inspect(results, false, null)))
.catch(error => console.error(inspect(error, false, null)))
19 changes: 19 additions & 0 deletions docs/examples/src.example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { TrextOptions } from '@flex-development/trext'
import { trext } from '@flex-development/trext'
import { inspect } from 'util'

/**
* @file Examples - Ignoring Directory Indexes
* @module docs/examples/src
*/

const TREXT_OPTIONS: TrextOptions<'js', 'cjs'> = {
from: 'js',
pattern: /.js$/,
src: 'lib',
to: 'cjs'
}

trext('cjs/', TREXT_OPTIONS)
.then(results => console.info(inspect(results, false, null)))
.catch(error => console.error(inspect(error, false, null)))
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@
"@babel/types": "7.15.6",
"@types/babel__core": "7.1.16",
"glob": "7.2.0",
"mkdirp": "1.0.4"
"mkdirp": "1.0.4",
"path-type": "5.0.0"
},
"devDependencies": {
"@babel/eslint-parser": "7.15.8",
Expand Down
3 changes: 2 additions & 1 deletion src/config/defaults.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type { TrextDefaults } from '@trext/types'

const DEFAULTS: TrextDefaults = {
babel: {},
pattern: /\..+$/
pattern: /\..+$/,
src: `${process.cwd()}/src`
}

export default DEFAULTS
11 changes: 11 additions & 0 deletions src/interfaces/trext-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ interface TrextOptions<F extends string = string, T extends string = string> {
*/
pattern?: RegexString

/**
* Directory where source files are located.
*
* Used to identify and ignore `import` and `require` statements that include
* directory entry points without a specifier or a `/index` suffix (i.e: `from
* './types'`, where `./types/index.*` is the file being imported).
*
* @default `${process.cwd()}/src`
*/
src?: string

/**
* New file extension or function that returns a file extension.
*
Expand Down
12 changes: 12 additions & 0 deletions src/plugins/__tests__/trextel.plugin.functional.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ describe('functional:plugins/Trextel', () => {
do: 'not change extension if require is not string literal',
expected: { arguments: [] },
state: { opts: { from: 'js', to: 'mjs' } }
},
{
_arguments: [stringLiteral('../interfaces')],
do: 'ignore require statement if require is directory entry point',
expected: { value: '../interfaces' },
state: { opts: { from: 'js', to: 'mjs' } }
}
]

Expand Down Expand Up @@ -95,6 +101,12 @@ describe('functional:plugins/Trextel', () => {
expected: { value: pkg.name },
source: stringLiteral(pkg.name),
state: { opts: { from: 'js', to: 'cjs' } }
},
{
do: 'ignore import declaration if import is directory entry point',
expected: { value: '../types' },
source: stringLiteral('../types'),
state: { opts: { from: 'js', to: 'mjs' } }
}
]

Expand Down
124 changes: 72 additions & 52 deletions src/plugins/trextel.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
importDeclaration,
stringLiteral
} from '@babel/types'
import DEFAULTS from '@trext/config/defaults.config'
import { TrextelState } from '@trext/interfaces'
import { TrextNodePath } from '@trext/types'
import { isDirectorySync as isDirectory } from 'path-type'

/**
* @file Plugins - Trextel
Expand Down Expand Up @@ -45,87 +48,104 @@ class Trextel<F extends string = string, T extends string = string>
}

/**
* Transforms call expressions to use `options.to`.
* Transforms call expressions and import statements to use `options.to`.
*
* @param {NodePath<CallExpression>} nodePath - Current node path
* @template F - Old file extension name(s)
* @template T - New file extension name(s)
*
* @param {TrextNodePath} nodePath - Current node path
* @param {TrextelState<F, T>} state - Plugin state
* @return {void} Nothing when complete
*/
CallExpression(
nodePath: NodePath<CallExpression>,
static transform<F extends string = string, T extends string = string>(
nodePath: TrextNodePath,
state: TrextelState<F, T>
): void {
// Get call expression node and callee
const node = nodePath.node
const callee = node.callee
const { name, type } = node.callee as Record<'name' | 'type', string>

// Filter out callee by name or type
if (type !== 'Identifier' || name !== 'require') return

// Get first argument passed
const nargs = node.arguments
const arg = nargs[0]

// Do nothing for multiple args or if handling a non-string literal
if (nargs.length !== 1 || arg.type !== 'StringLiteral') return
): TrextNodePath | void {
// Get node
let node = nodePath.node
const {
arguments: args,
callee,
source,
specifiers,
type
} = node as Record<string, any>

// Get user options
const { from, src, to } = { ...DEFAULTS, ...state.opts }

// Get source code
const code: string = (type === 'CallExpression' ? args[0] : source).value

// Ignore directory entry points
if (isDirectory(`${src}${code.slice(code.indexOf('/'))}`)) return

// Ignore absolute imports
if (!/^\./.test(arg.value)) return
if (!/^\./.test(code)) return

// Get output extension
const to = state.opts.to
let $to = typeof to === 'function' ? to(nodePath) : to
if (!$to.startsWith('.')) $to = `.${$to}`

// Ignore already converted extensions
if (new RegExp(`\.${Trextel.escapeSpecials($to)}$`).test(arg.value)) return
if (new RegExp(`\.${Trextel.escapeSpecials($to)}$`).test(code)) return

// Escape special characters in input extension
const $from = new RegExp(`\.${Trextel.escapeSpecials(state.opts.from)}$|$`)
const $from = new RegExp(`\.${Trextel.escapeSpecials(from)}$|$`)

// Create string literal
const $code = stringLiteral(code.replace($from, $to))

// Transform call expression or import statement
switch (type) {
case 'CallExpression':
node = callExpression(callee, [$code])
break
case 'ImportDeclaration':
node = importDeclaration(specifiers, $code)
break
default:
break
}

// Transform call expression
nodePath.replaceWith(
callExpression(callee, [stringLiteral(arg.value.replace($from, $to))])
)
nodePath.replaceWith(node)
}

/**
* Transforms import statements to use `options.to`.
* Transforms call expressions to use `options.to`.
*
* @param {NodePath<ImportDeclaration>} nodePath - Current node path
* @param {NodePath<CallExpression>} nodePath - Current node path
* @param {TrextelState<F, T>} state - Plugin state
* @return {void} Nothing when complete
*/
ImportDeclaration(
nodePath: NodePath<ImportDeclaration>,
CallExpression(
nodePath: NodePath<CallExpression>,
state: TrextelState<F, T>
): void {
// Get import declaration node and source code
const node = nodePath.node
const code = node.source.value
const { arguments: args, callee } = nodePath.node

// Ignore absolute imports
if (!/^\./.test(code)) return
// Filter out by callee name and type
if (callee.type !== 'Identifier' || callee.name !== 'require') return

// Get output extension
const to = state.opts.to
let $to = typeof to === 'function' ? to(nodePath) : to
if (!$to.startsWith('.')) $to = `.${$to}`
// Do nothing for multiple args or if handling a non-string literal
if (args.length !== 1 || args[0].type !== 'StringLiteral') return

// Ignore already converted extensions
if (new RegExp(`\.${Trextel.escapeSpecials($to)}$`).test(code)) return
// Transform node
Trextel.transform(nodePath, state)
}

// Escape special characters in input extension
const $from = new RegExp(`\.${Trextel.escapeSpecials(state.opts.from)}$|$`)

// Transform import statement
nodePath.replaceWith(
importDeclaration(
node.specifiers,
stringLiteral(code.replace($from, $to))
)
)
/**
* Transforms import statements to use `options.to`.
*
* @param {NodePath<ImportDeclaration>} nodePath - Current node path
* @param {TrextelState<F, T>} state - Plugin state
* @return {void} Nothing when complete
*/
ImportDeclaration(
nodePath: NodePath<ImportDeclaration>,
state: TrextelState<F, T>
): void {
Trextel.transform(nodePath, state)
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ export type { default as SourceMapComment } from './source-map-comment.type'
export type { default as TrextDefaults } from './trext-defaults.type'
export type { default as TrextFileResult } from './trext-file-result.type'
export type { default as TrextMatch } from './trext-match.type'
export type { default as TrextNodePath } from './trext-node-path.type'
export type { default as TrextToFn } from './trext-to-fn.type'
export type { default as TrextTo } from './trext-to.type'
3 changes: 3 additions & 0 deletions src/types/trext-defaults.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ type TrextDefaults = {

/** {@link TrextOptions#pattern} */
pattern: Exclude<NonNullable<TrextOptions['pattern']>, string>

/** {@link TrextOptions#src} */
src: string
}

export default TrextDefaults
11 changes: 5 additions & 6 deletions src/types/trext-match.type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { NodePath } from '@babel/core'
import type { CallExpression, ImportDeclaration } from '@babel/types'
import type RegexString from './regex-string.type'
import type TrextNodePath from './trext-node-path.type'

/**
* @file Type Definitions - TrextMatch
Expand All @@ -9,13 +8,13 @@ import type RegexString from './regex-string.type'

/**
* The matched substring when [replacing a file extension][1], or when used
* within `Trextel`, a custom Babel plugin, a type of {@link NodePath} object:
* within `Trextel`, our custom Babel plugin, a type of `NodePath` object:
*
* - `NodePath<CallExpression>`; see {@link CallExpression}
* - `NodePath<ImportDeclaration>`; see {@link ImportDeclaration}
* - `NodePath<CallExpression>`
* - `NodePath<ImportDeclaration>`
*
* [1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_a_parameter
*/
type TrextMatch = RegexString | NodePath<CallExpression | ImportDeclaration>
type TrextMatch = RegexString | TrextNodePath

export default TrextMatch
19 changes: 19 additions & 0 deletions src/types/trext-node-path.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { NodePath } from '@babel/core'
import type { CallExpression, ImportDeclaration } from '@babel/types'

/**
* @file Type Definitions - TrextNodePath
* @module trext/types/TrextNodePath
*/

/**
* {@link NodePath} objects used by `Trextel`, our [custom Babel plugin][1].
*
* - See {@link CallExpression}
* - See {@link ImportDeclaration}
*
* [1]: https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md
*/
type TrextNodePath = NodePath<CallExpression | ImportDeclaration>

export default TrextNodePath
Loading

0 comments on commit 3313025

Please sign in to comment.