diff --git a/src/index.js b/src/index.js index a27bc0e0..7c72c08a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +import path from "path"; + import postcss from "postcss"; import { satisfies } from "semver"; import postcssPackage from "postcss/package.json"; @@ -11,8 +13,13 @@ import { exec, normalizeSourceMap, normalizeSourceMapAfterPostcss, + parsePackageJson, + findPackageJsonDir, } from "./utils"; +let hasExplicitDependencyOnPostCSS = false; +let packageJsonDir; + /** * **PostCSS Loader** * @@ -26,7 +33,6 @@ import { * * @return {callback} callback Result */ - export default async function loader(content, sourceMap, meta) { const options = this.getOptions(schema); const callback = this.async(); @@ -102,6 +108,32 @@ export default async function loader(content, sourceMap, meta) { processOptions ); } catch (error) { + // The `findPackageJsonDir` function returns `string` or `null`. + // This is used to do for caching, that is, an explicit comparison with `undefined` + // is used to make the condition body run once. + if (packageJsonDir === undefined) { + packageJsonDir = findPackageJsonDir(process.cwd(), this.fs.statSync); + } + // Check postcss versions to avoid using PostCSS 7. + // For caching reasons, we use the readFileSync and existsSync functions from the context, + // not the functions from the `fs` module. + if ( + !hasExplicitDependencyOnPostCSS && + postcssFactory().version.startsWith("7.") && + packageJsonDir + ) { + const filePath = path.resolve(packageJsonDir, "package.json"); + const pkg = parsePackageJson(filePath, this.fs.readFileSync); + if (!pkg.dependencies.postcss && !pkg.devDependencies.postcss) { + this.emitWarning( + "Add postcss as project dependency. postcss is not a peer dependency for postcss-loader. " + + "Use `npm install postcss` or `yarn add postcss`" + ); + } else { + hasExplicitDependencyOnPostCSS = true; + } + } + if (error.file) { this.addDependency(error.file); } diff --git a/src/utils.js b/src/utils.js index 4ca898ab..1c847f5b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -408,10 +408,33 @@ function normalizeSourceMapAfterPostcss(map, resourceContext) { return newMap; } +function parsePackageJson(filePath, readFileSync) { + return JSON.parse(readFileSync(filePath, "utf8")); +} + +function findPackageJsonDir(cwd, statSync) { + let dir = cwd; + for (;;) { + try { + if (statSync(path.join(dir, "package.json")).isFile()) break; + // eslint-disable-next-line no-empty + } catch (error) {} + const parent = path.dirname(dir); + if (dir === parent) { + dir = null; + break; + } + dir = parent; + } + return dir; +} + export { loadConfig, getPostcssOptions, exec, normalizeSourceMap, normalizeSourceMapAfterPostcss, + parsePackageJson, + findPackageJsonDir, }; diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index e26bce3e..75379459 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -1,5 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`check postcss versions to avoid using PostCSS 7 should emit a warning if postcss version is not explicitly specified when the loader is failed: warnings 1`] = ` +Array [ + "ModuleWarning: Module Warning (from \`replaced original path\`): +(Emitted value instead of an instance of Error) Add postcss as project dependency. postcss is not a peer dependency for postcss-loader. Use \`npm install postcss\` or \`yarn add postcss\`", +] +`; + exports[`loader should emit asset using the "messages" API: errors 1`] = `Array []`; exports[`loader should emit asset using the "messages" API: warnings 1`] = `Array []`; diff --git a/test/loader.test.js b/test/loader.test.js index 4cd9b196..2d218943 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -2,6 +2,9 @@ import path from "path"; import postcss from "postcss"; +// eslint-disable-next-line import/no-namespace +import * as utils from "../src/utils"; + import { compile, getCompiler, @@ -198,3 +201,45 @@ describe("loader", () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); }); + +describe("check postcss versions to avoid using PostCSS 7", async () => { + async function getStats() { + const compiler = getCompiler("./css/index.js", { + implementation: (...args) => { + const result = postcss(...args); + result.version = "7.0.0"; + result.process = () => + Promise.reject(new Error("Something went wrong.")); + return result; + }, + }); + return compile(compiler); + } + + it("should emit a warning if postcss version is not explicitly specified when the loader is failed", async () => { + jest + .spyOn(utils, "parsePackageJson") + .mockReturnValue({ dependencies: {}, devDependencies: {} }); + const stats = await getStats(); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + }); + + it("should not show a warning if postcss version is explicitly defined", async () => { + jest.spyOn(utils, "parsePackageJson").mockReturnValue({ + dependencies: {}, + devDependencies: { postcss: "8.0.0" }, + }); + const stats = await getStats(); + expect(stats.compilation.warnings.length).toBe(0); + }); + + it("should not show a warning if the package.json file was not found", async () => { + jest.spyOn(utils, "findPackageJsonDir").mockReturnValue(null); + jest.spyOn(utils, "parsePackageJson").mockReturnValue({ + dependencies: {}, + devDependencies: { postcss: "8.0.0" }, + }); + const stats = await getStats(); + expect(stats.compilation.warnings.length).toBe(0); + }); +});