Files
my-pi/pi-permission-system/docs/plans/archive/0032-drop-js-import-extensions.md
T

101 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
issue: 32
issue_title: "Drop .js extensions from all internal imports"
---
# Drop `.js` extensions from all internal imports
## Problem Statement
All internal imports in `src/` and `tests/` carry explicit `.js` extensions (e.g. `from "./common.js"`).
This convention is required by `moduleResolution: "Node16"` / `"NodeNext"`, but the project uses `moduleResolution: "Bundler"` with `noEmit: true`, so the extensions serve no purpose.
They add noise to every new file and are a common source of copy-paste mistakes.
## Goals
- Strip `.js` from every relative import path in `src/` and `tests/` (~116 occurrences across ~30 files).
- Add a guard so `.js` extensions do not creep back in.
- Verify `npx vitest run` and `npm run build` both pass after the change.
## Non-Goals
- No behavior change to the permission system.
- No changes to `package.json`, `tsconfig.json`, or the public API.
- No changes to imports of `node:*` builtins or bare package specifiers (only relative `./` and `../` paths).
## Background
- `tsconfig.json` uses `"module": "ESNext"` and `"moduleResolution": "Bundler"` — both resolve extensionless relative imports natively.
- Vitest transforms via esbuild, which also resolves extensionless imports.
- `tsc` is `"noEmit": true` — Node's strict-ESM extension rule never fires.
- **Runtime loading**: Pi loads extensions via `@mariozechner/jiti` (a fork of `unjs/jiti`), which uses esbuild internally and resolves extensionless `.ts` imports the same way Vitest does.
Empirically verified: stripping `.js` from `src/permission-manager.ts` imports passes both `npm run build` and `npx vitest run` with no changes.
- Biome has `useImportExtensions` (enforces extensions), which is the opposite of what we want.
There is no built-in Biome rule to *ban* extensions, so prevention uses a lint script.
No permission surface is involved — this is a purely mechanical code-quality change.
## Design Overview
### Step 1: Mechanical find-and-replace
Use `sed` or a script to strip `.js` from every relative import in `src/**/*.ts` and `tests/**/*.ts`:
```text
from "./foo.js" → from "./foo"
from "../bar.js" → from "../bar"
```
Only touch relative paths (starting with `./` or `../`).
Do not touch `node:*`, bare specifiers, or type-only imports (they follow the same rule, so strip those too).
### Step 2: Prevention guard
Add a `lint:imports` npm script that greps for `.js"` in relative imports and fails if any are found:
```jsonc
"lint:imports": "! grep -rn --include='*.ts' 'from \"\\.\\{1,2\\}/.*\\.js\"' src/ tests/"
```
Wire it into `lint:all` so `npm run check` catches regressions.
### Step 3: Verify
Run `npm run build` (tsc) and `npm run test` (vitest).
Both must pass with zero changes beyond the import paths.
## Module-Level Changes
| File / glob | Change |
| --------------------------- | ----------------------------------------------- |
| `src/**/*.ts` (~20 files) | Strip `.js` from relative import paths |
| `tests/**/*.ts` (~10 files) | Strip `.js` from relative import paths |
| `package.json` | Add `lint:imports` script; append to `lint:all` |
No schema, config, or documentation changes required.
## TDD Order
This change is mechanical with no new logic, so a classic red→green cycle is lightweight:
1. **Green: strip extensions in `src/`** — run `npm run build` to confirm tsc still passes.
Commit: `refactor: drop .js extensions from src/ imports (#32)`
2. **Green: strip extensions in `tests/`** — run `npx vitest run` to confirm tests still pass.
Commit: `refactor: drop .js extensions from tests/ imports (#32)`
3. **Guard: add lint:imports script** — add the grep guard and verify it passes on the cleaned tree and fails if a `.js` extension is reintroduced.
Commit: `chore: add lint:imports guard against .js extensions (#32)`
Alternatively, steps 12 can be a single commit since the change is atomic and trivially reversible.
## Risks and Mitigations
| Risk | Mitigation |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Missed an import that actually needs `.js` (e.g. a non-TS asset) | Only strip from `*.ts` files with relative paths; `npm run build` and `npm run test` catch any resolution failures immediately. |
| Could this silently weaken a permission? | No — this change touches only import specifier strings, not runtime behavior or policy resolution. |
| `lint:imports` grep is too broad / too narrow | The pattern targets `from ".<relative>.js"` only; tested against the cleaned tree and a synthetic reintroduction before committing. |
## Open Questions
None — the issue's proposed change is unambiguous and the project's toolchain already supports extensionless imports.