indexit

A small cli app for managing (auto-filling) javascript/typescript index files.

Usage no npm install needed!

<script type="module">
  import indexit from 'https://cdn.skypack.dev/indexit';
</script>

README

Build Release

Indexit is a small cli app for managing javascript and/or typescript index files.

Install

npm install indexit
# or
yarn add indexit

Usage

Anywhere you want an index file to be managed you add the comment /* Autogenerated Index */ and from there to the end of the file, the program can do it's thing (exports will be exported alphabetically by their "name").

Then you can run indexit test (this is the equivalent of running indexit test src/** src/) to see a preview of the indexes it would write to and what would be written.

You can list as many globs as you want: indexit test glob1/** glob2/** glob3/**.

Notes

  • If you get no results note that you must use forward slashes / for file paths in the glob. Backslashes are only for escaping characters.
  • If you want the indexes at, for example, glob1 (glob1/index.js) to be allowed to be written to you must also add glob1/ to the globs, otherwise it won't work. This is why the default is src/** src/.
  • The node_modules directory is always ignored.

To actually write the changes run indexit update with the options you tested.

IMPORTANT!: git add/commit your changes before running or double check the test output thoroughly.

Prerequisites

This program makes several assumptions:

  • All non-index files export exactly one export.
  • If you manually manage a folder's index file it is only ever exporting one type of export, that is, it should not have, for example, both named and default exports, or both typescript types and named exports, etc.

In either case, only the first found will be used.

You can find a good example of how things get exported in the tests/fixtures folder, the index.expected.ts files are what would be written.

The Start/End Tags

The start and end tags delimit the editable parts of the index files, so you can still have comments before/after them.

The default start tag is /* Autogenerated Index [OPTIONS] */, and there is no default end tag, the editable area extends until the end of the file.

The options bit in the start tag allows you to specify how that index file will be exported.

By default we take /* Autogenerated Index */ to mean /* Autogenerated Index [Named]*/ since if we took it to mean the options were empty, nothing would be generated.

To force pass no options you would do: /* Autogenerated Index [] */.

To pass multiple options, just separate them with a coma: /* Autogenerated Index [Named, Ignore] */

Note: Options are case insensitive.

Modifying the Tags

The section of index files the app is allowed to modify can be managed with the flags --tag.start and --tag.end. The tag start must include the [OPTIONS] part(e.g. /* Autogenerated Index [OPTIONS] */) so we can extract any options from the index files.

Note: It is assumed you will want /* Autogenerated Index */ to match as well even though it's missing the extra space where we remove the [OPTIONS] part, so spaces after that bit are allowed.

Options

Options: Named, Named-Unwrapped, Default, Types, Types-Namespaced, Ignore

You cannot have both Named/Named-Unwrapped and Default exports, but all other combinations are supported.

Named, Named-Unwrapped, and Default

These two options determine how regular exports are generated and how that files is re-exported if it is.

Named

Files

For files, it produces exports like this:

export { functionName } from "fileName"

Folders

For folders it depends whether it contains named or default exports.

Folders - Named

If the folder contains named exports, by default we use export ns from syntax:

export * as folderName from "./folderName"

...but this might not be supported by your compiler. In that case you can pass --no-wildcard-exports to get exports like this:

import * as _folderName from "./folderName"
export const folderName = _folderName

Folders - Default

If the folder contains a default export it depends on whether the default export is named or not, in which case the name detected will be used instead of the folder's name.

export { default as folderName } from "./folderName"
// or if the default export is named:
// export default = function defaultName () {...}
export { default as defaultName } from "fileName"

The --no-default-named flag can be passed to always set the name to the folder name.

Named-Unwrapped

export * from "./folderName"

Default

import { named } from "./named"
import { default as defaultName } from ".defaultName"
import * as folderNamed from ".folderNamed"
import folderDefault from ".folderDefault"
export default {
    defaultName,
    folderDefault,
    folderNamed,
    named,
    // ...
}

Ignore

Occasionally you might not want a nested folder to get re-exported in it's parent:

For example, say we divided our helpers into different folders. You wouldn't want to import a function from helper through helpers, since it can't be tree shaken because helpers re-exports the entirety of the type folder (regardless of it's export type).

root/src
└──helpers
    └── type
        └── helper

If type uses the Ignore option, helpers won't re-export it, that way the helper folder can exist purely for organizational purposes without re-export anything.

Types

export * from ".typeFile"

Types-Namespaced

export * as typeFile from ".typeFile"

CLI Options

The program also provides a few other useful options to control the output (e.g. --sort, --spaces, --newlines, etc). Run indexit test --help for details.

How it Works

After an index file with a header is found, it looks at all sibling files:

If the sibling contains a string like export function/const/let, it is assumed you want a named export, e.g. export {name} from ".fileName".

If the sibling contains the string export default, we try to look for the name of the export (e.g. export default someVar) to export it like export someVar from "./fileName", otherwise if there's an anonymous function (e.g.export default function() {}) or an object we use the file name: export fileName from "./fileName"

If the sibling is a folder with an index file and it is assumed you want to re-export it using the folder's name as the name. If the index is autogenerated the options in the start tag are used to determine how it's exported, otherwise if it's manually generated we look at the first export type we find.

Note how almost everything has a "name" one way or another.

Other

I have taken care that async, typescript generic function names, and any weird characters in function names are correctly identified, though there might still be more cases that don't work.

Currently the following regex is used to extract the portion with the name: https://regexr.com/4s9ol (you can see the names by using the list and inputting $2\n).

Todos

  • Error Handling if we can't write file.
  • Warn when an index file matches a glob but no tag was detected.
  • Option when using test to only print out the parts between the start/end tags.
  • Tests for sort/order and formatting options.
  • Tests for start/end tag handling.