Make webpack respect dependencies vs peerDependencies when deciding dependency is external (#7545)

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen authored and GitHub committed 2023-04-13 16:01:43 +03:00
1 parent adf67fb6a7
commit 7dc0ce7192
19 files changed
+231 -44

No files matched your search

@@ -0,0 +1,9 @@
const getDependencyName = (requireString) => {
const [a, b] = requireString.split("/");
const scoped = a.startsWith("@");
return scoped ? `${a}/${b}` : a;
};
module.exports = { getDependencyName };
@@ -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");
});
});
@@ -0,0 +1,29 @@
const ExternalModuleFactoryPlugin = require("webpack/lib/ExternalModuleFactoryPlugin");
const path = require("path");
const {
toModuleMatcherRegExp,
} = require("./to-module-matcher-reg-exp/to-module-matcher-reg-exp");
class MakePeerDependenciesExternalPlugin {
apply(compiler) {
compiler.hooks.compile.tap("compile", (params) => {
const peerDependencies = getPeerDependencies();
new ExternalModuleFactoryPlugin(
compiler.options.output.library.type,
peerDependencies.map(toModuleMatcherRegExp)
).apply(params.normalModuleFactory);
});
}
}
const getPeerDependencies = () => {
const pathToPackageJson = path.resolve(process.cwd(), "package.json");
const packageJson = require(pathToPackageJson);
return Object.keys(packageJson.peerDependencies || {});
};
module.exports = { MakePeerDependenciesExternalPlugin };
@@ -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,
};
@@ -0,0 +1,3 @@
const toModuleMatcherRegExp = x => new RegExp(`^${x}(/.*)*$`);
module.exports = { toModuleMatcherRegExp };
@@ -0,0 +1,57 @@
import { toModuleMatcherRegExp } from "./to-module-matcher-reg-exp";
describe('to-module-matcher-reg-exp', () => {
let regExp;
beforeEach(() => {
regExp = toModuleMatcherRegExp("some-package");
});
it('given exactly matching package, matches', () => {
const targetString = 'some-package';
const [match] = targetString.match(regExp);
expect(match).toBeTruthy()
});
it('given matching package with entrypoint, matches', () => {
const targetString = 'some-package/some-entrypoint';
const [match] = targetString.match(regExp);
expect(match).toBeTruthy()
});
it('given matching package with directory, matches', () => {
const targetString = 'some-package/some-directory/some-other-directory';
const [match] = targetString.match(regExp);
expect(match).toBeTruthy()
});
it('given package that starts with same name but is still different, does not match', () => {
const targetString = 'some-package-but-still-different';
const actual = targetString.match(regExp);
expect(actual).toBeNull()
});
it('given package that starts with something else, does not match', () => {
const targetString = 'different-some-package';
const actual = targetString.match(regExp);
expect(actual).toBeNull()
});
it('given irrelevant package, does not match', () => {
const targetString = 'some-other-package';
const actual = targetString.match(regExp);
expect(actual).toBeNull()
});
});