From 3c4ce9aa270fa66ff80df84c2be9200c45984216 Mon Sep 17 00:00:00 2001 From: Vishal Kumar Singh Date: Sat, 16 May 2026 12:25:02 +0530 Subject: [PATCH 1/4] feat: add express.static dotfiles codemod Add a codemod that explicitly sets dotfiles: 'allow' on express.static() calls to preserve Express 4 behavior where dotfiles were served by default. In Express 5, the dotfiles option defaults to 'ignore', which can break functionality that depends on serving dot-directories like .well-known (used by Android App Links and Apple Universal Links). Closes expressjs/codemod#116 --- codemods/static-dotfiles/README.md | 51 ++++++++++++++++++ codemods/static-dotfiles/codemod.yaml | 26 +++++++++ codemods/static-dotfiles/package.json | 22 ++++++++ codemods/static-dotfiles/src/workflow.ts | 53 +++++++++++++++++++ .../static-dotfiles/tests/expected/static.ts | 22 ++++++++ .../static-dotfiles/tests/input/static.ts | 22 ++++++++ codemods/static-dotfiles/workflow.yaml | 28 ++++++++++ codemods/v5-migration-recipe/workflow.yaml | 5 +- 8 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 codemods/static-dotfiles/README.md create mode 100644 codemods/static-dotfiles/codemod.yaml create mode 100644 codemods/static-dotfiles/package.json create mode 100644 codemods/static-dotfiles/src/workflow.ts create mode 100644 codemods/static-dotfiles/tests/expected/static.ts create mode 100644 codemods/static-dotfiles/tests/input/static.ts create mode 100644 codemods/static-dotfiles/workflow.yaml 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 From a51feb28c99d33893917535980dc5dae78f2ebee Mon Sep 17 00:00:00 2001 From: Vishal Kumar Singh Date: Mon, 25 May 2026 22:40:54 +0530 Subject: [PATCH 2/4] feat(static-dotfiles): resolve express aliases Detect default, namespace, and CommonJS express bindings before rewriting static() calls. Add a multiline fixture to cover indentation-sensitive options objects. --- codemods/static-dotfiles/src/workflow.ts | 73 ++++++++++++++----- .../static-dotfiles/tests/expected/aliases.ts | 24 ++++++ .../static-dotfiles/tests/input/aliases.ts | 23 ++++++ package-lock.json | 12 +++ 4 files changed, 115 insertions(+), 17 deletions(-) create mode 100644 codemods/static-dotfiles/tests/expected/aliases.ts create mode 100644 codemods/static-dotfiles/tests/input/aliases.ts diff --git a/codemods/static-dotfiles/src/workflow.ts b/codemods/static-dotfiles/src/workflow.ts index 04dae74..ae940a0 100644 --- a/codemods/static-dotfiles/src/workflow.ts +++ b/codemods/static-dotfiles/src/workflow.ts @@ -1,15 +1,15 @@ import type Js from '@codemod.com/jssg-types/src/langs/javascript' import type { Edit, SgRoot } from '@codemod.com/jssg-types/src/main' +const DOTFILES_OPTION = "dotfiles: 'allow' /* Express 5: preserve v4 behavior */" + async function transform(root: SgRoot): Promise { const rootNode = root.root() + const expressBindings = collectExpressBindings(rootNode.text()) const nodes = rootNode.findAll({ rule: { - any: [ - { pattern: 'express.static($PATH)' }, - { pattern: 'express.static($PATH, $OPTS)' }, - ], + any: [{ pattern: '$CALL.static($PATH)' }, { pattern: '$CALL.static($PATH, $OPTS)' }], }, }) @@ -18,10 +18,14 @@ async function transform(root: SgRoot): Promise { const edits: Edit[] = [] for (const call of nodes) { + const target = call.getMatch('CALL') const pathArg = call.getMatch('PATH') const optsArg = call.getMatch('OPTS') - if (!pathArg) continue + if (!target || !pathArg) continue + + const targetText = target.text() + if (!isExpressBinding(targetText, expressBindings)) continue if (optsArg) { const optsText = optsArg.text() @@ -29,19 +33,10 @@ async function transform(root: SgRoot): Promise { 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})`)) - } + const newOpts = addDotfilesOption(optsText) + edits.push(call.replace(call.text().replace(optsText, newOpts))) } else { - edits.push( - call.replace( - `express.static(${pathArg.text()}, { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })` - ) - ) + edits.push(call.replace(`${targetText}.static(${pathArg.text()}, { ${DOTFILES_OPTION} })`)) } } @@ -50,4 +45,48 @@ async function transform(root: SgRoot): Promise { return rootNode.commitEdits(edits) } +function collectExpressBindings(source: string): Set { + const bindings = new Set(['express']) + + const defaultImportPattern = /import\s+([A-Za-z_$][\w$]*)(?:\s*,[\s\S]*?)?\s+from\s+['"]express['"]/g + const namespaceImportPattern = /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]express['"]/g + const defaultAsImportPattern = /import\s+\{\s*default\s+as\s+([A-Za-z_$][\w$]*)[\s\S]*?\}\s+from\s+['"]express['"]/g + const requirePattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\s*\(\s*['"]express['"]\s*\)/g + + const patterns = [defaultImportPattern, namespaceImportPattern, defaultAsImportPattern, requirePattern] + + for (const pattern of patterns) { + let match = pattern.exec(source) + while (match !== null) { + bindings.add(match[1]) + match = pattern.exec(source) + } + } + + return bindings +} + +function addDotfilesOption(optsText: string): string { + const trimmed = optsText.trimEnd() + + if (!trimmed.includes('\n')) { + const inner = trimmed.slice(1, -1).trim() + + return inner ? `{ ${inner}, ${DOTFILES_OPTION} }` : `{ ${DOTFILES_OPTION} }` + } + + const closingBraceIndex = trimmed.lastIndexOf('}') + const body = trimmed.slice(0, closingBraceIndex).replace(/\s*$/, '') + const closingIndentMatch = trimmed.match(/\n([ \t]*)\}$/) + const closingIndent = closingIndentMatch?.[1] ?? '' + const propertyIndentMatch = body.match(/\n([ \t]*)[^\n]*$/) + const propertyIndent = propertyIndentMatch?.[1] ?? ' ' + + return `${body}\n${propertyIndent}${DOTFILES_OPTION}\n${closingIndent}}` +} + +function isExpressBinding(binding: string, expressBindings: Set): boolean { + return expressBindings.has(binding) || /^require\(\s*['"]express['"]\s*\)$/.test(binding) +} + export default transform diff --git a/codemods/static-dotfiles/tests/expected/aliases.ts b/codemods/static-dotfiles/tests/expected/aliases.ts new file mode 100644 index 0000000..7b52c60 --- /dev/null +++ b/codemods/static-dotfiles/tests/expected/aliases.ts @@ -0,0 +1,24 @@ +import staticExpress from "express"; +import * as expressNS from "express"; + +const expressRequire = require("express"); + +const app = { + use() {}, +}; + +app.use(staticExpress.static('aliased', { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); + +app.use( + staticExpress.static( + 'multi-line', + { + maxAge: '1d', + dotfiles: 'allow' /* Express 5: preserve v4 behavior */ + } + ) +); + +app.use(expressNS.static('namespace', { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); + +app.use(expressRequire.static('commonjs', { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); diff --git a/codemods/static-dotfiles/tests/input/aliases.ts b/codemods/static-dotfiles/tests/input/aliases.ts new file mode 100644 index 0000000..bc92258 --- /dev/null +++ b/codemods/static-dotfiles/tests/input/aliases.ts @@ -0,0 +1,23 @@ +import staticExpress from "express"; +import * as expressNS from "express"; + +const expressRequire = require("express"); + +const app = { + use() {}, +}; + +app.use(staticExpress.static('aliased')); + +app.use( + staticExpress.static( + 'multi-line', + { + maxAge: '1d', + } + ) +); + +app.use(expressNS.static('namespace')); + +app.use(expressRequire.static('commonjs')); diff --git a/package-lock.json b/package-lock.json index 2d9f155..4f0b8c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,6 +68,14 @@ "@codemod.com/jssg-types": "^1.5.0" } }, + "codemods/static-dotfiles": { + "name": "@expressjs/static-dotfiles", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@codemod.com/jssg-types": "^1.5.0" + } + }, "codemods/status-send-order": { "name": "@expressjs/status-send-order", "version": "1.0.0", @@ -278,6 +286,10 @@ "resolved": "codemods/route-del-to-delete", "link": true }, + "node_modules/@expressjs/static-dotfiles": { + "resolved": "codemods/static-dotfiles", + "link": true + }, "node_modules/@expressjs/status-send-order": { "resolved": "codemods/status-send-order", "link": true From 559f3d9d959f2efe6ae5dd6c422f8f14a4e5e276 Mon Sep 17 00:00:00 2001 From: Vishal Kumar Singh Date: Mon, 25 May 2026 22:41:47 +0530 Subject: [PATCH 3/4] chore(static-dotfiles): bump codemod version to 1.1.0 Match the reviewer request to bump the codemod minor version alongside the alias-handling fix. --- codemods/static-dotfiles/package.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codemods/static-dotfiles/package.json b/codemods/static-dotfiles/package.json index acd941c..395de9f 100644 --- a/codemods/static-dotfiles/package.json +++ b/codemods/static-dotfiles/package.json @@ -1,7 +1,7 @@ { "name": "@expressjs/static-dotfiles", "private": true, - "version": "1.0.0", + "version": "1.1.0", "description": "Adds explicit dotfiles option to express.static() calls to preserve Express 4 behavior where dotfiles were served by default", "type": "module", "scripts": { diff --git a/package-lock.json b/package-lock.json index 4f0b8c4..e6d7b1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,7 +70,7 @@ }, "codemods/static-dotfiles": { "name": "@expressjs/static-dotfiles", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "devDependencies": { "@codemod.com/jssg-types": "^1.5.0" From a1a6b47c23b58688ee716be1f57aae34e053d1ab Mon Sep 17 00:00:00 2001 From: Vishal Kumar Singh Date: Mon, 25 May 2026 23:03:17 +0530 Subject: [PATCH 4/4] fix(static-dotfiles): resolve express aliases without regex --- codemods/static-dotfiles/src/workflow.ts | 113 +++++++++++++----- .../static-dotfiles/tests/expected/aliases.ts | 2 + .../static-dotfiles/tests/input/aliases.ts | 2 + 3 files changed, 88 insertions(+), 29 deletions(-) diff --git a/codemods/static-dotfiles/src/workflow.ts b/codemods/static-dotfiles/src/workflow.ts index ae940a0..33eb9df 100644 --- a/codemods/static-dotfiles/src/workflow.ts +++ b/codemods/static-dotfiles/src/workflow.ts @@ -1,11 +1,10 @@ import type Js from '@codemod.com/jssg-types/src/langs/javascript' -import type { Edit, SgRoot } from '@codemod.com/jssg-types/src/main' +import type { Edit, SgNode, SgRoot } from '@codemod.com/jssg-types/src/main' const DOTFILES_OPTION = "dotfiles: 'allow' /* Express 5: preserve v4 behavior */" async function transform(root: SgRoot): Promise { const rootNode = root.root() - const expressBindings = collectExpressBindings(rootNode.text()) const nodes = rootNode.findAll({ rule: { @@ -24,8 +23,7 @@ async function transform(root: SgRoot): Promise { if (!target || !pathArg) continue - const targetText = target.text() - if (!isExpressBinding(targetText, expressBindings)) continue + if (!isExpressBinding(target)) continue if (optsArg) { const optsText = optsArg.text() @@ -36,7 +34,7 @@ async function transform(root: SgRoot): Promise { const newOpts = addDotfilesOption(optsText) edits.push(call.replace(call.text().replace(optsText, newOpts))) } else { - edits.push(call.replace(`${targetText}.static(${pathArg.text()}, { ${DOTFILES_OPTION} })`)) + edits.push(call.replace(`${target.text()}.static(${pathArg.text()}, { ${DOTFILES_OPTION} })`)) } } @@ -45,25 +43,13 @@ async function transform(root: SgRoot): Promise { return rootNode.commitEdits(edits) } -function collectExpressBindings(source: string): Set { - const bindings = new Set(['express']) +function getStringLiteralValue(node: SgNode | null | undefined): string | null { + if (!node || !node.is('string')) return null - const defaultImportPattern = /import\s+([A-Za-z_$][\w$]*)(?:\s*,[\s\S]*?)?\s+from\s+['"]express['"]/g - const namespaceImportPattern = /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]express['"]/g - const defaultAsImportPattern = /import\s+\{\s*default\s+as\s+([A-Za-z_$][\w$]*)[\s\S]*?\}\s+from\s+['"]express['"]/g - const requirePattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\s*\(\s*['"]express['"]\s*\)/g + const text = node.text() + if (text.length < 2) return null - const patterns = [defaultImportPattern, namespaceImportPattern, defaultAsImportPattern, requirePattern] - - for (const pattern of patterns) { - let match = pattern.exec(source) - while (match !== null) { - bindings.add(match[1]) - match = pattern.exec(source) - } - } - - return bindings + return text.slice(1, -1) } function addDotfilesOption(optsText: string): string { @@ -76,17 +62,86 @@ function addDotfilesOption(optsText: string): string { } const closingBraceIndex = trimmed.lastIndexOf('}') - const body = trimmed.slice(0, closingBraceIndex).replace(/\s*$/, '') - const closingIndentMatch = trimmed.match(/\n([ \t]*)\}$/) - const closingIndent = closingIndentMatch?.[1] ?? '' - const propertyIndentMatch = body.match(/\n([ \t]*)[^\n]*$/) - const propertyIndent = propertyIndentMatch?.[1] ?? ' ' + const body = trimmed.slice(0, closingBraceIndex).trimEnd() + const closingIndent = getIndentAfterLastNewline(trimmed) + const propertyIndent = getIndentAfterLastNewline(body) || ' ' return `${body}\n${propertyIndent}${DOTFILES_OPTION}\n${closingIndent}}` } -function isExpressBinding(binding: string, expressBindings: Set): boolean { - return expressBindings.has(binding) || /^require\(\s*['"]express['"]\s*\)$/.test(binding) +function isExpressBinding(binding: SgNode): boolean { + if (binding.is('call_expression')) { + return isExpressRequireCall(binding) + } + + 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 isExpressImport(importStatement) + } + + const declarator = findAncestorOrSelf(node, 'variable_declarator') + if (declarator) { + return isExpressRequireDeclarator(declarator) + } + + return false +} + +function isExpressImport(importStatement: SgNode): boolean { + const source = importStatement.field('source') + return getStringLiteralValue(source) === 'express' +} + +function isExpressRequireDeclarator(declarator: SgNode): boolean { + if (!declarator.is('variable_declarator')) return false + + const value = declarator.field('value') + if (!value?.is('call_expression')) return false + + return isExpressRequireCall(value) +} + +function isExpressRequireCall(node: SgNode): 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 expressSource = args.children().find((child) => child.is('string')) + return getStringLiteralValue(expressSource) === 'express' +} + +function findAncestorOrSelf(node: SgNode, kind: string): SgNode | null { + let current: SgNode | null = node + + while (current) { + if (current.is(kind)) return current + current = current.parent() + } + + return null +} + +function getIndentAfterLastNewline(text: string): string { + const newlineIndex = text.lastIndexOf('\n') + if (newlineIndex === -1) return '' + + let indent = '' + for (let index = newlineIndex + 1; index < text.length; index++) { + const char = text[index] + if (char !== ' ' && char !== '\t') break + indent += char + } + + return indent } export default transform diff --git a/codemods/static-dotfiles/tests/expected/aliases.ts b/codemods/static-dotfiles/tests/expected/aliases.ts index 7b52c60..311798f 100644 --- a/codemods/static-dotfiles/tests/expected/aliases.ts +++ b/codemods/static-dotfiles/tests/expected/aliases.ts @@ -22,3 +22,5 @@ app.use( app.use(expressNS.static('namespace', { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); app.use(expressRequire.static('commonjs', { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); + +app.use(require("express").static('direct-require', { dotfiles: 'allow' /* Express 5: preserve v4 behavior */ })); diff --git a/codemods/static-dotfiles/tests/input/aliases.ts b/codemods/static-dotfiles/tests/input/aliases.ts index bc92258..5c920cc 100644 --- a/codemods/static-dotfiles/tests/input/aliases.ts +++ b/codemods/static-dotfiles/tests/input/aliases.ts @@ -21,3 +21,5 @@ app.use( app.use(expressNS.static('namespace')); app.use(expressRequire.static('commonjs')); + +app.use(require("express").static('direct-require'));