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

5.1 KiB
Raw Blame History

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 .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:

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:

  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.