diff --git a/codemods/static-dotfiles/README.md b/codemods/static-dotfiles/README.md new file mode 100644 index 0000000..1f602ef --- /dev/null +++ b/codemods/static-dotfiles/README.md @@ -0,0 +1,51 @@ +# Migrate `express.static` dotfiles behavior + +In Express 5, the `express.static` middleware's `dotfiles` option now defaults to `"ignore"`. This is a change from Express 4, where dotfiles were served by default. As a result, files inside a directory that starts with a dot (`.`), such as `.well-known`, will no longer be accessible and will return a 404 Not Found error. + +This codemod adds an explicit `dotfiles: 'allow'` option to `express.static()` calls that don't already specify a `dotfiles` option, preserving the Express 4 behavior. + +## Example + +### Before + +```javascript +app.use(express.static('public')) +``` + +### After + +```javascript +app.use(express.static('public', { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })) +``` + +### With existing options + +#### Before + +```javascript +app.use(express.static('public', { maxAge: '1d' })) +``` + +#### After + +```javascript +app.use(express.static('public', { maxAge: '1d', dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })) +``` + +## Security Consideration + +After running this codemod, review each `express.static()` call to determine if serving dotfiles is actually necessary for your application. If you don't need to serve dotfiles, you can: + +1. Remove the `dotfiles: 'allow'` option to use the new Express 5 default (`"ignore"`) +2. Or explicitly set `dotfiles: 'deny'` to return a 403 Forbidden for dotfile requests + +For directories like `.well-known` that need to be served (e.g., for Android App Links or Apple Universal Links), consider serving them explicitly: + +```javascript +app.use('/.well-known', express.static('public/.well-known', { dotfiles: 'allow' })) +app.use(express.static('public')) +``` + +## References + +- [Express 5 Migration Guide - express.static dotfiles](https://expressjs.com/en/guide/migrating-5.html#express.static.dotfiles) diff --git a/codemods/static-dotfiles/codemod.yaml b/codemods/static-dotfiles/codemod.yaml new file mode 100644 index 0000000..8c02d25 --- /dev/null +++ b/codemods/static-dotfiles/codemod.yaml @@ -0,0 +1,26 @@ +schema_version: "1.0" +name: "@expressjs/static-dotfiles" +version: "1.0.0" +description: Adds explicit dotfiles option to express.static() calls to preserve Express 4 behavior where dotfiles were served by default +author: Vishal Kumar Singh +license: MIT +workflow: workflow.yaml +repository: "https://github.com/expressjs/codemod/tree/HEAD/codemods/static-dotfiles" +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - express + - static + - dotfiles + - express.static + +registry: + access: public + visibility: public diff --git a/codemods/static-dotfiles/package.json b/codemods/static-dotfiles/package.json new file mode 100644 index 0000000..acd941c --- /dev/null +++ b/codemods/static-dotfiles/package.json @@ -0,0 +1,22 @@ +{ + "name": "@expressjs/static-dotfiles", + "private": true, + "version": "1.0.0", + "description": "Adds explicit dotfiles option to express.static() calls to preserve Express 4 behavior where dotfiles were served by default", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/expressjs/codemod.git", + "directory": "codemods/static-dotfiles", + "bugs": "https://github.com/expressjs/codemod/issues" + }, + "author": "Vishal Kumar Singh", + "license": "MIT", + "homepage": "https://github.com/expressjs/codemod/blob/main/codemods/static-dotfiles/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.5.0" + } +} diff --git a/codemods/static-dotfiles/src/workflow.ts b/codemods/static-dotfiles/src/workflow.ts new file mode 100644 index 0000000..04dae74 --- /dev/null +++ b/codemods/static-dotfiles/src/workflow.ts @@ -0,0 +1,53 @@ +import type Js from '@codemod.com/jssg-types/src/langs/javascript' +import type { Edit, SgRoot } from '@codemod.com/jssg-types/src/main' + +async function transform(root: SgRoot): Promise { + const rootNode = root.root() + + const nodes = rootNode.findAll({ + rule: { + any: [ + { pattern: 'express.static($PATH)' }, + { pattern: 'express.static($PATH, $OPTS)' }, + ], + }, + }) + + if (!nodes.length) return null + + const edits: Edit[] = [] + + for (const call of nodes) { + const pathArg = call.getMatch('PATH') + const optsArg = call.getMatch('OPTS') + + if (!pathArg) continue + + if (optsArg) { + const optsText = optsArg.text() + if (optsText.includes('dotfiles')) { + continue + } + + if (optsText.startsWith('{') && optsText.endsWith('}')) { + const inner = optsText.slice(1, -1).trim() + const newOpts = inner + ? `{ ${inner}, dotfiles: 'allow' /* Express 5: preserve v4 behavior */ }` + : `{ dotfiles: 'allow' /* Express 5: preserve v4 behavior */ }` + edits.push(call.replace(`express.static(${pathArg.text()}, ${newOpts})`)) + } + } else { + edits.push( + call.replace( + `express.static(${pathArg.text()}, { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })` + ) + ) + } + } + + if (!edits.length) return null + + return rootNode.commitEdits(edits) +} + +export default transform diff --git a/codemods/static-dotfiles/tests/expected/static.ts b/codemods/static-dotfiles/tests/expected/static.ts new file mode 100644 index 0000000..3abfb93 --- /dev/null +++ b/codemods/static-dotfiles/tests/expected/static.ts @@ -0,0 +1,22 @@ +import express from "express"; + +const app = express(); + +app.use(express.static('public', { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); + +app.use(express.static('assets', { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); + +app.use('/files', express.static('uploads', { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); + +app.use(express.static('public', { maxAge: '1d', dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); + +app.use(express.static('public', { index: false, maxAge: 86400000, dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); + +app.use(express.static('public', { dotfiles: 'deny' })); + +app.use(express.static('public', { dotfiles: 'allow', maxAge: '1d' })); + +const staticPath = './static'; +app.use(express.static(staticPath, { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); + +app.use(express.static(__dirname + '/public', { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); diff --git a/codemods/static-dotfiles/tests/input/static.ts b/codemods/static-dotfiles/tests/input/static.ts new file mode 100644 index 0000000..fb64cac --- /dev/null +++ b/codemods/static-dotfiles/tests/input/static.ts @@ -0,0 +1,22 @@ +import express from "express"; + +const app = express(); + +app.use(express.static('public')); + +app.use(express.static('assets')); + +app.use('/files', express.static('uploads')); + +app.use(express.static('public', { maxAge: '1d' })); + +app.use(express.static('public', { index: false, maxAge: 86400000 })); + +app.use(express.static('public', { dotfiles: 'deny' })); + +app.use(express.static('public', { dotfiles: 'allow', maxAge: '1d' })); + +const staticPath = './static'; +app.use(express.static(staticPath)); + +app.use(express.static(__dirname + '/public')); diff --git a/codemods/static-dotfiles/workflow.yaml b/codemods/static-dotfiles/workflow.yaml new file mode 100644 index 0000000..7a2ecd1 --- /dev/null +++ b/codemods/static-dotfiles/workflow.yaml @@ -0,0 +1,28 @@ +# 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: Adds explicit dotfiles option to express.static() calls to preserve Express 4 behavior + 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 diff --git a/codemods/v5-migration-recipe/workflow.yaml b/codemods/v5-migration-recipe/workflow.yaml index 77ae2fd..dbdbd28 100644 --- a/codemods/v5-migration-recipe/workflow.yaml +++ b/codemods/v5-migration-recipe/workflow.yaml @@ -29,4 +29,7 @@ nodes: source: "@expressjs/camelcase-sendfile" - name: Migrates usage of the legacy APIs `app.del()` to `app.delete()` codemod: - source: "@expressjs/route-del-to-delete" \ No newline at end of file + 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