Organize imports with Droid Exec

Group, sort, and clean up imports across every package in a monorepo with droid exec, then type-check each package before you commit.

Use this recipe to make imports consistent across a JavaScript or TypeScript codebase: grouped, sorted, deduplicated, and free of unused entries. The script works one package at a time and runs Droid inside each package, so path aliases resolve against that package's own tsconfig.json. That is what keeps it safe in a monorepo.

Tip

If plain sorting is all you need, a deterministic tool such as eslint-plugin-import's import/order rule with --fix is faster and cheaper. Use Droid for the parts those tools skip, such as converting require() calls and following house conventions that no rule encodes.

How it works

  1. 1
    Pick packages. You pass one or more package directories. Each must contain a package.json.
  2. 2
    List files. git ls-files lists tracked .js, .jsx, .ts, and .tsx files in the package, so ignored and generated files are skipped.
  3. 3
    Refactor in parallel. Each file gets its own droid exec run at --auto low, started inside the package with --cwd. Only imports change.
  4. 4
    Verify. After a package finishes, the script runs tsc --noEmit for it if it has a tsconfig.json, and stops on the first failure.

Prerequisites

  • The Droid CLI is installed and FACTORY_API_KEY is set. See Droid Exec installation.
  • TypeScript packages can be type-checked with npx tsc --noEmit -p <package>.

The script

droid-organize-imports.sh
#!/usr/bin/env bash
# Organize imports with droid exec, one package at a time.
# Usage: ./droid-organize-imports.sh <package-dir> [<package-dir> ...]
set -euo pipefail
 
CONCURRENCY="${CONCURRENCY:-4}"
DRY_RUN="${DRY_RUN:-false}"
 
PROMPT='Organize the imports in the file named below.
Change only import and require statements.
1. Group imports in this order, with one blank line between groups:
   external packages, then internal aliases (such as @/ or a workspace
   package), then relative paths.
2. Sort alphabetically within each group.
3. Remove imports that are not used. Keep side-effect imports such as
   import "./polyfills", in their original order.
4. Merge duplicate imports from the same module.
5. Convert require() to import only if this package is already an ES module.
Do not change what any import resolves to.
Do not touch code outside the import block.'
 
organize_file() {
  local pkg="$1" file="$2"
  # --cwd needs an absolute path.
  if droid exec --auto low --cwd "$(cd "$pkg" && pwd)" "$PROMPT
File: $file" > /dev/null; then
    echo "done: $pkg/$file"
  else
    echo "failed: $pkg/$file" >&2
    return 1
  fi
}
export -f organize_file
export PROMPT
 
for pkg in "$@"; do
  if [ ! -f "$pkg/package.json" ]; then
    echo "skip: $pkg has no package.json" >&2
    continue
  fi
 
  files=$(git -C "$pkg" ls-files '*.js' '*.jsx' '*.ts' '*.tsx')
  if [ -z "$files" ]; then
    echo "skip: $pkg has no JS or TS files" >&2
    continue
  fi
  echo "$pkg: $(echo "$files" | grep -c .) files"
  [ "$DRY_RUN" = "true" ] && continue
 
  echo "$files" | tr '\n' '\0' | xargs -0 -P "$CONCURRENCY" -I {} \
    bash -c 'organize_file "$1" "$2"' _ "$pkg" {}
 
  if [ -f "$pkg/tsconfig.json" ] && ! npx tsc --noEmit -p "$pkg"; then
    echo "type check failed in $pkg" >&2
    exit 1
  fi
done

Run it

Preview file counts first:

Bash
DRY_RUN=true ./droid-organize-imports.sh packages/* apps/*

Then run one package and review the diff before you do the rest:

Bash
./droid-organize-imports.sh packages/ui
git diff --stat
The organize imports script processing four files in packages/api and three in packages/ui (sped up)

A typical result groups external, alias, and relative imports, merges duplicates, and drops unused imports:

Diff
 import { readFileSync } from 'node:fs';
-import { HttpError, NotFoundError } from './errors';
-import { saveUser, findUser } from './db';
 import { join } from 'node:path';
-import type { User } from './db';
-import { audit } from './db';
+
+import { audit, findUser, saveUser, type User } from './db';
+import { HttpError, NotFoundError } from './errors';
Diff
-import { onClick } from './Button';
 import { formatName } from '@/format';
-import { join } from 'node:path';
+
+import { onClick } from './Button';
 export { onClick, formatName };

Adapt the rules

Edit PROMPT to match your conventions. Common changes:

  • Say whether type-only imports stay as separate import type statements. Without a rule, Droid may merge them into inline type specifiers, as in the example above.
  • Name your internal scopes, such as @acme/*, so Droid groups them with internal aliases.
  • List modules whose import order matters, such as CSS resets or instrumentation, and tell Droid to leave them first.

When your conventions are settled, add a lint rule for them so new code stays consistent and later runs only touch files that drift.

Open a pull request

Commit each package separately so reviewers can check them independently:

Bash
git checkout -b chore/organize-imports
git add packages/ui && git commit -m "chore(ui): organize imports"
git add packages/api && git commit -m "chore(api): organize imports"
git push -u origin HEAD
gh pr create --fill