From f127a07916d6b3dc1228171ff4c00b259d17cb9f Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Thu, 13 Apr 2023 10:41:28 +0300 Subject: [PATCH] feat: Enforce dependencies for imports from node modules in package.json Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen --- .../webpack/src/get-node-config.js | 7 +-- .../get-dependency-name.js | 9 ++++ .../get-dependency-name.test.js | 27 ++++++++++ ...protect-from-importing-non-dependencies.js | 54 +++++++++++++++++++ 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 packages/infrastructure/webpack/src/plugins/get-dependency-name/get-dependency-name.js create mode 100644 packages/infrastructure/webpack/src/plugins/get-dependency-name/get-dependency-name.test.js create mode 100644 packages/infrastructure/webpack/src/plugins/protect-from-importing-non-dependencies.js diff --git a/packages/infrastructure/webpack/src/get-node-config.js b/packages/infrastructure/webpack/src/get-node-config.js index 4ffd9f831a..828201af04 100644 --- a/packages/infrastructure/webpack/src/get-node-config.js +++ b/packages/infrastructure/webpack/src/get-node-config.js @@ -1,6 +1,5 @@ const ForkTsCheckerPlugin = require("fork-ts-checker-webpack-plugin"); -const nodeExternals = require("webpack-node-externals"); -const path = require("path"); +const { ProtectFromImportingNonDependencies } = require("./plugins/protect-from-importing-non-dependencies"); module.exports = ({ entrypointFilePath, outputDirectory }) => ({ name: entrypointFilePath, @@ -14,10 +13,12 @@ module.exports = ({ entrypointFilePath, outputDirectory }) => ({ }, resolve: { - extensions: [".ts", ".tsx"], + extensions: [".ts", ".tsx", ".js"], }, plugins: [ + new ProtectFromImportingNonDependencies(), + new ForkTsCheckerPlugin({ typescript: { mode: "write-dts", diff --git a/packages/infrastructure/webpack/src/plugins/get-dependency-name/get-dependency-name.js b/packages/infrastructure/webpack/src/plugins/get-dependency-name/get-dependency-name.js new file mode 100644 index 0000000000..520d0c14c6 --- /dev/null +++ b/packages/infrastructure/webpack/src/plugins/get-dependency-name/get-dependency-name.js @@ -0,0 +1,9 @@ +const getDependencyName = (requireString) => { + const [a, b] = requireString.split("/"); + + const scoped = a.startsWith("@"); + + return scoped ? `${a}/${b}` : a; +}; + +module.exports = { getDependencyName }; diff --git a/packages/infrastructure/webpack/src/plugins/get-dependency-name/get-dependency-name.test.js b/packages/infrastructure/webpack/src/plugins/get-dependency-name/get-dependency-name.test.js new file mode 100644 index 0000000000..a6f57bb74d --- /dev/null +++ b/packages/infrastructure/webpack/src/plugins/get-dependency-name/get-dependency-name.test.js @@ -0,0 +1,27 @@ +import { getDependencyName } from "./get-dependency-name"; + +describe("get-dependency-name", () => { + it("given scoped dependency with entrypoint, returns dependency name", () => { + const actual = getDependencyName("@some-scope/some-package/entrypoint"); + + expect(actual).toBe("@some-scope/some-package"); + }); + + it("given scoped dependency but no entrypoint, returns dependency name", () => { + const actual = getDependencyName("@some-scope/some-package"); + + expect(actual).toBe("@some-scope/some-package"); + }); + + it("given non scoped dependency with entrypoint, returns dependency name", () => { + const actual = getDependencyName("some-package/some-entrypoint"); + + expect(actual).toBe("some-package"); + }); + + it("given non scoped dependency but no entrypoint, returns dependency name", () => { + const actual = getDependencyName("some-package"); + + expect(actual).toBe("some-package"); + }); +}); diff --git a/packages/infrastructure/webpack/src/plugins/protect-from-importing-non-dependencies.js b/packages/infrastructure/webpack/src/plugins/protect-from-importing-non-dependencies.js new file mode 100644 index 0000000000..3ccadfa670 --- /dev/null +++ b/packages/infrastructure/webpack/src/plugins/protect-from-importing-non-dependencies.js @@ -0,0 +1,54 @@ +const path = require("path"); +const { getDependencyName } = require("./get-dependency-name/get-dependency-name"); + +const pathToPackageJson = path.resolve(process.cwd(), "package.json"); + +class ProtectFromImportingNonDependencies { + apply(compiler) { + const dependencies = getDependenciesAndPeerDependencies(); + + const nodeModulesToBeResolved = new Set(); + + compiler.hooks.normalModuleFactory.tap("irrelevant", (normalModuleFactory) => { + normalModuleFactory.hooks.resolve.tap("irrelevant", (toBeResolved) => { + + const isLocalDependency = toBeResolved.request.startsWith("."); + const isDependencyOfDependency = + toBeResolved.context.includes("node_modules"); + + if (!isLocalDependency && !isDependencyOfDependency) { + + const dependencyName = getDependencyName(toBeResolved.request); + + nodeModulesToBeResolved.add(dependencyName); + } + }); + }); + + compiler.hooks.afterCompile.tap("compile", () => { + const notSpecifiedDependencies = [...nodeModulesToBeResolved].filter( + (x) => !dependencies.includes(x) + ); + + if (notSpecifiedDependencies.length) { + throw new Error( + `Tried to import dependencies that are not specified in the package.json "${pathToPackageJson}". Add "${notSpecifiedDependencies.join( + '", "' + )}" to dependencies or peerDependencies.` + ); + } + }); + } +} +const getDependenciesAndPeerDependencies = () => { + const packageJson = require(pathToPackageJson); + + const dependencies = Object.keys(packageJson.dependencies || {}); + const peerDependencies = Object.keys(packageJson.peerDependencies || {}); + + return [...dependencies, ...peerDependencies]; +}; + +module.exports = { + ProtectFromImportingNonDependencies, +};