Skip to content
Merged
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
96 changes: 96 additions & 0 deletions codemods/static-mime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Migrate `express.static.mime`

In Express 5, `mime` is no longer an exported property of `express.static`. The
[`mime-types` package](https://github.com/jshttp/mime-types) should be used to
work with MIME type values instead.

This codemod rewrites every `express.static.mime` reference to use a `mime-types`
binding and adds the corresponding import/require to the file:

1. Replaces `express.static.mime` with a `mime-types` local binding (default name `mime`).
2. Adds the import once per file, matching how `express` is imported
(`import mime from 'mime-types'` for ESM, `const mime = require('mime-types')` for CommonJS).
3. Reuses an existing `mime-types` import when the file already has one, and falls
back to a non-colliding name (`mimeTypes`) when `mime` is already taken.

The object Express 4 exposed as `express.static.mime` was a [`mime@1.x`](https://github.com/broofa/mime/tree/v1.6.0)
instance, whose API is **not** identical to `mime-types`. The codemod rewrites
each member accordingly:

| `express.static.mime` (mime@1.x) | `mime-types` | Handled by |
| --- | --- | --- |
| `.lookup(path)` | `.lookup(path)` | rename of the binding |
| `.extension(type)` | `.extension(type)` | rename of the binding |
| `.types` / `.extensions` | `.types` / `.extensions` | rename of the binding |
| `.charsets.lookup(type)` | `.charset(type)` | method rewrite |
| `.define(map)` | _no equivalent_ | flagged with a `TODO` comment |
| `.load(path)` | _no equivalent_ | flagged with a `TODO` comment |
| `.default_type` | _no equivalent_ | flagged with a `TODO` comment |

## Example

```diff
import express from 'express'
+ import mime from 'mime-types'

- const type = express.static.mime.lookup('json')
+ const type = mime.lookup('json')
```

### Renamed method

```diff
- const charset = express.static.mime.charsets.lookup('text/html')
+ const charset = mime.charset('text/html')
```

### Methods without a `mime-types` equivalent

`define`, `load`, and `default_type` cannot be migrated automatically, so the
codemod points them at the new binding and flags them for manual review:

```diff
- express.static.mime.define({ 'text/x-custom': ['cstm'] })
+ mime.define({ 'text/x-custom': ['cstm'] }) /* TODO: 'mime-types' has no define(); migrate manually */
```

### CommonJS

```diff
const express = require('express')
+ const mime = require('mime-types')

- express.static.mime.lookup('json')
+ mime.lookup('json')
```

### `package.json`

For projects that depend on `express`, the codemod also adds `mime-types` to the
same dependency section, so the newly referenced package is declared:

```diff
"dependencies": {
- "express": "^5.0.0"
+ "express": "^5.0.0",
+ "mime-types": "^3.0.0"
}
```

An existing `mime-types` entry is left untouched, and `package.json` files without
`express` are not modified.

## Notes

- Behavior differs slightly even for the shared methods: `mime-types` returns
`false` for unknown input, whereas `mime@1.x` `lookup()` fell back to
`default_type` (`application/octet-stream`). Review code that relied on that
fallback.
- Members flagged with a `TODO` comment (`define`, `load`, `default_type`) have no
`mime-types` equivalent and must be migrated by hand.
- Run `npm install` after the migration so the added `mime-types` dependency is installed.

## References

- [Express 5 Migration Guide - express.static.mime](https://expressjs.com/en/guide/migrating-5#express.static.mime)
- [`mime-types` package](https://github.com/jshttp/mime-types)
28 changes: 28 additions & 0 deletions codemods/static-mime/codemod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
schema_version: "1.0"
name: "@expressjs/static-mime"
version: "1.0.0"
description: Migrates express.static.mime (removed in Express 5) to the mime-types package
author: Sebastian Beltran
license: MIT
workflow: workflow.yaml
repository: "https://github.com/expressjs/codemod/tree/HEAD/codemods/static-mime"
category: migration

targets:
languages:
- json
- javascript
- typescript

keywords:
- transformation
- migration
- express
- static
- mime
- mime-types
- express.static.mime

registry:
access: public
visibility: public
7 changes: 7 additions & 0 deletions codemods/static-mime/package-json/tests/expected/adds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "my-app",
"dependencies": {
"express": "^5.0.0",
"mime-types": "^3.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "already-has-it",
"dependencies": {
"express": "^5.0.0",
"mime-types": "^2.1.35"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "express-plugin",
"devDependencies": {
"express": "^5.0.0",
"typescript": "^5.7.2",
"mime-types": "^3.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "not-express",
"dependencies": {
"koa": "^2.15.3"
}
}
6 changes: 6 additions & 0 deletions codemods/static-mime/package-json/tests/input/adds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "my-app",
"dependencies": {
"express": "^5.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "already-has-it",
"dependencies": {
"express": "^5.0.0",
"mime-types": "^2.1.35"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "express-plugin",
"devDependencies": {
"express": "^5.0.0",
"typescript": "^5.7.2"
}
}
6 changes: 6 additions & 0 deletions codemods/static-mime/package-json/tests/input/no-express.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "not-express",
"dependencies": {
"koa": "^2.15.3"
}
}
22 changes: 22 additions & 0 deletions codemods/static-mime/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@expressjs/static-mime",
"private": true,
"version": "1.0.0",
"description": "Migrates express.static.mime (removed in Express 5) to the mime-types package",
"type": "module",
"scripts": {
"test": "npx codemod jssg test -l typescript ./src/workflow.ts ./ && npx codemod jssg test -l json ./src/package-json.ts ./package-json"
},
"repository": {
"type": "git",
"url": "git+https://github.com/expressjs/codemod.git",
"directory": "codemods/static-mime",
"bugs": "https://github.com/expressjs/codemod/issues"
},
"author": "Sebastian Beltran",
"license": "MIT",
"homepage": "https://github.com/expressjs/codemod/blob/main/codemods/static-mime/README.md",
"devDependencies": {
"@codemod.com/jssg-types": "^1.5.0"
}
}
73 changes: 73 additions & 0 deletions codemods/static-mime/src/package-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type Json from '@codemod.com/jssg-types/src/langs/json'
import type { Edit, SgRoot } from '@codemod.com/jssg-types/src/main'

const MIME_TYPES_PACKAGE = 'mime-types'
// Version that ships with Express 5; `express.static.mime` used to bundle mime@1.
const MIME_TYPES_VERSION = '^3.0.0'
const DEPENDENCY_SECTIONS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] as const

type PackageJson = {
[key: string]: unknown
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function findDependencySection(
packageJson: PackageJson,
packageName: string,
): (typeof DEPENDENCY_SECTIONS)[number] | null {
for (const section of DEPENDENCY_SECTIONS) {
const dependencies = packageJson[section]
if (isRecord(dependencies) && Object.hasOwn(dependencies, packageName)) {
return section
}
}

return null
}

function detectIndent(source: string): string | number {
const match = source.match(/\n([ \t]+)"/)

return match?.[1] ?? 2
}

function detectLineEnding(source: string): string {
return source.includes('\r\n') ? '\r\n' : '\n'
}

// Adds `mime-types` to a project's dependencies after `express.static.mime` is
// migrated away. Only Express projects are touched, the dependency is added to
// the same section as `express`, and an existing `mime-types` entry is left as is.
async function transform(root: SgRoot<Json>): Promise<string | null> {
const rootNode = root.root()
const source = rootNode.text()
let packageJson: PackageJson

try {
packageJson = JSON.parse(source) as PackageJson
} catch {
return null
}

const expressSection = findDependencySection(packageJson, 'express')
if (!expressSection) return null

// Never override an existing mime-types entry (regardless of which section).
if (findDependencySection(packageJson, MIME_TYPES_PACKAGE)) return null

const dependencies = packageJson[expressSection]
if (!isRecord(dependencies)) return null

dependencies[MIME_TYPES_PACKAGE] = MIME_TYPES_VERSION

const lineEnding = detectLineEnding(source)
const nextSource = `${JSON.stringify(packageJson, null, detectIndent(source)).replace(/\n/g, lineEnding)}${source.endsWith('\n') ? lineEnding : ''}`
const edits: Edit[] = [rootNode.replace(nextSource)]

return rootNode.commitEdits(edits)
}

export default transform
Loading
Loading