Skip to content

Commit

Permalink
feat: handle node modules, improve test coverage, track coverage (#9)
Browse files Browse the repository at this point in the history
* fix: avoid skipping `fileURLToPath` (broke the shit out of fs ops)
* test: extract `spawnPromisified` into build util
* test: add cases for logger
* chore: convert logger to ts
* test: cover exit code for logger
* fix: package.json "engines"
* test: improve types by replacing `string` types with explicit template literals
* fix: ignore namespaced specifiers that don't have a file extension
* chore: restore node compatibility flag to v22
* test(ci): add node 23.x & disable `fail-fast`
* fix: handle node module regardless of whether implementation is found
* fix: logger prefix `correct-ts-extensions` → `correct-ts-specifiers`
* doc: cite `rewriteRelativeImportExtensions` config option
  • Loading branch information
JakobJingleheimer authored Nov 21, 2024
1 parent dd172de commit 0224965
Show file tree
Hide file tree
Showing 42 changed files with 772 additions and 193 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ jobs:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
node-version: [22.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
node-version: [23.x, 22.x]

steps:
- uses: actions/checkout@v3
Expand Down
34 changes: 16 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,46 @@
# Correct TypeScript specifiers
# Correct TypeScript Specifiers

![tests](https://github.com/JakobJingleheimer/correct-ts-specifiers/actions/workflows/ci.yml/badge.svg)

This package transforms import specifiers in source-code from the broken state TypeScript's compiler (`tsc`) requires into proper ones. This is a one-and-done process, and the updated source-code should be committed to your version control (eg git); thereafter, source-code import statements should be authored to be compliant with the ECMAScript (JavaScript) standard.
This package transforms import specifiers in source-code from the broken state TypeScript's compiler (`tsc`) required (prior TypeScript v5.7 RC) into proper ones. This is useful when source-code is processed by standards-compliant software like Node.js. This is a one-and-done process, and the updated source-code should be committed to your version control (ex git); thereafter, source-code import statements should be authored compliant with the ECMAScript (JavaScript) standard.

This is useful when source-code is processed by standards-compliant software like Node.js.
> [!TIP]
> Those using `tsc` to compile will need to enable [`rewriteRelativeImportExtensions`](https://www.typescriptlang.org/tsconfig/#rewriteRelativeImportExtensions); using `tsc` for only type-checking (ex via a lint/test step like `npm run test:types`) needs [`allowImportingTsExtensions`](https://www.typescriptlang.org/tsconfig/#allowImportingTsExtensions) (and some additional compile options—see the cited documentation);
This package does not just blindly find & replace file extensions within specifiers: It confirms that the targeted file of replacement specifier actually exists; in cases where there is ambiguity (such as two files with the same basename in the same location but different relevant file extensions), it logs an error, skips that specifier, and continues processing.
This package does not just blindly find & replace file extensions within specifiers: It confirms that the replacement specifier actually exists; in ambiguous cases (such as two files with the same basename in the same location but different relevant file extensions like `/tmp/foo.js` and `/tmp/foo.ts`), it logs an error, skips that specifier, and continues processing.

This package does not confirm that the targetted module in the replacement contains the exports cited in the import statement. This should not actually ever result in a problem because ambiguous cases are skipped (so if there is a problem, it existed before the migration started). Merely running your source-code after the mirgration completes will confirm all is good (if there are problems, node will error, citing exactly where the problems are).
> [!CAUTION]
> This package does not confirm that imported modules contain the desired export(s). This _shouldn't_ actually ever result in a problem because ambiguous cases are skipped (so if there is a problem, it existed before the migration started). Merely running your source-code after the mirgration completes will confirm all is well (if there are problems, node will error, citing the problems).
> [!TIP]
> Node.js requires the `type` keyword be present on type imports. For own code, this package usually handles that. However, in some cases and for node modules, it does not. Robust tooling already exists that will automatically fix this, such as [`consistent-type-imports` via typescript-lint](https://typescript-eslint.io/rules/consistent-type-imports) and [`use-import-type` via biome](https://biomejs.dev/linter/rules/use-import-type/). If your source code needs that, first run this codemod and then one of those fixers.
## Running

> [!CAUTION]
> This will change your source-code. Commit or stash any unsaved changes before running this package.
> This will change your source-code. Commit any unsaved changes before running this package.
```sh
npx codemod@latest correct-ts-specifiers
```

If you're using `tsconfig`'s `paths`, you will need a loader like [`nodejs-loaders/dev/alias`](https://github.com/JakobJingleheimer/nodejs-loaders?tab=readme-ov-file#alias)
If you're using `tsconfig`'s `paths`, you will need a loader like [`@nodejs-loaders/alias`](https://github.com/JakobJingleheimer/nodejs-loaders/blob/main/packages/alias?tab=readme-ov-file)


```sh
npm i nodejs-loaders
npm i @nodejs-loaders/alias

NODE_OPTIONS="--loader=nodejs-loaders/dev/alias" \
NODE_OPTIONS="--loader=@nodejs-loaders/alias" \
npx codemod@latest correct-ts-specifiers
```

> [!IMPORTANT]
> If you want your source-code to still be processessable with `tsc` (for instance, to run type-checking as a lint or test step), you'll need to set [`allowImportingTsExtensions`](https://www.typescriptlang.org/tsconfig/#allowImportingTsExtensions). You will need to use a different transpiler to convert your source-code to JavaScript (you probably should be doing that anyway).
## Supported cases

* no file extension → `.cts`, `.mts`, `.js`, `.ts`, `.d.cts`, `.d.mts`, or `.d.ts`
* `.cjs``.cts`, `.mjs``.mts`, `.js``.ts`
* `.js``.d.cts`, `.d.mts`, or `.d.ts`
* Package.json subimports
* tsconfig paths (requires a loader)
* [Package.json subimports](https://nodejs.org/api/packages.html#subpath-imports)
* [tsconfig paths](https://www.typescriptlang.org/tsconfig/#paths) (requires a loader)
* Commonjs-like directory specifiers

Before:
Expand All @@ -56,8 +58,6 @@ import { baseUrl } from '#config.js'; // package.json imports

export { Zed } from './zed';

// should be unchanged

export const makeLink = (path: URL) => (new URL(path, baseUrl)).href;

const nil = await import('./nil.js');
Expand All @@ -82,8 +82,6 @@ import { baseUrl } from '#config.js'; // package.json imports

export type { Zed } from './zed.d.ts';

// should be unchanged

export const makeLink = (path: URL) => (new URL(path, baseUrl)).href;

const nil = await import('./nil.ts');
Expand Down
37 changes: 37 additions & 0 deletions build/spawn-promisified.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { spawn } from 'node:child_process';


export function spawnPromisified(...args: Parameters<typeof spawn>) {
let stderr = '';
let stdout = '';

const child = spawn(...args);
child.stderr!.setEncoding('utf8');
child.stderr!.on('data', (data) => { stderr += data; });
child.stdout!.setEncoding('utf8');
child.stdout!.on('data', (data) => { stdout += data; });

return new Promise<{
code: number | null,
signal: NodeJS.Signals | null,
stderr: string,
stdout: string,
}>((resolve, reject) => {
child.on('close', (code, signal) => {
resolve({
code,
signal,
stderr,
stdout,
});
});
child.on('error', (code, signal) => {
reject({
code,
signal,
stderr,
stdout,
});
});
});
}
57 changes: 48 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
"type": "module",
"main": "./src/workflow.ts",
"engines": {
"node": "22"
"node": ">=22"
},
"scripts": {
"start": "node --no-warnings --experimental-import-meta-resolve --experimental-strip-types ./src/workflow.ts",
"test": "node --no-warnings --experimental-import-meta-resolve --experimental-test-module-mocks --experimental-test-snapshots --experimental-strip-types --import './build/snapshots.ts' --test --experimental-test-coverage --test-coverage-exclude='**/*.test.ts' './**/*.test.ts'"
"test": "node --no-warnings --experimental-import-meta-resolve --experimental-test-module-mocks --experimental-test-snapshots --experimental-strip-types --import './build/snapshots.ts' --test --experimental-test-coverage --test-coverage-include='src/**/*' --test-coverage-exclude='**/*.test.ts' './**/*.test.ts'"
},
"repository": {
"type": "git",
Expand All @@ -27,10 +27,13 @@
},
"homepage": "https://github.com/JakobJingleheimer/correct-ts-specifiers#readme",
"devDependencies": {
"@types/lodash.get": "^4.4.9",
"@types/node": "^22.3.0",
"nodejs-loaders": "^1.0.0"
"nodejs-loaders": "^1.0.0",
"type-fest": "^4.26.1"
},
"dependencies": {
"@codemod.com/workflow": "^0.0.28"
"@codemod.com/workflow": "^0.0.28",
"lodash.get": "^4.4.2"
}
}
5 changes: 3 additions & 2 deletions src/fexists.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import {
} from 'node:test';
import { fileURLToPath } from 'node:url';

import type { FSAbsolutePath } from './index.d.ts';


type FSAccess = typeof import('node:fs/promises').access;
type FExists = typeof import('./fexists.ts').fexists;
type ResolveSpecifier = typeof import('./resolve-specifier.ts').resolveSpecifier;
// type MockModuleContext = ReturnType<typeof mock.module>;

const RESOLVED_SPECIFIER_ERR = 'Resolved specifier did not match expected';

Expand Down Expand Up @@ -59,7 +60,7 @@ describe('fexists', () => {

it('should return `true` for a bare specifier', async () => {
const specifier = 'foo';
const parentUrl = fileURLToPath(import.meta.resolve('./fixtures/e2e/test.js'));
const parentUrl = fileURLToPath(import.meta.resolve('./fixtures/e2e/test.js')) as FSAbsolutePath;

assert.equal(await fexists(parentUrl, specifier), true);
assert.equal(
Expand Down
25 changes: 15 additions & 10 deletions src/fexists.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { access, constants } from 'node:fs/promises';

import type { FSAbsolutePath, Specifier } from './index.d.ts';
import type {
FSAbsolutePath,
Specifier,
} from './index.d.ts';
import { resolveSpecifier } from './resolve-specifier.ts';


export function fexists(
parentPath: FSAbsolutePath,
specifier: Specifier,
) {
const resolvedSpecifier = resolveSpecifier(parentPath, specifier);
const resolvedSpecifier = resolveSpecifier(parentPath, specifier) as FSAbsolutePath;

return access(
resolvedSpecifier,
constants.F_OK,
)
.then(
() => true,
() => false,
);
return fexistsResolved(resolvedSpecifier);
};

export const fexistsResolved = (resolvedSpecifier: FSAbsolutePath) => access(
resolvedSpecifier,
constants.F_OK,
)
.then(
() => true,
() => false,
);
11 changes: 10 additions & 1 deletion src/fixtures/e2e/Bird/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import { Avians } from 'animal-features';

export class Bird {
constructor(public name: string) {}
constructor(
public name: string,
public eyes?: {
left?: Avians.EyeColour,
right?: Avians.EyeColour,
},
public feathers?: Avians.FeatherColour | Avians.FeatherColour[],
) {}
}
11 changes: 10 additions & 1 deletion src/fixtures/e2e/Cat.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import { Felines } from 'animal-features';

export class Cat {
constructor(public name: string) {}
constructor(
public name: string,
public eyes?: {
left?: Felines.EyeColour,
right?: Felines.EyeColour,
},
public fur?: Felines.FurColour,
) {}
}
11 changes: 10 additions & 1 deletion src/fixtures/e2e/Dog/index.mts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import { Canines } from 'animal-features';

export class Dog {
constructor(public name: string) {}
constructor(
public name: string,
public eyes?: {
left?: Canines.EyeColour,
right?: Canines.EyeColour,
},
public fur?: Canines.FurColour,
) {}
}
Loading

0 comments on commit 0224965

Please sign in to comment.