From 483c25d89035cb1848d55d826289dca95cb6c0b4 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Sun, 5 Dec 2021 15:16:36 -0600 Subject: [PATCH] Support ECMAScript modules See [#320][0] and [#340][1]. [0]: https://github.com/helmetjs/helmet/issues/320 [1]: https://github.com/helmetjs/helmet/pull/340 --- .gitignore | 1 + .npmignore | 37 +----- .prettierignore | 1 + .prettierrc.js => .prettierrc-dist.cjs | 17 +-- CHANGELOG.md | 4 + MAINTAINER_README.md | 44 ++++++++ README.md | 10 ++ bin/build-helmet.js | 45 ++++++++ bin/build-middleware-package.js | 52 ++++++--- bin/clean.js | 8 +- bin/helpers.js | 30 +++++ index.ts | 24 +++- jest.config.js | 2 +- middlewares/content-security-policy/index.ts | 4 +- .../package-files.json | 1 - .../cross-origin-embedder-policy/index.ts | 1 - .../cross-origin-opener-policy/index.ts | 1 - .../cross-origin-resource-policy/index.ts | 1 - .../package-files.json | 1 - middlewares/expect-ct/index.ts | 1 - middlewares/expect-ct/package-files.json | 1 - middlewares/origin-agent-cluster/index.ts | 1 - middlewares/referrer-policy/index.ts | 1 - .../referrer-policy/package-files.json | 1 - .../strict-transport-security/index.ts | 1 - .../package-files.json | 1 - middlewares/x-content-type-options/index.ts | 1 - .../x-content-type-options/package-files.json | 1 - middlewares/x-dns-prefetch-control/index.ts | 1 - .../x-dns-prefetch-control/package-files.json | 1 - middlewares/x-download-options/index.ts | 1 - .../x-download-options/package-files.json | 1 - middlewares/x-frame-options/index.ts | 1 - .../x-frame-options/package-files.json | 1 - .../index.ts | 1 - .../package-files.json | 1 - middlewares/x-powered-by/index.ts | 1 - middlewares/x-powered-by/package-files.json | 1 - middlewares/x-xss-protection/index.ts | 1 - .../x-xss-protection/package-files.json | 1 - package-lock.json | 106 ++++++++++++++++++ package.json | 20 +++- test/content-security-policy.test.ts | 4 +- test/helpers.ts | 4 +- test/index.test.ts | 58 +++++----- tsconfig-commonjs.json | 4 + tsconfig-esm.json | 9 ++ tsconfig-types.json | 7 ++ tsconfig.json | 5 +- 49 files changed, 390 insertions(+), 132 deletions(-) rename .prettierrc.js => .prettierrc-dist.cjs (53%) create mode 100644 MAINTAINER_README.md create mode 100755 bin/build-helmet.js create mode 100644 bin/helpers.js delete mode 100644 middlewares/content-security-policy/package-files.json delete mode 100644 middlewares/cross-origin-resource-policy/package-files.json delete mode 100644 middlewares/expect-ct/package-files.json delete mode 100644 middlewares/referrer-policy/package-files.json delete mode 100644 middlewares/strict-transport-security/package-files.json delete mode 100644 middlewares/x-content-type-options/package-files.json delete mode 100644 middlewares/x-dns-prefetch-control/package-files.json delete mode 100644 middlewares/x-download-options/package-files.json delete mode 100644 middlewares/x-frame-options/package-files.json delete mode 100644 middlewares/x-permitted-cross-domain-policies/package-files.json delete mode 100644 middlewares/x-powered-by/package-files.json delete mode 100644 middlewares/x-xss-protection/package-files.json create mode 100644 tsconfig-commonjs.json create mode 100644 tsconfig-esm.json create mode 100644 tsconfig-types.json diff --git a/.gitignore b/.gitignore index fc459af0..62d122a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /node_modules/ /dist/ +/tmp-commonjs-index.ts /coverage/ diff --git a/.npmignore b/.npmignore index ea2ddbd5..5e71306c 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,5 @@ # `package.json` is published to npm, so everything we add is included in the installed tarball. -# Removing the `files` key and replacing it with this `.npmignore` saves about 0.2 kB, which should improve installation performance slightly. +# Removing the `files` key and replacing it with this `.npmignore` saves some bytes, which should improve installation performance slightly. * @@ -8,36 +8,7 @@ !README.md !SECURITY.md -!dist/index.js +!dist/index.cjs !dist/index.d.ts - -!dist/middlewares/content-security-policy/index.d.ts -!dist/middlewares/content-security-policy/index.js -!dist/middlewares/cross-origin-embedder-policy/index.d.ts -!dist/middlewares/cross-origin-embedder-policy/index.js -!dist/middlewares/cross-origin-opener-policy/index.d.ts -!dist/middlewares/cross-origin-opener-policy/index.js -!dist/middlewares/cross-origin-resource-policy/index.d.ts -!dist/middlewares/cross-origin-resource-policy/index.js -!dist/middlewares/expect-ct/index.d.ts -!dist/middlewares/expect-ct/index.js -!dist/middlewares/origin-agent-cluster/index.d.ts -!dist/middlewares/origin-agent-cluster/index.js -!dist/middlewares/referrer-policy/index.d.ts -!dist/middlewares/referrer-policy/index.js -!dist/middlewares/strict-transport-security/index.d.ts -!dist/middlewares/strict-transport-security/index.js -!dist/middlewares/x-content-type-options/index.d.ts -!dist/middlewares/x-content-type-options/index.js -!dist/middlewares/x-dns-prefetch-control/index.d.ts -!dist/middlewares/x-dns-prefetch-control/index.js -!dist/middlewares/x-download-options/index.d.ts -!dist/middlewares/x-download-options/index.js -!dist/middlewares/x-frame-options/index.d.ts -!dist/middlewares/x-frame-options/index.js -!dist/middlewares/x-permitted-cross-domain-policies/index.d.ts -!dist/middlewares/x-permitted-cross-domain-policies/index.js -!dist/middlewares/x-powered-by/index.d.ts -!dist/middlewares/x-powered-by/index.js -!dist/middlewares/x-xss-protection/index.d.ts -!dist/middlewares/x-xss-protection/index.js +!dist/index.js +!dist/middlewares/**/*.d.ts diff --git a/.prettierignore b/.prettierignore index 7053dc17..81114aa0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ +/dist/ /coverage/ diff --git a/.prettierrc.js b/.prettierrc-dist.cjs similarity index 53% rename from .prettierrc.js rename to .prettierrc-dist.cjs index 0ca2b3f0..4551a33b 100644 --- a/.prettierrc.js +++ b/.prettierrc-dist.cjs @@ -2,16 +2,9 @@ module.exports = { // This shaves a few bytes off the built files while still keeping them readable. // When testing on 4f550aab7ccf00a6dfe686d57195268b3ef06b1a, it reduces the tarball size by about 100 bytes. // This should help installation performance slightly. - overrides: [ - { - files: ["dist/**/*.js", "dist/**/*.d.ts"], - options: { - printWidth: 2000, - trailingComma: "none", - useTabs: true, - arrowParens: "avoid", - semi: false, - }, - }, - ], + printWidth: 2000, + trailingComma: "none", + useTabs: true, + arrowParens: "avoid", + semi: false, }; diff --git a/CHANGELOG.md b/CHANGELOG.md index 72ba6055..4625ae04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ### 5.0.0 - ??? +### Added + +- ECMAScript module imports (i.e., `import helmet from "helmet"` and `import { frameguard } from "helmet"`). See [#320](https://github.com/helmetjs/helmet/issues/320) + ### Changed - **Breaking:** `helmet.contentSecurityPolicy`: `useDefaults` option now defaults to `true` diff --git a/MAINTAINER_README.md b/MAINTAINER_README.md new file mode 100644 index 00000000..6822b7fd --- /dev/null +++ b/MAINTAINER_README.md @@ -0,0 +1,44 @@ +# Helmet maintainer readme + +These are notes for any maintainers of Helmet...which is currently just [Evan Hahn](https://evanhahn.com/). These notes may be of interest to future maintainers, contributors, or people making forks. + +## Releasing Helmet + +tl;dr: + +- `npm publish` builds and releases the `helmet` package +- `npm run build-middleware-package -- $MIDDLEWARE_NAME` builds `$MIDDLEWARE_NAME` and puts it in a temporary directory to be published + +Helmet releases have the following goals: + +- Users should be able to import the package with CommonJS. The following code snippet should work as expected: + + ```js + const helmet = require("helmet"); + app.use(helmet()); + app.use(helmet.ieNoOpen()); + ``` + +- Users should be able to import the package with ECMAScript modules. The default export should be a function, and the rest of the functions should be available too. The following snippets should work as expected: + + ```js + import helmet from "helmet"; + app.use(helmet()); + ``` + + ```js + import { ieNoOpen } from "helmet"; + app.use(ieNoOpen()); + ``` + +- TypeScript users should be able to import Helmet and various exported types. + + ```ts + import helmet, { HelmetOptions, ContentSecurityPolicyOptions } from "helmet"; + ``` + +- Some middlewares have their own npm packages, such as `helmet-csp` or `frameguard`. + +- `helmet` should have no production dependencies, including middleware packages, to simplify installation. + +To that end, there are special scripts for building `helmet` and for building middleware packages. diff --git a/README.md b/README.md index 6e24715a..2513ad51 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,16 @@ app.use(helmet()); // ... ``` +You can also use ECMAScript modules if you prefer. + +```js +import helmet from "helmet"; + +const app = express(); + +app.use(helmet()); +``` + ## How it works Helmet is [Connect](https://github.com/senchalabs/connect)-style middleware, which is compatible with frameworks like [Express](https://expressjs.com/). (If you need support for other frameworks or languages, [see this list](https://helmetjs.github.io/see-also/).) diff --git a/bin/build-helmet.js b/bin/build-helmet.js new file mode 100755 index 00000000..cdd9b945 --- /dev/null +++ b/bin/build-helmet.js @@ -0,0 +1,45 @@ +#!/usr/bin/env node +import * as path from "path"; +import { fileURLToPath } from "url"; +import rollupTypescript from "@rollup/plugin-typescript"; +import { writeRollup, withCommonJsFile } from "./helpers.js"; + +const thisPath = fileURLToPath(import.meta.url); +const rootPath = path.join(path.dirname(thisPath), ".."); +const esmSourcePath = path.join(rootPath, "index.ts"); +const esmDistPath = path.join(rootPath, "dist", "index.js"); +const commonJsDistPath = path.join(rootPath, "dist", "index.cjs"); + +const compileEsm = () => + writeRollup( + { + input: esmSourcePath, + plugins: [rollupTypescript({ tsconfig: "./tsconfig-esm.json" })], + }, + { file: esmDistPath } + ); + +const compileCommonjs = () => + withCommonJsFile(esmSourcePath, (commonJsSourcePath) => + writeRollup( + { + input: commonJsSourcePath, + plugins: [rollupTypescript({ tsconfig: "./tsconfig-commonjs.json" })], + }, + { + exports: "default", + file: commonJsDistPath, + format: "cjs", + } + ) + ); + +async function main() { + await compileEsm(); + await compileCommonjs(); +} + +main(process.argv).catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/bin/build-middleware-package.js b/bin/build-middleware-package.js index a37ee3d7..155bb05e 100755 --- a/bin/build-middleware-package.js +++ b/bin/build-middleware-package.js @@ -1,11 +1,17 @@ #!/usr/bin/env node -const path = require("path"); -const fs = require("fs").promises; -const os = require("os"); -const crypto = require("crypto"); +import * as path from "path"; +import { promises as fs } from "fs"; +import * as os from "os"; +import * as crypto from "crypto"; +import { fileURLToPath } from "url"; +import rollupTypescript from "@rollup/plugin-typescript"; +import { writeRollup, withCommonJsFile } from "./helpers.js"; -const PROJECT_ROOT_PATH = path.join(__dirname, ".."); -const getRootFilePath = (filename) => path.join(PROJECT_ROOT_PATH, filename); +const thisPath = fileURLToPath(import.meta.url); +const rootPath = path.join(path.dirname(thisPath), ".."); +const getRootFilePath = (filename) => path.join(rootPath, filename); + +const readJson = async (path) => JSON.parse(await fs.readFile(path)); async function main(argv) { if (argv.length !== 3) { @@ -20,14 +26,12 @@ async function main(argv) { ); const getSourceFilePath = (filename) => - path.join(PROJECT_ROOT_PATH, "middlewares", argv[2], filename); + path.join(rootPath, "middlewares", argv[2], filename); const getDistFilePath = (filename) => - path.join(PROJECT_ROOT_PATH, "dist", "middlewares", argv[2], filename); + path.join(rootPath, "dist", "middlewares", argv[2], filename); const getStagingFilePath = (filename) => path.join(stagingDirectoryPath, filename); - const packageFiles = require(getSourceFilePath("package-files.json")); - const packageJson = { author: "Adam Baldwin (https://evilpacket.net)", contributors: ["Evan Hahn (https://evanhahn.com)"], @@ -44,14 +48,33 @@ async function main(argv) { engines: { node: ">=12.0.0", }, - files: ["CHANGELOG.md", "LICENSE", "README.md", ...packageFiles], + files: ["CHANGELOG.md", "LICENSE", "README.md", "index.js", "index.d.ts"], main: "index.js", typings: "index.d.ts", - ...require(getSourceFilePath("package-overrides.json")), + exports: { + ".": { + require: "./index.js", + types: "./index.d.ts", + }, + }, + ...(await readJson(getSourceFilePath("package-overrides.json"))), }; await fs.mkdir(stagingDirectoryPath, { recursive: true, mode: 0o700 }); await Promise.all([ + withCommonJsFile(getSourceFilePath("index.ts"), (commonJsSourcePath) => + writeRollup( + { + input: commonJsSourcePath, + plugins: [rollupTypescript()], + }, + { + exports: "default", + file: getStagingFilePath("index.js"), + format: "cjs", + } + ) + ), fs.writeFile( getStagingFilePath("package.json"), JSON.stringify(packageJson) @@ -65,8 +88,9 @@ async function main(argv) { getStagingFilePath("CHANGELOG.md") ), fs.copyFile(getRootFilePath("LICENSE"), getStagingFilePath("LICENSE")), - ...packageFiles.map((filename) => - fs.copyFile(getDistFilePath(filename), getStagingFilePath(filename)) + fs.copyFile( + getDistFilePath("index.d.ts"), + getStagingFilePath("index.d.ts") ), ]); diff --git a/bin/clean.js b/bin/clean.js index 69e5659b..f76194fd 100755 --- a/bin/clean.js +++ b/bin/clean.js @@ -1,8 +1,10 @@ #!/usr/bin/env node // This lets us remove files on all platforms. Notably, `rm` is missing on Windows. -const path = require("path"); -const fs = require("fs"); +import * as path from "path"; +import * as fs from "fs"; +import { fileURLToPath } from "url"; -const distPath = path.join(__dirname, "..", "dist"); +const thisPath = fileURLToPath(import.meta.url); +const distPath = path.join(path.dirname(thisPath), "..", "dist"); fs.rmSync(distPath, { recursive: true, force: true }); diff --git a/bin/helpers.js b/bin/helpers.js new file mode 100644 index 00000000..b3e1a3db --- /dev/null +++ b/bin/helpers.js @@ -0,0 +1,30 @@ +import * as path from "path"; +import { promises as fs } from "fs"; +import { rollup } from "rollup"; + +export async function writeRollup(inputOptions, outputOptions) { + const bundle = await rollup(inputOptions); + await bundle.write(outputOptions); + await bundle.close(); +} + +export async function withCommonJsFile(esmSourcePath, fn) { + const commonJsSourcePath = path.join( + path.dirname(esmSourcePath), + "tmp-commonjs-index.ts" + ); + + const lines = (await fs.readFile(esmSourcePath, "utf8")).split(/\r?\n/); + const resultLines = lines.slice( + 0, + lines.findIndex((line) => line.includes("!helmet-end-of-commonjs")) + ); + + try { + await fs.writeFile(commonJsSourcePath, resultLines.join("\n")); + + await fn(commonJsSourcePath); + } finally { + await fs.unlink(commonJsSourcePath); + } +} diff --git a/index.ts b/index.ts index b8de6457..c3833080 100644 --- a/index.ts +++ b/index.ts @@ -31,7 +31,7 @@ import xPermittedCrossDomainPolicies, { import xPoweredBy from "./middlewares/x-powered-by"; import xXssProtection from "./middlewares/x-xss-protection"; -interface HelmetOptions { +export interface HelmetOptions { contentSecurityPolicy?: ContentSecurityPolicyOptions | boolean; crossOriginEmbedderPolicy?: boolean; crossOriginOpenerPolicy?: CrossOriginOpenerPolicyOptions | boolean; @@ -282,4 +282,24 @@ const helmet: Helmet = Object.assign( } ); -export = helmet; +export default helmet; + +// !helmet-end-of-commonjs + +export { + contentSecurityPolicy, + crossOriginEmbedderPolicy, + crossOriginOpenerPolicy, + crossOriginResourcePolicy, + expectCt, + originAgentCluster, + referrerPolicy, + strictTransportSecurity as hsts, + xContentTypeOptions as noSniff, + xDnsPrefetchControl as dnsPrefetchControl, + xDownloadOptions as ieNoOpen, + xFrameOptions as frameguard, + xPermittedCrossDomainPolicies as permittedCrossDomainPolicies, + xPoweredBy as hidePoweredBy, + xXssProtection as xssFilter, +}; diff --git a/jest.config.js b/jest.config.js index 862d056a..1461b85e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,4 @@ -module.exports = { +export default { collectCoverage: true, collectCoverageFrom: ["/index.ts", "/middlewares/**/*.ts"], coverageThreshold: { diff --git a/middlewares/content-security-policy/index.ts b/middlewares/content-security-policy/index.ts index 3b9eab21..6ca60632 100644 --- a/middlewares/content-security-policy/index.ts +++ b/middlewares/content-security-policy/index.ts @@ -242,6 +242,8 @@ contentSecurityPolicy.getDefaultDirectives = getDefaultDirectives; contentSecurityPolicy.dangerouslyDisableDefaultSrc = dangerouslyDisableDefaultSrc; -module.exports = contentSecurityPolicy; export default contentSecurityPolicy; + +// !helmet-end-of-commonjs + export { getDefaultDirectives, dangerouslyDisableDefaultSrc }; diff --git a/middlewares/content-security-policy/package-files.json b/middlewares/content-security-policy/package-files.json deleted file mode 100644 index 962389e9..00000000 --- a/middlewares/content-security-policy/package-files.json +++ /dev/null @@ -1 +0,0 @@ -["index.js", "index.d.ts"] diff --git a/middlewares/cross-origin-embedder-policy/index.ts b/middlewares/cross-origin-embedder-policy/index.ts index 4c87eb5d..cfc4afd5 100644 --- a/middlewares/cross-origin-embedder-policy/index.ts +++ b/middlewares/cross-origin-embedder-policy/index.ts @@ -11,5 +11,4 @@ function crossOriginEmbedderPolicy() { }; } -module.exports = crossOriginEmbedderPolicy; export default crossOriginEmbedderPolicy; diff --git a/middlewares/cross-origin-opener-policy/index.ts b/middlewares/cross-origin-opener-policy/index.ts index 88b971ca..1200ced8 100644 --- a/middlewares/cross-origin-opener-policy/index.ts +++ b/middlewares/cross-origin-opener-policy/index.ts @@ -39,5 +39,4 @@ function crossOriginOpenerPolicy( }; } -module.exports = crossOriginOpenerPolicy; export default crossOriginOpenerPolicy; diff --git a/middlewares/cross-origin-resource-policy/index.ts b/middlewares/cross-origin-resource-policy/index.ts index a00dc8ee..74e69386 100644 --- a/middlewares/cross-origin-resource-policy/index.ts +++ b/middlewares/cross-origin-resource-policy/index.ts @@ -35,5 +35,4 @@ function crossOriginResourcePolicy( }; } -module.exports = crossOriginResourcePolicy; export default crossOriginResourcePolicy; diff --git a/middlewares/cross-origin-resource-policy/package-files.json b/middlewares/cross-origin-resource-policy/package-files.json deleted file mode 100644 index 962389e9..00000000 --- a/middlewares/cross-origin-resource-policy/package-files.json +++ /dev/null @@ -1 +0,0 @@ -["index.js", "index.d.ts"] diff --git a/middlewares/expect-ct/index.ts b/middlewares/expect-ct/index.ts index 2a810f95..017722ae 100644 --- a/middlewares/expect-ct/index.ts +++ b/middlewares/expect-ct/index.ts @@ -45,5 +45,4 @@ function expectCt(options: Readonly = {}) { }; } -module.exports = expectCt; export default expectCt; diff --git a/middlewares/expect-ct/package-files.json b/middlewares/expect-ct/package-files.json deleted file mode 100644 index 962389e9..00000000 --- a/middlewares/expect-ct/package-files.json +++ /dev/null @@ -1 +0,0 @@ -["index.js", "index.d.ts"] diff --git a/middlewares/origin-agent-cluster/index.ts b/middlewares/origin-agent-cluster/index.ts index 00c316f6..cf2a02c1 100644 --- a/middlewares/origin-agent-cluster/index.ts +++ b/middlewares/origin-agent-cluster/index.ts @@ -11,5 +11,4 @@ function originAgentCluster() { }; } -module.exports = originAgentCluster; export default originAgentCluster; diff --git a/middlewares/referrer-policy/index.ts b/middlewares/referrer-policy/index.ts index f59aabf5..0e6e7e38 100644 --- a/middlewares/referrer-policy/index.ts +++ b/middlewares/referrer-policy/index.ts @@ -59,5 +59,4 @@ function referrerPolicy(options: Readonly = {}) { }; } -module.exports = referrerPolicy; export default referrerPolicy; diff --git a/middlewares/referrer-policy/package-files.json b/middlewares/referrer-policy/package-files.json deleted file mode 100644 index 962389e9..00000000 --- a/middlewares/referrer-policy/package-files.json +++ /dev/null @@ -1 +0,0 @@ -["index.js", "index.d.ts"] diff --git a/middlewares/strict-transport-security/index.ts b/middlewares/strict-transport-security/index.ts index cc82a151..e0fafb44 100644 --- a/middlewares/strict-transport-security/index.ts +++ b/middlewares/strict-transport-security/index.ts @@ -67,5 +67,4 @@ function strictTransportSecurity( }; } -module.exports = strictTransportSecurity; export default strictTransportSecurity; diff --git a/middlewares/strict-transport-security/package-files.json b/middlewares/strict-transport-security/package-files.json deleted file mode 100644 index 962389e9..00000000 --- a/middlewares/strict-transport-security/package-files.json +++ /dev/null @@ -1 +0,0 @@ -["index.js", "index.d.ts"] diff --git a/middlewares/x-content-type-options/index.ts b/middlewares/x-content-type-options/index.ts index 24d9c9db..2eaa9330 100644 --- a/middlewares/x-content-type-options/index.ts +++ b/middlewares/x-content-type-options/index.ts @@ -11,5 +11,4 @@ function xContentTypeOptions() { }; } -module.exports = xContentTypeOptions; export default xContentTypeOptions; diff --git a/middlewares/x-content-type-options/package-files.json b/middlewares/x-content-type-options/package-files.json deleted file mode 100644 index 962389e9..00000000 --- a/middlewares/x-content-type-options/package-files.json +++ /dev/null @@ -1 +0,0 @@ -["index.js", "index.d.ts"] diff --git a/middlewares/x-dns-prefetch-control/index.ts b/middlewares/x-dns-prefetch-control/index.ts index dca19d60..1f16c494 100644 --- a/middlewares/x-dns-prefetch-control/index.ts +++ b/middlewares/x-dns-prefetch-control/index.ts @@ -19,5 +19,4 @@ function xDnsPrefetchControl( }; } -module.exports = xDnsPrefetchControl; export default xDnsPrefetchControl; diff --git a/middlewares/x-dns-prefetch-control/package-files.json b/middlewares/x-dns-prefetch-control/package-files.json deleted file mode 100644 index 962389e9..00000000 --- a/middlewares/x-dns-prefetch-control/package-files.json +++ /dev/null @@ -1 +0,0 @@ -["index.js", "index.d.ts"] diff --git a/middlewares/x-download-options/index.ts b/middlewares/x-download-options/index.ts index 6f841d01..b52450f6 100644 --- a/middlewares/x-download-options/index.ts +++ b/middlewares/x-download-options/index.ts @@ -11,5 +11,4 @@ function xDownloadOptions() { }; } -module.exports = xDownloadOptions; export default xDownloadOptions; diff --git a/middlewares/x-download-options/package-files.json b/middlewares/x-download-options/package-files.json deleted file mode 100644 index 962389e9..00000000 --- a/middlewares/x-download-options/package-files.json +++ /dev/null @@ -1 +0,0 @@ -["index.js", "index.d.ts"] diff --git a/middlewares/x-frame-options/index.ts b/middlewares/x-frame-options/index.ts index 9faddabc..cc99a244 100644 --- a/middlewares/x-frame-options/index.ts +++ b/middlewares/x-frame-options/index.ts @@ -41,5 +41,4 @@ function xFrameOptions(options: Readonly = {}) { }; } -module.exports = xFrameOptions; export default xFrameOptions; diff --git a/middlewares/x-frame-options/package-files.json b/middlewares/x-frame-options/package-files.json deleted file mode 100644 index 962389e9..00000000 --- a/middlewares/x-frame-options/package-files.json +++ /dev/null @@ -1 +0,0 @@ -["index.js", "index.d.ts"] diff --git a/middlewares/x-permitted-cross-domain-policies/index.ts b/middlewares/x-permitted-cross-domain-policies/index.ts index f8e99890..b991daae 100644 --- a/middlewares/x-permitted-cross-domain-policies/index.ts +++ b/middlewares/x-permitted-cross-domain-policies/index.ts @@ -40,5 +40,4 @@ function xPermittedCrossDomainPolicies( }; } -module.exports = xPermittedCrossDomainPolicies; export default xPermittedCrossDomainPolicies; diff --git a/middlewares/x-permitted-cross-domain-policies/package-files.json b/middlewares/x-permitted-cross-domain-policies/package-files.json deleted file mode 100644 index 962389e9..00000000 --- a/middlewares/x-permitted-cross-domain-policies/package-files.json +++ /dev/null @@ -1 +0,0 @@ -["index.js", "index.d.ts"] diff --git a/middlewares/x-powered-by/index.ts b/middlewares/x-powered-by/index.ts index ef6a165c..09ca7f82 100644 --- a/middlewares/x-powered-by/index.ts +++ b/middlewares/x-powered-by/index.ts @@ -11,5 +11,4 @@ function xPoweredBy() { }; } -module.exports = xPoweredBy; export default xPoweredBy; diff --git a/middlewares/x-powered-by/package-files.json b/middlewares/x-powered-by/package-files.json deleted file mode 100644 index 962389e9..00000000 --- a/middlewares/x-powered-by/package-files.json +++ /dev/null @@ -1 +0,0 @@ -["index.js", "index.d.ts"] diff --git a/middlewares/x-xss-protection/index.ts b/middlewares/x-xss-protection/index.ts index 672ae52d..428cf2e2 100644 --- a/middlewares/x-xss-protection/index.ts +++ b/middlewares/x-xss-protection/index.ts @@ -11,5 +11,4 @@ function xXssProtection() { }; } -module.exports = xXssProtection; export default xXssProtection; diff --git a/middlewares/x-xss-protection/package-files.json b/middlewares/x-xss-protection/package-files.json deleted file mode 100644 index 962389e9..00000000 --- a/middlewares/x-xss-protection/package-files.json +++ /dev/null @@ -1 +0,0 @@ -["index.js", "index.d.ts"] diff --git a/package-lock.json b/package-lock.json index 0b4ba1ca..e9f0af17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "4.6.0", "license": "MIT", "devDependencies": { + "@rollup/plugin-typescript": "^8.3.0", "@types/connect": "^3.4.35", "@types/jest": "^27.0.3", "@types/supertest": "^2.0.11", @@ -18,6 +19,7 @@ "eslint": "^8.3.0", "jest": "^27.3.1", "prettier": "^2.4.1", + "rollup": "^2.60.2", "supertest": "^6.1.6", "ts-jest": "^27.0.7", "typescript": "^4.5.2" @@ -1006,6 +1008,41 @@ "node": ">= 8" } }, + "node_modules/@rollup/plugin-typescript": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.0.tgz", + "integrity": "sha512-I5FpSvLbtAdwJ+naznv+B4sjXZUcIvLLceYpITAn7wAP8W0wqc5noLdGIp9HGVntNhRWXctwPYrSSFQxtl0FPA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "resolve": "^1.17.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0", + "tslib": "*", + "typescript": ">=3.7.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -1089,6 +1126,12 @@ "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", "dev": true }, + "node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -2450,6 +2493,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4580,6 +4629,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "2.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.2.tgz", + "integrity": "sha512-1Bgjpq61sPjgoZzuiDSGvbI1tD91giZABgjCQBKM5aYLnzjq52GoDuWVwT/cm/MCxCMPU8gqQvkj8doQ5C8Oqw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6162,6 +6226,27 @@ "fastq": "^1.6.0" } }, + "@rollup/plugin-typescript": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.0.tgz", + "integrity": "sha512-I5FpSvLbtAdwJ+naznv+B4sjXZUcIvLLceYpITAn7wAP8W0wqc5noLdGIp9HGVntNhRWXctwPYrSSFQxtl0FPA==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "resolve": "^1.17.0" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + } + }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -6242,6 +6327,12 @@ "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", "dev": true }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -7278,6 +7369,12 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -8897,6 +8994,15 @@ "glob": "^7.1.3" } }, + "rollup": { + "version": "2.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.2.tgz", + "integrity": "sha512-1Bgjpq61sPjgoZzuiDSGvbI1tD91giZABgjCQBKM5aYLnzjq52GoDuWVwT/cm/MCxCMPU8gqQvkj8doQ5C8Oqw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index 20fe5a49..c3dac835 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "node": ">=12.0.0" }, "devDependencies": { + "@rollup/plugin-typescript": "^8.3.0", "@types/connect": "^3.4.35", "@types/jest": "^27.0.3", "@types/supertest": "^2.0.11", @@ -34,23 +35,32 @@ "eslint": "^8.3.0", "jest": "^27.3.1", "prettier": "^2.4.1", + "rollup": "^2.60.2", "supertest": "^6.1.6", "ts-jest": "^27.0.7", "typescript": "^4.5.2" }, "scripts": { "pretest": "npm run lint", - "prepublishOnly": "npm run build", + "prepublishOnly": "npm run build-helmet", "lint": "npm run lint:eslint && npm run lint:prettier", "lint:eslint": "eslint \"**/*.ts\"", "lint:prettier": "prettier --check \"**/*{md,js,json,ts}\"", "format": "prettier --write \"**/*{md,js,json,ts}\"", "clean": "node ./bin/clean.js", - "build": "npm run clean && tsc && npm run format", - "build-middleware-package": "npm run build && node ./bin/build-middleware-package.js", + "build-helmet": "npm run clean && node ./bin/build-helmet.js && prettier --write --config .prettierrc-dist.cjs --ignore-path /dev/null dist", + "build-middleware-package": "npm run clean && tsc --emitDeclarationOnly -p tsconfig-types.json && node ./bin/build-middleware-package.js", "test": "jest" }, "license": "MIT", - "types": "dist/index.d.ts", - "main": "dist/index" + "type": "module", + "types": "./dist/index.d.ts", + "main": "./dist/index.cjs", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + } + } } diff --git a/test/content-security-policy.test.ts b/test/content-security-policy.test.ts index 288fd224..e18f8b0b 100644 --- a/test/content-security-policy.test.ts +++ b/test/content-security-policy.test.ts @@ -1,7 +1,7 @@ import { IncomingMessage, ServerResponse } from "http"; import { check } from "./helpers"; -import connect = require("connect"); -import supertest = require("supertest"); +import connect from "connect"; +import supertest from "supertest"; import contentSecurityPolicy, { getDefaultDirectives, dangerouslyDisableDefaultSrc, diff --git a/test/helpers.ts b/test/helpers.ts index c370ea03..ccf2d77c 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,6 +1,6 @@ import { IncomingMessage, ServerResponse } from "http"; -import connect = require("connect"); -import supertest = require("supertest"); +import connect from "connect"; +import supertest from "supertest"; interface MiddlewareFunction { (req: IncomingMessage, res: ServerResponse, next: () => void): void; diff --git a/test/index.test.ts b/test/index.test.ts index 51e9e8b0..7003f0c1 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,9 +1,9 @@ import { IncomingMessage, ServerResponse } from "http"; import { check } from "./helpers"; -import connect = require("connect"); -import supertest = require("supertest"); +import connect from "connect"; +import supertest from "supertest"; -import helmet from ".."; +import * as helmet from ".."; import contentSecurityPolicy from "../middlewares/content-security-policy"; import crossOriginEmbedderPolicy from "../middlewares/cross-origin-embedder-policy"; @@ -22,6 +22,8 @@ import xPoweredBy from "../middlewares/x-powered-by"; import xXssProtection from "../middlewares/x-xss-protection"; describe("helmet", () => { + const topLevel = helmet.default; + it("includes all middleware with their default options", async () => { // NOTE: This test relies on the CSP object being ordered a certain way, // which could change (and be non-breaking). If that becomes a problem, @@ -45,23 +47,23 @@ describe("helmet", () => { "x-xss-protection": "0", }; - await check(helmet(), expectedHeaders); - await check(helmet({}), expectedHeaders); - await check(helmet(Object.create(null)), expectedHeaders); + await check(topLevel(), expectedHeaders); + await check(topLevel({}), expectedHeaders); + await check(topLevel(Object.create(null)), expectedHeaders); }); it("allows individual middlewares to be disabled", async () => { - await check(helmet({ contentSecurityPolicy: false }), { + await check(topLevel({ contentSecurityPolicy: false }), { "content-security-policy": null, }); - await check(helmet({ dnsPrefetchControl: false }), { + await check(topLevel({ dnsPrefetchControl: false }), { "x-dns-prefetch-control": null, }); }); it("works with all default middlewares disabled", async () => { await check( - helmet({ + topLevel({ contentSecurityPolicy: false, dnsPrefetchControl: false, expectCt: false, @@ -91,37 +93,37 @@ describe("helmet", () => { }; expect(() => { - helmet(fakeRequest as any); + topLevel(fakeRequest as any); }).toThrow(); }); it("allows default middleware to be explicitly enabled (a no-op)", async () => { - await check(helmet({ frameguard: true }), { + await check(topLevel({ frameguard: true }), { "x-frame-options": "SAMEORIGIN", }); }); it("allows Cross-Origin-Embedder-Policy middleware to be enabled", async () => { - await check(helmet({ crossOriginEmbedderPolicy: true }), { + await check(topLevel({ crossOriginEmbedderPolicy: true }), { "cross-origin-embedder-policy": "require-corp", }); }); it("allows Cross-Origin-Embedder-Policy middleware to be explicitly disabled", async () => { - await check(helmet({ crossOriginEmbedderPolicy: false }), { + await check(topLevel({ crossOriginEmbedderPolicy: false }), { "cross-origin-embedder-policy": null, }); }); it("allows Cross-Origin-Opener-Policy middleware to be enabled with its default", async () => { - await check(helmet({ crossOriginOpenerPolicy: true }), { + await check(topLevel({ crossOriginOpenerPolicy: true }), { "cross-origin-opener-policy": "same-origin", }); }); it("allows Cross-Origin-Opener-Policy middleware to be enabled with custom arguments", async () => { await check( - helmet({ + topLevel({ crossOriginOpenerPolicy: { policy: "same-origin-allow-popups" }, }), { @@ -131,20 +133,20 @@ describe("helmet", () => { }); it("allows Cross-Origin-Opener-Policy middleware to be explicitly disabled", async () => { - await check(helmet({ crossOriginOpenerPolicy: false }), { + await check(topLevel({ crossOriginOpenerPolicy: false }), { "cross-origin-opener-policy": null, }); }); it("allows Cross-Origin-Resource-Policy middleware to be enabled with its default", async () => { - await check(helmet({ crossOriginResourcePolicy: true }), { + await check(topLevel({ crossOriginResourcePolicy: true }), { "cross-origin-resource-policy": "same-origin", }); }); it("allows Cross-Origin-Resource-Policy middleware to be enabled with custom arguments", async () => { await check( - helmet({ crossOriginResourcePolicy: { policy: "same-site" } }), + topLevel({ crossOriginResourcePolicy: { policy: "same-site" } }), { "cross-origin-resource-policy": "same-site", } @@ -152,19 +154,19 @@ describe("helmet", () => { }); it("allows Cross-Origin-Resource-Policy middleware to be explicitly disabled", async () => { - await check(helmet({ crossOriginResourcePolicy: false }), { + await check(topLevel({ crossOriginResourcePolicy: false }), { "cross-origin-resource-policy": null, }); }); it("allows Origin-Agent-Cluster middleware to be enabled", async () => { - await check(helmet({ originAgentCluster: true }), { + await check(topLevel({ originAgentCluster: true }), { "origin-agent-cluster": "?1", }); }); it("allows Origin-Agent-Cluster middleware to be explicitly disabled", async () => { - await check(helmet({ originAgentCluster: false }), { + await check(topLevel({ originAgentCluster: false }), { "origin-agent-cluster": null, }); }); @@ -172,7 +174,7 @@ describe("helmet", () => { it("properly handles a middleware calling `next()` with an error", async () => { const app = connect() .use( - helmet({ + topLevel({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'", () => "bad;value"], @@ -206,7 +208,7 @@ describe("helmet", () => { }); it("logs a warning when passing options to crossOriginEmbedderPolicy", () => { - helmet({ crossOriginEmbedderPolicy: { option: "foo" } as any }); + topLevel({ crossOriginEmbedderPolicy: { option: "foo" } as any }); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( @@ -215,7 +217,7 @@ describe("helmet", () => { }); it("logs a warning when passing options to hidePoweredBy", () => { - helmet({ hidePoweredBy: { setTo: "deprecated option" } as any }); + topLevel({ hidePoweredBy: { setTo: "deprecated option" } as any }); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( @@ -224,7 +226,7 @@ describe("helmet", () => { }); it("logs a warning when passing options to ieNoOpen", () => { - helmet({ ieNoOpen: { option: "foo" } as any }); + topLevel({ ieNoOpen: { option: "foo" } as any }); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( @@ -233,7 +235,7 @@ describe("helmet", () => { }); it("logs a warning when passing options to originAgentCluster", () => { - helmet({ originAgentCluster: { option: "foo" } as any }); + topLevel({ originAgentCluster: { option: "foo" } as any }); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( @@ -242,7 +244,7 @@ describe("helmet", () => { }); it("logs a warning when passing options to noSniff", () => { - helmet({ noSniff: { option: "foo" } as any }); + topLevel({ noSniff: { option: "foo" } as any }); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( @@ -251,7 +253,7 @@ describe("helmet", () => { }); it("logs a warning when passing options to xssFilter", () => { - helmet({ xssFilter: { setOnOldIe: true } as any }); + topLevel({ xssFilter: { setOnOldIe: true } as any }); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( diff --git a/tsconfig-commonjs.json b/tsconfig-commonjs.json new file mode 100644 index 00000000..cac0a68a --- /dev/null +++ b/tsconfig-commonjs.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig", + "include": ["tmp-commonjs-index.ts"] +} diff --git a/tsconfig-esm.json b/tsconfig-esm.json new file mode 100644 index 00000000..c3bd6135 --- /dev/null +++ b/tsconfig-esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig", + "exclude": ["dist"], + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationDir": "." + } +} diff --git a/tsconfig-types.json b/tsconfig-types.json new file mode 100644 index 00000000..6d7b1e96 --- /dev/null +++ b/tsconfig-types.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "outDir": "dist", + "declaration": true + } +} diff --git a/tsconfig.json b/tsconfig.json index c540c1a8..8fb9e362 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,13 @@ { "compilerOptions": { - "declaration": true, "esModuleInterop": true, - "module": "commonjs", + "module": "esnext", + "moduleResolution": "node", "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noUnusedLocals": true, "noUnusedParameters": true, "noUncheckedIndexedAccess": true, - "outDir": "./dist", "strict": true, "target": "es6" }