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.
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
- 1Pick packages. You pass one or more package directories. Each must contain a
package.json. - 2List files.
git ls-fileslists tracked.js,.jsx,.ts, and.tsxfiles in the package, so ignored and generated files are skipped. - 3Refactor in parallel. Each file gets its own
droid execrun at--auto low, started inside the package with--cwd. Only imports change. - 4Verify. After a package finishes, the script runs
tsc --noEmitfor it if it has atsconfig.json, and stops on the first failure.
Prerequisites
- The Droid CLI is installed and
FACTORY_API_KEYis set. See Droid Exec installation. - TypeScript packages can be type-checked with
npx tsc --noEmit -p <package>.
The script
#!/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
doneRun it
Preview file counts first:
DRY_RUN=true ./droid-organize-imports.sh packages/* apps/*Then run one package and review the diff before you do the rest:
./droid-organize-imports.sh packages/ui
git diff --statA typical result groups external, alias, and relative imports, merges duplicates, and drops unused imports:
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';-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 typestatements. Without a rule, Droid may merge them into inlinetypespecifiers, 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:
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