5.1 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 32 | 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
.jsfrom every relative import path insrc/andtests/(~116 occurrences across ~30 files). - Add a guard so
.jsextensions do not creep back in. - Verify
npx vitest runandnpm run buildboth 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.jsonuses"module": "ESNext"and"moduleResolution": "Bundler"— both resolve extensionless relative imports natively.- Vitest transforms via esbuild, which also resolves extensionless imports.
tscis"noEmit": true— Node's strict-ESM extension rule never fires.- Runtime loading: Pi loads extensions via
@mariozechner/jiti(a fork ofunjs/jiti), which uses esbuild internally and resolves extensionless.tsimports the same way Vitest does. Empirically verified: stripping.jsfromsrc/permission-manager.tsimports passes bothnpm run buildandnpx vitest runwith 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:
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:
"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:
- Green: strip extensions in
src/— runnpm run buildto confirm tsc still passes. Commit:refactor: drop .js extensions from src/ imports (#32) - Green: strip extensions in
tests/— runnpx vitest runto confirm tests still pass. Commit:refactor: drop .js extensions from tests/ imports (#32) - Guard: add lint:imports script — add the grep guard and verify it passes on the cleaned tree and fails if a
.jsextension is reintroduced. Commit:chore: add lint:imports guard against .js extensions (#32)
Alternatively, steps 1–2 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.