diff --git a/codemods/static-mime/README.md b/codemods/static-mime/README.md new file mode 100644 index 0000000..b410602 --- /dev/null +++ b/codemods/static-mime/README.md @@ -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) diff --git a/codemods/static-mime/codemod.yaml b/codemods/static-mime/codemod.yaml new file mode 100644 index 0000000..6a7a57e --- /dev/null +++ b/codemods/static-mime/codemod.yaml @@ -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 diff --git a/codemods/static-mime/package-json/tests/expected/adds.json b/codemods/static-mime/package-json/tests/expected/adds.json new file mode 100644 index 0000000..d7f76ea --- /dev/null +++ b/codemods/static-mime/package-json/tests/expected/adds.json @@ -0,0 +1,7 @@ +{ + "name": "my-app", + "dependencies": { + "express": "^5.0.0", + "mime-types": "^3.0.0" + } +} diff --git a/codemods/static-mime/package-json/tests/expected/already-present.json b/codemods/static-mime/package-json/tests/expected/already-present.json new file mode 100644 index 0000000..ab1ff55 --- /dev/null +++ b/codemods/static-mime/package-json/tests/expected/already-present.json @@ -0,0 +1,7 @@ +{ + "name": "already-has-it", + "dependencies": { + "express": "^5.0.0", + "mime-types": "^2.1.35" + } +} diff --git a/codemods/static-mime/package-json/tests/expected/dev-dependency.json b/codemods/static-mime/package-json/tests/expected/dev-dependency.json new file mode 100644 index 0000000..8ed8088 --- /dev/null +++ b/codemods/static-mime/package-json/tests/expected/dev-dependency.json @@ -0,0 +1,8 @@ +{ + "name": "express-plugin", + "devDependencies": { + "express": "^5.0.0", + "typescript": "^5.7.2", + "mime-types": "^3.0.0" + } +} diff --git a/codemods/static-mime/package-json/tests/expected/no-express.json b/codemods/static-mime/package-json/tests/expected/no-express.json new file mode 100644 index 0000000..09b18b5 --- /dev/null +++ b/codemods/static-mime/package-json/tests/expected/no-express.json @@ -0,0 +1,6 @@ +{ + "name": "not-express", + "dependencies": { + "koa": "^2.15.3" + } +} diff --git a/codemods/static-mime/package-json/tests/input/adds.json b/codemods/static-mime/package-json/tests/input/adds.json new file mode 100644 index 0000000..7343ffd --- /dev/null +++ b/codemods/static-mime/package-json/tests/input/adds.json @@ -0,0 +1,6 @@ +{ + "name": "my-app", + "dependencies": { + "express": "^5.0.0" + } +} diff --git a/codemods/static-mime/package-json/tests/input/already-present.json b/codemods/static-mime/package-json/tests/input/already-present.json new file mode 100644 index 0000000..ab1ff55 --- /dev/null +++ b/codemods/static-mime/package-json/tests/input/already-present.json @@ -0,0 +1,7 @@ +{ + "name": "already-has-it", + "dependencies": { + "express": "^5.0.0", + "mime-types": "^2.1.35" + } +} diff --git a/codemods/static-mime/package-json/tests/input/dev-dependency.json b/codemods/static-mime/package-json/tests/input/dev-dependency.json new file mode 100644 index 0000000..df597cb --- /dev/null +++ b/codemods/static-mime/package-json/tests/input/dev-dependency.json @@ -0,0 +1,7 @@ +{ + "name": "express-plugin", + "devDependencies": { + "express": "^5.0.0", + "typescript": "^5.7.2" + } +} diff --git a/codemods/static-mime/package-json/tests/input/no-express.json b/codemods/static-mime/package-json/tests/input/no-express.json new file mode 100644 index 0000000..09b18b5 --- /dev/null +++ b/codemods/static-mime/package-json/tests/input/no-express.json @@ -0,0 +1,6 @@ +{ + "name": "not-express", + "dependencies": { + "koa": "^2.15.3" + } +} diff --git a/codemods/static-mime/package.json b/codemods/static-mime/package.json new file mode 100644 index 0000000..b27f764 --- /dev/null +++ b/codemods/static-mime/package.json @@ -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" + } +} diff --git a/codemods/static-mime/src/package-json.ts b/codemods/static-mime/src/package-json.ts new file mode 100644 index 0000000..9500aeb --- /dev/null +++ b/codemods/static-mime/src/package-json.ts @@ -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 { + 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): Promise { + 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 diff --git a/codemods/static-mime/src/workflow.ts b/codemods/static-mime/src/workflow.ts new file mode 100644 index 0000000..1082742 --- /dev/null +++ b/codemods/static-mime/src/workflow.ts @@ -0,0 +1,297 @@ +import type Js from '@codemod.com/jssg-types/src/langs/javascript' +import type { Edit, SgNode, SgRoot } from '@codemod.com/jssg-types/src/main' + +const MIME_TYPES_MODULE = 'mime-types' +// Candidate local names tried in order when introducing a new `mime-types` +// binding, skipping any that are already declared in the file. +const NAME_CANDIDATES = ['mime', 'mimeTypes', 'mimeTypesLib'] + +// Migrates `express.static.mime` (removed in Express 5) to the `mime-types` +// package, e.g. `express.static.mime.lookup('json')` -> `mime.lookup('json')` +// plus an added `import mime from 'mime-types'` / `const mime = require('mime-types')`. +async function transform(root: SgRoot): Promise { + const rootNode = root.root() + + const nodes = rootNode.findAll({ + rule: { pattern: '$EXPRESS.static.mime' }, + }) + + if (!nodes.length) return null + + // Only the occurrences whose `express` actually resolves to the express module. + const matches: { node: SgNode; express: SgNode }[] = [] + for (const node of nodes) { + const express = node.getMatch('EXPRESS') + if (!express) continue + if (!isExpressBinding(express)) continue + matches.push({ node, express }) + } + + if (!matches.length) return null + + // Reuse an existing `mime-types` import if the file already has one; + // otherwise pick a non-colliding local name and remember to add the import. + const existingName = findExistingMimeTypesName(rootNode) + const localName = existingName ?? pickLocalName(rootNode) + + const edits: Edit[] = [] + for (const { node } of matches) { + const { node: target, replacement } = classifyUsage(node, localName) + edits.push(target.replace(replacement)) + } + + if (!existingName) { + const importEdit = buildImportEdit(rootNode, matches[0].express, localName) + if (importEdit) edits.push(importEdit) + } + + return rootNode.commitEdits(edits) +} + +interface Usage { + // The outermost node to rewrite for this `express.static.mime` occurrence. + node: SgNode + replacement: string +} + +// The mime@1.x instance Express 4 exposed as `express.static.mime` is not +// API-compatible with `mime-types`. Besides the shared methods (`lookup`, +// `extension`, `types`, `extensions`) handled by the plain rename, two cases +// need extra work: +// - `mime.charsets.lookup(type)` becomes `mime.charset(type)`. +// - `define`, `load`, and `default_type` have no `mime-types` equivalent, so +// they are flagged inline for manual migration instead of silently breaking. +function classifyUsage(mimeNode: SgNode, localName: string): Usage { + const access = mimeNode.parent() + const property = getPropertyName(access) + + if (property === 'charsets') { + // `express.static.mime.charsets.lookup(type)` -> `mime.charset(type)` + const lookupAccess = access?.parent() + if (getPropertyName(lookupAccess) === 'lookup') { + const call = lookupAccess?.parent() + const args = call?.is('call_expression') ? call.field('arguments') : null + if (call && args) { + return { node: call, replacement: `${localName}.charset${args.text()}` } + } + } + + return flag( + access ?? mimeNode, + `${localName}.charset`, + "'mime-types' has no charsets.lookup(); use charset() and migrate manually", + ) + } + + if (property === 'define' || property === 'load') { + const call = access?.parent() + if (call?.is('call_expression')) { + const rewritten = call.text().replace(mimeNode.text(), localName) + return flag(call, rewritten, `'mime-types' has no ${property}(); migrate manually`) + } + } + + if (property === 'default_type' && access) { + return flag(access, `${localName}.default_type`, "'mime-types' has no default_type; migrate manually") + } + + return { node: mimeNode, replacement: localName } +} + +function flag(node: SgNode, code: string, message: string): Usage { + return { node, replacement: `${code} /* TODO: ${message} */` } +} + +// Returns the property name of a `member_expression` (e.g. `charsets` for +// `express.static.mime.charsets`), or null for any other node. +function getPropertyName(node: SgNode | null | undefined): string | null { + if (!node?.is('member_expression')) return null + + const property = node.field('property') + return property ? property.text() : null +} + +// Builds the edit that introduces the `mime-types` binding. The statement style +// (ESM `import` vs CommonJS `require`) and insertion point follow how `express` +// itself is brought in; an inline `require("express")` falls back to prepending +// the require at the top of the file. +function buildImportEdit(rootNode: SgNode, express: SgNode, localName: string): Edit | null { + const anchor = resolveImportAnchor(express) + + if (anchor?.kind === 'esm') { + const statement = `import ${localName} from '${MIME_TYPES_MODULE}';` + return anchor.node.replace(`${anchor.node.text()}\n${statement}`) + } + + const statement = `const ${localName} = require('${MIME_TYPES_MODULE}');` + + if (anchor) { + return anchor.node.replace(`${anchor.node.text()}\n${statement}`) + } + + // No statement to anchor to (e.g. inline `require("express").static.mime`): + // prepend the require before the first top-level statement. + const firstStatement = rootNode.children().find((child) => !child.is('comment')) + if (!firstStatement) return null + + return firstStatement.replace(`${statement}\n\n${firstStatement.text()}`) +} + +interface ImportAnchor { + // The statement after which (esm/cjs) the new binding is inserted. + node: SgNode + kind: 'esm' | 'cjs' +} + +function resolveImportAnchor(express: SgNode): ImportAnchor | null { + // Inline `require("express")`: no binding statement to anchor to. + if (express.is('call_expression')) return null + + const definition = express.definition({ resolveExternal: false }) + if (!definition) return null + + const importStatement = findAncestorOrSelf(definition.node, 'import_statement') + if (importStatement) return { node: importStatement, kind: 'esm' } + + const lexical = findAncestorOrSelf(definition.node, 'lexical_declaration') + if (lexical) return { node: lexical, kind: 'cjs' } + + const variable = findAncestorOrSelf(definition.node, 'variable_declaration') + if (variable) return { node: variable, kind: 'cjs' } + + return null +} + +// Returns the local name of an existing `mime-types` import (ESM default, +// namespace, or CommonJS require), or null when the file has none. +function findExistingMimeTypesName(rootNode: SgNode): string | null { + for (const importStatement of rootNode.findAll({ rule: { kind: 'import_statement' } })) { + if (getStringLiteralValue(importStatement.field('source')) !== MIME_TYPES_MODULE) continue + + const clause = importStatement.children().find((child) => child.is('import_clause')) + if (!clause) continue + + const defaultImport = clause.children().find((child) => child.is('identifier')) + if (defaultImport) return defaultImport.text() + + const namespace = clause.children().find((child) => child.is('namespace_import')) + const namespaceName = namespace?.children().find((child) => child.is('identifier')) + if (namespaceName) return namespaceName.text() + } + + for (const declarator of rootNode.findAll({ rule: { kind: 'variable_declarator' } })) { + const value = declarator.field('value') + if (!value?.is('call_expression') || !isRequireCall(value, MIME_TYPES_MODULE)) continue + + const name = declarator.field('name') + if (name?.is('identifier')) return name.text() + } + + return null +} + +function pickLocalName(rootNode: SgNode): string { + const declared = collectDeclaredNames(rootNode) + + for (const candidate of NAME_CANDIDATES) { + if (!declared.has(candidate)) return candidate + } + + // Extremely unlikely fallback: derive a numbered name that is still free. + let suffix = 2 + while (declared.has(`mimeTypes${suffix}`)) suffix++ + return `mimeTypes${suffix}` +} + +// Gathers identifiers introduced by declarations so a freshly added `mime-types` +// binding never shadows or collides with an existing name. +function collectDeclaredNames(rootNode: SgNode): Set { + const names = new Set() + + for (const declarator of rootNode.findAll({ rule: { kind: 'variable_declarator' } })) { + const name = declarator.field('name') + if (name?.is('identifier')) names.add(name.text()) + } + + const declarationKinds = ['function_declaration', 'generator_function_declaration', 'class_declaration'] as const + for (const kind of declarationKinds) { + for (const decl of rootNode.findAll({ rule: { kind } })) { + const name = decl.field('name') + if (name?.is('identifier')) names.add(name.text()) + } + } + + for (const importStatement of rootNode.findAll({ rule: { kind: 'import_statement' } })) { + const clause = importStatement.children().find((child) => child.is('import_clause')) + if (!clause) continue + // Conservatively treat every identifier in the import clause as taken. + for (const identifier of clause.findAll({ rule: { kind: 'identifier' } })) { + names.add(identifier.text()) + } + } + + return names +} + +function isRequireCall(node: SgNode, moduleName: string): boolean { + const callFunction = node.field('function') + if (!callFunction?.is('identifier') || callFunction.text() !== 'require') return false + + const args = node.field('arguments') + if (!args) return false + + const source = args.children().find((child) => child.is('string')) + return getStringLiteralValue(source) === moduleName +} + +function isExpressBinding(binding: SgNode): boolean { + if (binding.is('call_expression')) { + return isRequireCall(binding, 'express') + } + + const definition = binding.definition({ resolveExternal: false }) + if (!definition) return false + + return isExpressDefinition(definition.node) +} + +function isExpressDefinition(node: SgNode): boolean { + const importStatement = findAncestorOrSelf(node, 'import_statement') + if (importStatement) { + return getStringLiteralValue(importStatement.field('source')) === 'express' + } + + const declarator = findAncestorOrSelf(node, 'variable_declarator') + if (declarator) { + const value = declarator.field('value') + return !!value?.is('call_expression') && isRequireCall(value, 'express') + } + + return false +} + +function findAncestorOrSelf(node: SgNode, kind: string): SgNode | null { + let current: SgNode | null = node + + while (current) { + // Assign to a typed boolean so `is()`'s type predicate doesn't narrow + // `current` to `never` on the following line. + const matches: boolean = current.is(kind) + if (matches) return current + + current = current.parent() + } + + return null +} + +function getStringLiteralValue(node: SgNode | null | undefined): string | null { + if (!node || !node.is('string')) return null + + const text = node.text() + if (text.length < 2) return null + + return text.slice(1, -1) +} + +export default transform diff --git a/codemods/static-mime/tests/expected/aliases.ts b/codemods/static-mime/tests/expected/aliases.ts new file mode 100644 index 0000000..82aac43 --- /dev/null +++ b/codemods/static-mime/tests/expected/aliases.ts @@ -0,0 +1,11 @@ +import staticExpress from "express"; +import mime from 'mime-types'; +import * as expressNS from "express"; +import otherLib from "other-lib"; + +mime.lookup('a'); + +mime.charset('text/css'); + +// Not express: must be left untouched. +otherLib.static.mime.lookup('b'); diff --git a/codemods/static-mime/tests/expected/collision-exhausted.ts b/codemods/static-mime/tests/expected/collision-exhausted.ts new file mode 100644 index 0000000..3a04a10 --- /dev/null +++ b/codemods/static-mime/tests/expected/collision-exhausted.ts @@ -0,0 +1,8 @@ +import express from "express"; +import mimeTypes2 from 'mime-types'; + +const mime = 1; +const mimeTypes = 2; +const mimeTypesLib = 3; + +const type = mimeTypes2.lookup('json'); diff --git a/codemods/static-mime/tests/expected/collision-lib.ts b/codemods/static-mime/tests/expected/collision-lib.ts new file mode 100644 index 0000000..a63296c --- /dev/null +++ b/codemods/static-mime/tests/expected/collision-lib.ts @@ -0,0 +1,7 @@ +import express from "express"; +import mimeTypesLib from 'mime-types'; + +const mime = 1; +const mimeTypes = 2; + +const type = mimeTypesLib.lookup('json'); diff --git a/codemods/static-mime/tests/expected/collision.ts b/codemods/static-mime/tests/expected/collision.ts new file mode 100644 index 0000000..8c47d77 --- /dev/null +++ b/codemods/static-mime/tests/expected/collision.ts @@ -0,0 +1,6 @@ +import express from "express"; +import mimeTypes from 'mime-types'; + +const mime = mimeTypes; + +mime.lookup('json'); diff --git a/codemods/static-mime/tests/expected/commonjs.ts b/codemods/static-mime/tests/expected/commonjs.ts new file mode 100644 index 0000000..bbf5bab --- /dev/null +++ b/codemods/static-mime/tests/expected/commonjs.ts @@ -0,0 +1,6 @@ +const express = require('express'); +const mime = require('mime-types'); + +console.log(mime.lookup('json')); + +mime.lookup('index.html'); diff --git a/codemods/static-mime/tests/expected/existing-import.ts b/codemods/static-mime/tests/expected/existing-import.ts new file mode 100644 index 0000000..790fe8a --- /dev/null +++ b/codemods/static-mime/tests/expected/existing-import.ts @@ -0,0 +1,6 @@ +import express from "express"; +import mt from "mime-types"; + +mt.lookup('json'); + +mt.contentType('html'); diff --git a/codemods/static-mime/tests/expected/existing-namespace.ts b/codemods/static-mime/tests/expected/existing-namespace.ts new file mode 100644 index 0000000..78e7cc4 --- /dev/null +++ b/codemods/static-mime/tests/expected/existing-namespace.ts @@ -0,0 +1,6 @@ +import express from "express"; +import * as mimeTypes from "mime-types"; + +const jsonType = mimeTypes.lookup('json'); + +mimeTypes.contentType('html'); diff --git a/codemods/static-mime/tests/expected/existing-require.ts b/codemods/static-mime/tests/expected/existing-require.ts new file mode 100644 index 0000000..0d2e42a --- /dev/null +++ b/codemods/static-mime/tests/expected/existing-require.ts @@ -0,0 +1,6 @@ +const express = require("express"); +const mimeTypes = require("mime-types"); + +const jsonType = mimeTypes.lookup('json'); + +mimeTypes.contentType('html'); diff --git a/codemods/static-mime/tests/expected/maps.ts b/codemods/static-mime/tests/expected/maps.ts new file mode 100644 index 0000000..6ce9cd1 --- /dev/null +++ b/codemods/static-mime/tests/expected/maps.ts @@ -0,0 +1,8 @@ +import express from "express"; +import mime from 'mime-types'; + +const types = mime.types; + +const extensions = mime.extensions; + +const jsonType = mime.types['json']; diff --git a/codemods/static-mime/tests/expected/static.ts b/codemods/static-mime/tests/expected/static.ts new file mode 100644 index 0000000..402358c --- /dev/null +++ b/codemods/static-mime/tests/expected/static.ts @@ -0,0 +1,14 @@ +import express from "express"; +import mime from 'mime-types'; + +const jsonType = mime.lookup('json'); + +const charset = mime.charset('text/html'); + +const ext = mime.extension('application/json'); + +mime.define({ 'text/x-custom': ['cstm'] }) /* TODO: 'mime-types' has no define(); migrate manually */; + +mime.load('./custom.types') /* TODO: 'mime-types' has no load(); migrate manually */; + +const fallback = mime.default_type /* TODO: 'mime-types' has no default_type; migrate manually */; diff --git a/codemods/static-mime/tests/input/aliases.ts b/codemods/static-mime/tests/input/aliases.ts new file mode 100644 index 0000000..1471c21 --- /dev/null +++ b/codemods/static-mime/tests/input/aliases.ts @@ -0,0 +1,10 @@ +import staticExpress from "express"; +import * as expressNS from "express"; +import otherLib from "other-lib"; + +staticExpress.static.mime.lookup('a'); + +expressNS.static.mime.charsets.lookup('text/css'); + +// Not express: must be left untouched. +otherLib.static.mime.lookup('b'); diff --git a/codemods/static-mime/tests/input/collision-exhausted.ts b/codemods/static-mime/tests/input/collision-exhausted.ts new file mode 100644 index 0000000..4339bc2 --- /dev/null +++ b/codemods/static-mime/tests/input/collision-exhausted.ts @@ -0,0 +1,7 @@ +import express from "express"; + +const mime = 1; +const mimeTypes = 2; +const mimeTypesLib = 3; + +const type = express.static.mime.lookup('json'); diff --git a/codemods/static-mime/tests/input/collision-lib.ts b/codemods/static-mime/tests/input/collision-lib.ts new file mode 100644 index 0000000..aea56e1 --- /dev/null +++ b/codemods/static-mime/tests/input/collision-lib.ts @@ -0,0 +1,6 @@ +import express from "express"; + +const mime = 1; +const mimeTypes = 2; + +const type = express.static.mime.lookup('json'); diff --git a/codemods/static-mime/tests/input/collision.ts b/codemods/static-mime/tests/input/collision.ts new file mode 100644 index 0000000..7fd9f44 --- /dev/null +++ b/codemods/static-mime/tests/input/collision.ts @@ -0,0 +1,5 @@ +import express from "express"; + +const mime = express.static.mime; + +mime.lookup('json'); diff --git a/codemods/static-mime/tests/input/commonjs.ts b/codemods/static-mime/tests/input/commonjs.ts new file mode 100644 index 0000000..41b7f78 --- /dev/null +++ b/codemods/static-mime/tests/input/commonjs.ts @@ -0,0 +1,5 @@ +const express = require('express'); + +console.log(express.static.mime.lookup('json')); + +require("express").static.mime.lookup('index.html'); diff --git a/codemods/static-mime/tests/input/existing-import.ts b/codemods/static-mime/tests/input/existing-import.ts new file mode 100644 index 0000000..bf77fe4 --- /dev/null +++ b/codemods/static-mime/tests/input/existing-import.ts @@ -0,0 +1,6 @@ +import express from "express"; +import mt from "mime-types"; + +express.static.mime.lookup('json'); + +mt.contentType('html'); diff --git a/codemods/static-mime/tests/input/existing-namespace.ts b/codemods/static-mime/tests/input/existing-namespace.ts new file mode 100644 index 0000000..aaf7a65 --- /dev/null +++ b/codemods/static-mime/tests/input/existing-namespace.ts @@ -0,0 +1,6 @@ +import express from "express"; +import * as mimeTypes from "mime-types"; + +const jsonType = express.static.mime.lookup('json'); + +mimeTypes.contentType('html'); diff --git a/codemods/static-mime/tests/input/existing-require.ts b/codemods/static-mime/tests/input/existing-require.ts new file mode 100644 index 0000000..88cde48 --- /dev/null +++ b/codemods/static-mime/tests/input/existing-require.ts @@ -0,0 +1,6 @@ +const express = require("express"); +const mimeTypes = require("mime-types"); + +const jsonType = express.static.mime.lookup('json'); + +mimeTypes.contentType('html'); diff --git a/codemods/static-mime/tests/input/maps.ts b/codemods/static-mime/tests/input/maps.ts new file mode 100644 index 0000000..1f0edec --- /dev/null +++ b/codemods/static-mime/tests/input/maps.ts @@ -0,0 +1,7 @@ +import express from "express"; + +const types = express.static.mime.types; + +const extensions = express.static.mime.extensions; + +const jsonType = express.static.mime.types['json']; diff --git a/codemods/static-mime/tests/input/static.ts b/codemods/static-mime/tests/input/static.ts new file mode 100644 index 0000000..3d55343 --- /dev/null +++ b/codemods/static-mime/tests/input/static.ts @@ -0,0 +1,13 @@ +import express from "express"; + +const jsonType = express.static.mime.lookup('json'); + +const charset = express.static.mime.charsets.lookup('text/html'); + +const ext = express.static.mime.extension('application/json'); + +express.static.mime.define({ 'text/x-custom': ['cstm'] }); + +express.static.mime.load('./custom.types'); + +const fallback = express.static.mime.default_type; diff --git a/codemods/static-mime/workflow.yaml b/codemods/static-mime/workflow.yaml new file mode 100644 index 0000000..1da46f0 --- /dev/null +++ b/codemods/static-mime/workflow.yaml @@ -0,0 +1,38 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + runtime: + type: direct + steps: + - name: Migrates express.static.mime to the mime-types package + js-ast-grep: + js_file: src/workflow.ts + base_path: . + semantic_analysis: file + include: + - "**/*.cjs" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript + - name: Adds the mime-types dependency to package.json for Express projects + js-ast-grep: + js_file: src/package-json.ts + base_path: . + semantic_analysis: file + include: + - "**/package.json" + exclude: + - "**/node_modules/**" + language: json diff --git a/codemods/v5-migration-recipe/workflow.yaml b/codemods/v5-migration-recipe/workflow.yaml index dbdbd28..6c95ef9 100644 --- a/codemods/v5-migration-recipe/workflow.yaml +++ b/codemods/v5-migration-recipe/workflow.yaml @@ -32,4 +32,7 @@ nodes: source: "@expressjs/route-del-to-delete" - name: Adds explicit dotfiles option to express.static() calls to preserve Express 4 behavior codemod: - source: "@expressjs/static-dotfiles" \ No newline at end of file + source: "@expressjs/static-dotfiles" + - name: Migrates express.static.mime to the mime-types package + codemod: + source: "@expressjs/static-mime" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e6d7b1d..d241be9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,7 +70,15 @@ }, "codemods/static-dotfiles": { "name": "@expressjs/static-dotfiles", - "version": "1.1.0", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@codemod.com/jssg-types": "^1.5.0" + } + }, + "codemods/static-mime": { + "name": "@expressjs/static-mime", + "version": "1.0.0", "license": "MIT", "devDependencies": { "@codemod.com/jssg-types": "^1.5.0" @@ -290,6 +298,10 @@ "resolved": "codemods/static-dotfiles", "link": true }, + "node_modules/@expressjs/static-mime": { + "resolved": "codemods/static-mime", + "link": true + }, "node_modules/@expressjs/status-send-order": { "resolved": "codemods/status-send-order", "link": true