Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 0 additions & 19 deletions recipes/correct-ts-specifiers/.codemodrc.json

This file was deleted.

23 changes: 23 additions & 0 deletions recipes/correct-ts-specifiers/codemod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
schema_version: "1.0"
name: "@nodejs/correct-ts-specifiers"
version: "1.0.0"
description: "Replace erroneous 'js' or omitted file extensions of import specifiers in TypeScript files."
author: "Jacob Smith"
license: "MIT"
workflow: "workflow.yaml"
category: "migration"

targets:
languages:
- "javascript"
- "typescript"

keywords:
- "transformation"
- "migration"
- "esm"
- "typescript"

registry:
access: "public"
visibility: "public"
17 changes: 3 additions & 14 deletions recipes/correct-ts-specifiers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,15 @@
"version": "1.0.0",
"description": "Replace erroneous 'js' or omitted file extensions of import specifiers in TypeScript files.",
"type": "module",
"main": "./src/workflow.ts",
"engines": {
"node": ">=22.15.0"
},
"scripts": {
"start": "node --no-warnings --experimental-import-meta-resolve --experimental-strip-types ./src/workflow.ts",
"test-legacy": "node --no-warnings --experimental-import-meta-resolve --experimental-test-module-mocks --experimental-test-snapshots --experimental-strip-types --import='@nodejs/codemod-utils/snapshots' --test --experimental-test-coverage --test-coverage-include='src/**/*' --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='@nodejs/codemod-utils/snapshots' --test --experimental-test-coverage --test-coverage-include='src/**/*' --test-coverage-exclude='**/*.test.ts' './**/*.test.ts'"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nodejs/userland-migrations.git",
"directory": "recipes/correct-ts-specifiers",
"bugs": "https://github.com/nodejs/userland-migrations/issues"
},
"files": [
"README.md",
".codemodrc.json",
"bundle.js"
],
"keywords": [
"codemod",
"esm",
Expand All @@ -31,11 +21,10 @@
"license": "MIT",
"homepage": "https://github.com/nodejs/userland-migrations/tree/main/correct-ts-specifiers#readme",
"dependencies": {
"@codemod.com/workflow": "^0.0.31",
"@nodejs-loaders/alias": "^2.1.2"
"@nodejs/codemod-utils": "*"
},
"devDependencies": {
"@nodejs/codemod-utils": "*",
"@codemod.com/jssg-types": "^1.5.0",
"@types/node": "^25.5.0"
}
}
80 changes: 60 additions & 20 deletions recipes/correct-ts-specifiers/src/fexists.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { fileURLToPath } from 'node:url';

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

type FSAccess = typeof import('node:fs/promises').access;
type FSAccess = typeof import('fs').promises.access;
type FExists = typeof import('./fexists.ts').fexists;
type ResolveSpecifier = typeof import('./resolve-specifier.ts').resolveSpecifier;
type ResolveSpecifier =
typeof import('./resolve-specifier.ts').resolveSpecifier;
Comment on lines +9 to +10
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you want

Suggested change
type ResolveSpecifier =
typeof import('./resolve-specifier.ts').resolveSpecifier;
import type { ResolveSpecifier } from './resolve-specifier.ts');

Node ignores type imports, so that doesn't cause the module to be adversely loaded into the ModuleCache.


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

describe('fexists', { concurrency: false /* concurrency clobbers `before`s */ }, () => {
describe('fexists', {
concurrency: false /* concurrency clobbers `before`s */,
}, () => {
const parentPath = '/tmp/test.ts';
const constants = { F_OK: null };

Expand All @@ -20,10 +23,10 @@ describe('fexists', { concurrency: false /* concurrency clobbers `before`s */ },
before(() => {
const access = mock.fn<FSAccess>();
({ mock: mock__access } = access);
mock.module('node:fs/promises', {
mock.module('fs', {
namedExports: {
access,
constants,
promises: { access },
},
});
Comment on lines 23 to 31
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up: I subsequently realised before is actually superfluous (not sure if it actually has any use-case 🤔).

Suggested change
before(() => {
const access = mock.fn<FSAccess>();
({ mock: mock__access } = access);
mock.module('node:fs/promises', {
mock.module('fs', {
namedExports: {
access,
constants,
promises: { access },
},
});
const access = mock.fn<FSAccess>();
({ mock: mock__access } = access);
mock.module('node:fs/promises', {
mock.module('fs', {
namedExports: {
access,
constants,
promises: { access },
},
});


Expand All @@ -34,16 +37,18 @@ describe('fexists', { concurrency: false /* concurrency clobbers `before`s */ },
resolveSpecifier,
},
});
mock__resolveSpecifier.mockImplementation(function MOCK__resolveSpecifier(_pp, specifier) {
return specifier;
});
mock__resolveSpecifier.mockImplementation(
function MOCK__resolveSpecifier(_pp, specifier) {
return specifier;
},
);
});

describe('when the file exists', () => {
let fexists: FExists;

before(async () => {
mock__access.mockImplementation(async function MOCK_access() { });
mock__access.mockImplementation(async function MOCK_access() {});

({ fexists } = await import('./fexists.ts'));
});
Expand All @@ -59,21 +64,33 @@ describe('fexists', { concurrency: false /* concurrency clobbers `before`s */ },
) as FSAbsolutePath;

assert.equal(await fexists(parentUrl, specifier), true);
assert.equal(mock__access.calls[0].arguments[0], specifier, RESOLVED_SPECIFIER_ERR);
assert.equal(
mock__access.calls[0].arguments[0],
specifier,
RESOLVED_SPECIFIER_ERR,
);
});

it('should return `true` for a relative specifier', async () => {
const specifier = 'exists.js';

assert.equal(await fexists(parentPath, specifier), true);
assert.equal(mock__access.calls[0].arguments[0], specifier, RESOLVED_SPECIFIER_ERR);
assert.equal(
mock__access.calls[0].arguments[0],
specifier,
RESOLVED_SPECIFIER_ERR,
);
});

it('should return `true` for specifier with a query parameter', async () => {
const specifier = 'exists.js?v=1';

assert.equal(await fexists(parentPath, specifier), true);
assert.equal(mock__access.calls[0].arguments[0], specifier, RESOLVED_SPECIFIER_ERR);
assert.equal(
mock__access.calls[0].arguments[0],
specifier,
RESOLVED_SPECIFIER_ERR,
);
});

it('should return `true` for an absolute specifier', async () => {
Expand All @@ -86,7 +103,10 @@ describe('fexists', { concurrency: false /* concurrency clobbers `before`s */ },
});

it('should return `true` for a URL', async () => {
assert.equal(await fexists(parentPath, 'file://localhost/foo/exists.js'), true);
assert.equal(
await fexists(parentPath, 'file://localhost/foo/exists.js'),
true,
);
assert.equal(
mock__access.calls[0].arguments[0],
'file://localhost/foo/exists.js',
Expand Down Expand Up @@ -114,34 +134,54 @@ describe('fexists', { concurrency: false /* concurrency clobbers `before`s */ },
const specifier = 'noexists.js';

assert.equal(await fexists(parentPath, specifier), false);
assert.equal(mock__access.calls[0].arguments[0], specifier, RESOLVED_SPECIFIER_ERR);
assert.equal(
mock__access.calls[0].arguments[0],
specifier,
RESOLVED_SPECIFIER_ERR,
);
});

it('should return `false` for a relative specifier', async () => {
const specifier = 'noexists.js?v=1';

assert.equal(await fexists(parentPath, specifier), false);
assert.equal(mock__access.calls[0].arguments[0], specifier, RESOLVED_SPECIFIER_ERR);
assert.equal(
mock__access.calls[0].arguments[0],
specifier,
RESOLVED_SPECIFIER_ERR,
);
});

it('should return `false` for an absolute specifier', async () => {
const specifier = '/tmp/foo/noexists.js';

assert.equal(await fexists(parentPath, specifier), false);
assert.equal(mock__access.calls[0].arguments[0], specifier, RESOLVED_SPECIFIER_ERR);
assert.equal(
mock__access.calls[0].arguments[0],
specifier,
RESOLVED_SPECIFIER_ERR,
);
});

it('should return `false` for a URL specifier', async () => {
const specifier = 'file://localhost/foo/noexists.js';

assert.equal(await fexists(parentPath, specifier), false);
assert.equal(mock__access.calls[0].arguments[0], specifier, RESOLVED_SPECIFIER_ERR);
assert.equal(
mock__access.calls[0].arguments[0],
specifier,
RESOLVED_SPECIFIER_ERR,
);
});

it('should return `false` when the specifier can’t be resolved', async () => {
mock__resolveSpecifier.mockImplementationOnce(function MOCK__resolveSpecifier(_pp, _specifier) {
throw Object.assign(new Error('ERR_MODULE_NOT_FOUND'), { code: 'ERR_MODULE_NOT_FOUND' });
});
mock__resolveSpecifier.mockImplementationOnce(
function MOCK__resolveSpecifier(_pp, _specifier) {
throw Object.assign(new Error('ERR_MODULE_NOT_FOUND'), {
code: 'ERR_MODULE_NOT_FOUND',
});
},
);

assert.equal(await fexists(parentPath, 'noexists'), false);
});
Expand Down
13 changes: 9 additions & 4 deletions recipes/correct-ts-specifiers/src/fexists.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { access, constants } from 'node:fs/promises';
// biome-ignore lint/style/useNodejsImportProtocol: JSSG runtime resolves 'fs' for this codemod.
import { constants, promises as fs } from 'fs';

Check failure on line 2 in recipes/correct-ts-specifiers/src/fexists.ts

View workflow job for this annotation

GitHub Actions / Lint & types

Cannot find name 'fs'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

Check failure on line 2 in recipes/correct-ts-specifiers/src/fexists.ts

View workflow job for this annotation

GitHub Actions / Lint & types

Cannot find name 'fs'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

import type {
FSAbsolutePath,
Expand All @@ -13,17 +14,21 @@
) {
let resolvedSpecifier: FSAbsolutePath;
try {
resolvedSpecifier = resolveSpecifier(parentPath, specifier) as FSAbsolutePath;
resolvedSpecifier = resolveSpecifier(
parentPath,
specifier,
) as FSAbsolutePath;
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ERR_MODULE_NOT_FOUND') throw err;
if ((err as NodeJS.ErrnoException).code !== 'ERR_MODULE_NOT_FOUND')

Check failure on line 22 in recipes/correct-ts-specifiers/src/fexists.ts

View workflow job for this annotation

GitHub Actions / Lint & types

Cannot find namespace 'NodeJS'.

Check failure on line 22 in recipes/correct-ts-specifiers/src/fexists.ts

View workflow job for this annotation

GitHub Actions / Lint & types

Cannot find namespace 'NodeJS'.
throw err;
return false;
}

return fexistsResolved(resolvedSpecifier);
}

export const fexistsResolved = (resolvedSpecifier: FSAbsolutePath) =>
access(resolvedSpecifier, constants.F_OK).then(
fs.access(resolvedSpecifier, constants.F_OK).then(
() => true,
() => false,
);
1 change: 1 addition & 0 deletions recipes/correct-ts-specifiers/src/fixtures/e2e/test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// git restore --source main -- recipes/correct-ts-specifiers/src/fixtures/e2e/test.ts
import { URL } from 'node:url';

import { bar } from '@dep/bar';
Expand Down
7 changes: 5 additions & 2 deletions recipes/correct-ts-specifiers/src/get-not-found-url.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { pathToFileURL } from 'node:url';

Check failure on line 1 in recipes/correct-ts-specifiers/src/get-not-found-url.ts

View workflow job for this annotation

GitHub Actions / Lint & types

Cannot find name 'node:url'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

Check failure on line 1 in recipes/correct-ts-specifiers/src/get-not-found-url.ts

View workflow job for this annotation

GitHub Actions / Lint & types

Cannot find name 'node:url'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

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

export const getNotFoundUrl = (err: NodeJS.ErrnoException & { url?: FSAbsolutePath }) =>
pathToFileURL(err?.url ?? err.message.split("'")[1])?.href as ResolvedSpecifier;
export const getNotFoundUrl = (
err: NodeJS.ErrnoException & { url?: FSAbsolutePath },

Check failure on line 6 in recipes/correct-ts-specifiers/src/get-not-found-url.ts

View workflow job for this annotation

GitHub Actions / Lint & types

Cannot find namespace 'NodeJS'.

Check failure on line 6 in recipes/correct-ts-specifiers/src/get-not-found-url.ts

View workflow job for this annotation

GitHub Actions / Lint & types

Cannot find namespace 'NodeJS'.
) =>
pathToFileURL(err?.url ?? err.message.split("'")[1])
?.href as ResolvedSpecifier;
4 changes: 2 additions & 2 deletions recipes/correct-ts-specifiers/src/is-dir.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict';
import { type Mock, before, describe, it, mock } from 'node:test';

type LStat = typeof import('node:fs/promises').lstat;
type LStat = typeof import('fs').promises.lstat;
type ResolveSpecifier = typeof import('./is-dir.ts').isDir;
type IsDir = typeof import('./resolve-specifier.ts').resolveSpecifier;

Expand All @@ -21,7 +21,7 @@ describe('Is a directory', { concurrency: true }, () => {
mock_lstat = lstat.mock;
mock_resolveSpecifier = resolveSpecifier.mock;

mock.module('node:fs/promises', { namedExports: { lstat } });
mock.module('fs', { namedExports: { promises: { lstat } } });
mock.module('./resolve-specifier.ts', {
namedExports: { resolveSpecifier },
});
Expand Down
13 changes: 9 additions & 4 deletions recipes/correct-ts-specifiers/src/is-dir.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { lstat } from 'node:fs/promises';
// biome-ignore lint/style/useNodejsImportProtocol: JSSG runtime resolves 'fs' for this codemod.
import { promises as fs } from 'fs';

Check failure on line 2 in recipes/correct-ts-specifiers/src/is-dir.ts

View workflow job for this annotation

GitHub Actions / Lint & types

Cannot find name 'fs'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

Check failure on line 2 in recipes/correct-ts-specifiers/src/is-dir.ts

View workflow job for this annotation

GitHub Actions / Lint & types

Cannot find name 'fs'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

import type {
FSAbsolutePath,
Expand All @@ -8,16 +9,20 @@
} from './index.d.ts';
import { resolveSpecifier } from './resolve-specifier.ts';

export async function isDir(parentPath: FSAbsolutePath | ResolvedSpecifier, specifier: Specifier) {
export async function isDir(
parentPath: FSAbsolutePath | ResolvedSpecifier,
specifier: Specifier,
) {
let resolvedSpecifier: ResolvedSpecifier | NodeModSpecifier;
try {
resolvedSpecifier = resolveSpecifier(parentPath, specifier);
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ERR_MODULE_NOT_FOUND') return null;
if ((err as NodeJS.ErrnoException).code === 'ERR_MODULE_NOT_FOUND')

Check failure on line 20 in recipes/correct-ts-specifiers/src/is-dir.ts

View workflow job for this annotation

GitHub Actions / Lint & types

Cannot find namespace 'NodeJS'.

Check failure on line 20 in recipes/correct-ts-specifiers/src/is-dir.ts

View workflow job for this annotation

GitHub Actions / Lint & types

Cannot find namespace 'NodeJS'.
return null;
}

try {
const stat = await lstat(resolvedSpecifier!);
const stat = await fs.lstat(resolvedSpecifier!);
return stat.isDirectory();
} catch {
return null;
Expand Down
Loading
Loading