Skip to content

Commit

Permalink
Merge pull request #829 from wheresrhys/rhys/codemod
Browse files Browse the repository at this point in the history
Rhys/codemod
  • Loading branch information
wheresrhys committed Sep 19, 2024
2 parents 513e3c4 + cacb67c commit b5e662f
Show file tree
Hide file tree
Showing 21 changed files with 2,640 additions and 1,188 deletions.
5 changes: 4 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ export default [
ignores: [
'docs/**/*.js',
'packages/*/test/fixtures/*',
'packages/**/__tests__/fixtures/*',
'**/dist/**/*',
'packages/fetch-mock/types/index.test-d.ts',
],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.recommended.map((config) => {
return { ...config, ignores: ['packages/codemods/**'] };
}),
eslintConfigPrettier,
{
rules: {
Expand Down
2,741 changes: 1,559 additions & 1,182 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"test": "vitest . --ui --exclude=packages/jest/src/__tests__/*",
"test:ci": "vitest . --reporter=junit --outputFile=test-results/junit.xml --coverage.provider=istanbul --exclude=packages/jest/src/__tests__/*",
"test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js packages/jest",
"test:browser": "vitest . --browser.enabled --browser.name chromium --browser.provider playwright --exclude=packages/jest/src/__tests__/*",
"test:browser": "vitest . --browser.enabled --browser.name chromium --browser.provider playwright --exclude=packages/{jest,codemods}/src/__tests__/*",
"coverage:send": "cat ./coverage/lcov.info | coveralls",
"compat:module": "npm run compat:module -w import-compat"
},
Expand Down
1 change: 1 addition & 0 deletions packages/codemods/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
try.js
75 changes: 75 additions & 0 deletions packages/codemods/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# @fetch-mock/codemods

A tool for helping upgrade to fetch-mock@12.

## Usage

1. Install `npm i -D @fetch-mock/codemods jscodeshift`;
2. Manually modify any code using `.sandbox()` according to the example above.
3. Run `jscodeshift -t node_modules/@fetch-mock/codemods/wrc/index.jscodeshift .` to run over your entire project, or replace `.` with the directory/file paths you wish to modify. [The jscodeshift CLI has many options](https://jscodeshift.com/run/cli/) - adjust them to suit your project. **Note that the parser option should not be used as @fetch-mock/codemods forces use of the TSX parser in order to ensure compatibility with teh greatest range of projects**.
4. For scenarios where the codemod is unable to detect which variable contains a fetch-mock instance (e.g. when it is required in a global set up file, or when using `jest.mock()`) you may pass in one or more variable names using the `FM_VARIABLES` environment variable e.g. `FM_VARIABLES=fm,fetch`
5. After the codemods have executed, run your tests, correcting all the errors thrown. The notes on what is in/out of scope will help guide you.
6. Once all your tests are fixed you should be able to uninstall the codemods: `npm uninstall @fetch-mock/codemods jscodeshift`

## Features

- Identifies instances of fetch-mock imported using `require` or `import`
- Rewrites `.mock()` to `.route()`
- Rewrites `.reset()`, `.restore()`, `.resetBehavior()` and `.resetHistory()` to their equivalents in fetch-mock@12
- Rewrites `.lastUrl()`, `.lastOptions()` and `lastResponse()` to their equivalents in fetch-mock@12
- Adds an informative error whenever `.lastCall()` or `.calls()` are used with advice on how to manually correct these
- Converts `.getOnce()`, `.getAnyOnce()`, `.postOnce()` etc... - which have been removed - to calls to the underlying `.get()` method with additional options passed in.
- Removes uses of the deprecated options `overwriteRoutes`, `warnOnFallback`, `sendAsJson`
- Removes uses of the deprecated `fallbackToNetwork` option, and adds an informative error with details of how to replace with the `spyGlobal()` method

## Limitations/Out of scope

- Javascript is a language with multiple ways to do the same thing. While these codemods attempt to cover a few different patterns, it's likely that they don't cover all the ways fetch-mock is being used. Please raise an issue if you think they can be improved.
- fetch-mock@12 no longer has the `.mock()` method which combines defining a route _and_ setting up global mocking. All calls to `.mock()` are replaced by `.route()`.
If using global `fetch` you will also need to call `.mockGlobal()` at least once per test suite.
- The `.sandbox()` method previously created a `fetch` function that also had all the fetch-mock methods attached. This is no longer the case, but pulling it apart is complex and deliberately left out of scope for this codemod.
- Any uses of fetch-mock prior to assigning to a variable will not be modified e.g. `require('fetch-mock').mock('a', 'b')` will not be converted to `require('fetch-mock').route('a', 'b')`
- When using a pattern such as `jest.mock('node-fetch', () => require('fetch-mock').sandbox())`, the codemod is unable to identify that `require('node-fetch')` will be an instance of fetch-mock.
- On the server side fetch-mock was previously built around node-fetch's classes, but now uses native `fetch`. In most cases, even if your application code still uses node-fetch, your mocks will still work. However, if you explicitly create instances of `Request` or `Headers` using node-fetch's classes, you may need to provide these to fetch-mock.

Taking the last 4 points together, this example illustrates the kind of manual modifications required:

### Before

```js
jest.mock('node-fetch', () => require('fetch-mock').sandbox());

const nodeFetch = require('node-fetch');

it('runs a test', () => {
nodeFetch.get('http://a.com', 200);
myAPI.call();
expect(nodeFetch.called()).toBe(true);
});
```

### After

```js
const fetchMock = require('fetch-mock');

jest.mock('node-fetch', () => {
const nodeFetch = jest.requireActual('node-fetch');
// only needed if your application makes use of Response, Request
// or Headers classes directly
Object.assign(fetchMock.config, {
fetch: nodeFetch,
Response: nodeFetch.Response,
Request: nodeFetch.Request,
Headers: nodeFetch.Headers,
});
return fetchMock.fetchHandler;
});
const nodeFetch = require('node-fetch');

it('runs a test', () => {
fetchMock.get('http://a.com', 200);
myAPI.call();
expect(fetchMock.called()).toBe(true);
});
```
27 changes: 27 additions & 0 deletions packages/codemods/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@fetch-mock/codemods",
"description": "Codemods for upgrading fetch-mock",
"version": "0.0.0",
"main": "./src/index.js",
"type": "commonjs",
"engines": {
"node": ">=18.11.0"
},
"repository": {
"directory": "packages/codemods",
"type": "git",
"url": "git+https://github.com/wheresrhys/fetch-mock.git"
},
"scripts": {
"build": "echo 'no build'"
},
"license": "MIT",
"author": "Rhys Evans",
"bugs": {
"url": "https://github.com/wheresrhys/fetch-mock/issues"
},
"homepage": "http://www.wheresrhys.co.uk/fetch-mock",
"dependencies": {
"jscodeshift": "^17.0.0"
}
}
4 changes: 4 additions & 0 deletions packages/codemods/src/__tests__/fixtures/extra-vars.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const fetchMock = require('fetch-mock');
fetchMock.mock('blah', 200);
fm1.mock('blah', 200);
fm2.mock('blah', 200);
2 changes: 2 additions & 0 deletions packages/codemods/src/__tests__/fixtures/jsx.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import fetchMock from 'fetch-mock';
fetchMock.mock("blah", <div>Content</div>);
4 changes: 4 additions & 0 deletions packages/codemods/src/__tests__/fixtures/tsx.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import fetchMock from 'fetch-mock';
function helper (res: number): void {
fetchMock.mock("blah", <div>Content</div>);
};
4 changes: 4 additions & 0 deletions packages/codemods/src/__tests__/fixtures/typescript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import fetchMock from 'fetch-mock';
function helper (res: number): void {
fetchMock.mock("blah", res);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, it, expect } from 'vitest';
import { codemod } from '../index';
import jscodeshift from 'jscodeshift';

function expectCodemodResult(src, expected) {
expect(codemod(src, jscodeshift)).toEqual(expected);
}

describe('identifying fetch-mock instances', () => {
it('cjs require() named fetchMock', () => {
expectCodemodResult(
`
const fetchMock = require('fetch-mock');
fetchMock.mock("blah", 200)
`,
`
const fetchMock = require('fetch-mock');
fetchMock.route("blah", 200)
`,
);
});
it('cjs require() named something else', () => {
expectCodemodResult(
`
const fetchNot = require('fetch-mock');
fetchNot.mock("blah", 200)
`,
`
const fetchNot = require('fetch-mock');
fetchNot.route("blah", 200)
`,
);
});
it('esm import named fetchMock', () => {
expectCodemodResult(
`
import fetchMock from 'fetch-mock';
fetchMock.mock("blah", 200)
`,
`
import fetchMock from 'fetch-mock';
fetchMock.route("blah", 200)
`,
);
});
it('esm import named something else', () => {
expectCodemodResult(
`
import fetchNot from 'fetch-mock';
fetchNot.mock("blah", 200)
`,
`
import fetchNot from 'fetch-mock';
fetchNot.route("blah", 200)
`,
);
});
it.skip('unassigned instances of require("fetch-mock")', () => {
expectCodemodResult(
`require('fetch-mock').mock("blah", 200)`,
`require('fetch-mock').route("blah", 200)`,
);
});
it.skip('sandbox() instances', () => {
expectCodemodResult(
`
const fetchMock = require('fetch-mock');
const fm = fetchMock.sandbox();
fm.mock("blah", 200)
`,
`
const fetchMock = require('fetch-mock');
const fm = fetchMock.sandbox();
fm.route("blah", 200)
`,
);
});
it.skip('sandbox() instances used by jest / vitest.mock', () => {});
it.skip('identify multiple instances on a page e.g. fetchMock and fm=fetchMock.sandbox()', () => {});
it.skip('identify when a fm instance with lots of chained methods is assigned to a new variable', () => {
expectCodemodResult(
`
const fetchMock = require('fetch-mock');
const fm = fetchMock.get('a', 'b').get('a', 'b');
fm.mock("blah", 200)
`,
`
const fetchMock = require('fetch-mock');
const fm = fetchMock.get('a', 'b').get('a', 'b');
fm.route("blah", 200)
`,
);
});
});
42 changes: 42 additions & 0 deletions packages/codemods/src/__tests__/integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { it, describe, expect } from 'vitest';
import { promisify } from 'node:util';
import { exec as callbackExec } from 'node:child_process';
const exec = promisify(callbackExec);

describe('integration', () => {
it('can operate on typescript', async () => {
const { stdout } = await exec(
'jscodeshift --parser ts -p -d -t ./packages/codemods/src/index.js ./packages/codemods/src/__tests__/fixtures/typescript.ts',
);
expect(stdout).toContain(`import fetchMock from 'fetch-mock';
function helper (res: number): void {
fetchMock.route("blah", res);
}`);
});
it('can operate on jsx', async () => {
const { stdout } = await exec(
'jscodeshift --parser ts -p -d -t ./packages/codemods/src/index.js ./packages/codemods/src/__tests__/fixtures/jsx.jsx',
);
expect(stdout).toContain(`import fetchMock from 'fetch-mock';
fetchMock.route("blah", <div>Content</div>);`);
});

it('can operate on tsx', async () => {
const { stdout } = await exec(
'jscodeshift --parser ts -p -d -t ./packages/codemods/src/index.js ./packages/codemods/src/__tests__/fixtures/tsx.tsx',
);
expect(stdout).toContain(`import fetchMock from 'fetch-mock';
function helper (res: number): void {
fetchMock.route("blah", <div>Content</div>);
}`);
});
it('allow passing in one or more additional variable names for fetch-mock', async () => {
const { stdout } = await exec(
'FM_VARIABLES=fm1,fm2 jscodeshift --parser ts -p -d -t ./packages/codemods/src/index.js ./packages/codemods/src/__tests__/fixtures/extra-vars.js',
);
expect(stdout).toContain(`const fetchMock = require('fetch-mock');
fetchMock.route('blah', 200);
fm1.route('blah', 200);
fm2.route('blah', 200);`);
});
});
Loading

0 comments on commit b5e662f

Please sign in to comment.