mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
101 lines
5.1 KiB
Markdown
101 lines
5.1 KiB
Markdown
---
|
||
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 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.
|