diff --git a/package.json b/package.json index 37f5fa8493..abeeaa2426 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,8 @@ "node-notifier": "5.4.0", "node-sass": "4.12.0", "npm-run-all": "4.1.5", + "pixrem": "5.0.0", + "postcss-pseudoelements": "5.0.0", "postcss-short-size": "4.0.0", "postcss-trolling": "0.1.7", "pre-commit": "1.2.2", diff --git a/packages/next/build/webpack/config/blocks/css/plugins.ts b/packages/next/build/webpack/config/blocks/css/plugins.ts index 1e10145149..0bf441207b 100644 --- a/packages/next/build/webpack/config/blocks/css/plugins.ts +++ b/packages/next/build/webpack/config/blocks/css/plugins.ts @@ -2,36 +2,79 @@ import chalk from 'chalk' import { findConfig } from '../../../../../lib/find-config' import { resolveRequest } from '../../../../../lib/resolve-request' -export async function getPostCssPlugins(dir: string): Promise { - function load(plugins: { [key: string]: object | false }): unknown[] { - return Object.keys(plugins) - .map(pkg => { - const options = plugins[pkg] - if (options === false) { - return false - } +type CssPluginCollection_Array = (string | [string, boolean | object])[] - const pluginPath = resolveRequest(pkg, `${dir}/`) +type CssPluginCollection_Object = { [key: string]: object | boolean } - if (options == null || Object.keys(options).length === 0) { - return require(pluginPath) - } - return require(pluginPath)(options) - }) - .filter(Boolean) +type CssPluginCollection = + | CssPluginCollection_Array + | CssPluginCollection_Object + +type CssPluginShape = [string, object | boolean] + +const genericErrorText = 'Malformed PostCSS Configuration' + +function getError_NullConfig(pluginName: string) { + return `${chalk.red.bold( + 'Error' + )}: Your PostCSS configuration for '${pluginName}' cannot have ${chalk.bold( + 'null' + )} configuration.\nTo disable '${pluginName}', pass ${chalk.bold( + 'false' + )}, otherwise, pass ${chalk.bold('true')} or a configuration object.` +} + +function isIgnoredPlugin(pluginPath: string): boolean { + const ignoredRegex = /(?:^|[\\/])(postcss-modules-values|postcss-modules-scope|postcss-modules-extract-imports|postcss-modules-local-by-default|postcss-modules)(?:[\\/]|$)/i + const match = ignoredRegex.exec(pluginPath) + if (match == null) { + return false } - const config = await findConfig<{ plugins: { [key: string]: object } }>( - dir, - 'postcss' + const plugin = match.pop()! + console.warn( + `${chalk.yellow.bold('Warning')}: Please remove the ${chalk.underline( + plugin + )} plugin from your PostCSS configuration. ` + + `This plugin is automatically configured by Next.js.` ) + return true +} - let target: unknown[] +async function loadPlugin( + dir: string, + pluginName: string, + options: boolean | object +): Promise { + if (options === false || isIgnoredPlugin(pluginName)) { + return false + } - if (!config) { - target = load({ - [require.resolve('postcss-flexbugs-fixes')]: {}, - [require.resolve('postcss-preset-env')]: { + if (options == null) { + console.error(getError_NullConfig(pluginName)) + throw new Error(genericErrorText) + } + + const pluginPath = resolveRequest(pluginName, `${dir}/`) + if (isIgnoredPlugin(pluginPath)) { + return false + } else if (options === true) { + return require(pluginPath) + } else { + const keys = Object.keys(options) + if (keys.length === 0) { + return require(pluginPath) + } + return require(pluginPath)(options) + } +} + +function getDefaultPlugins(): CssPluginCollection { + return [ + require.resolve('postcss-flexbugs-fixes'), + [ + require.resolve('postcss-preset-env'), + { autoprefixer: { // Disable legacy flexbox support flexbox: 'no-2009', @@ -40,49 +83,109 @@ export async function getPostCssPlugins(dir: string): Promise { // web platform, i.e. in 2+ browsers unflagged. stage: 3, }, - }) - } else { - const plugins = config.plugins - if (plugins == null || typeof plugins !== 'object') { - throw new Error( - `Your custom PostCSS configuration must export a \`plugins\` key.` - ) - } + ], + ] +} - const invalidKey = Object.keys(config).find(key => key !== 'plugins') - if (invalidKey) { - console.warn( - `${chalk.yellow.bold( - 'Warning' - )}: Your PostCSS configuration defines a field which is not supported (\`${invalidKey}\`). ` + - `Please remove this configuration value.` - ) - } +export async function getPostCssPlugins( + dir: string +): Promise { + let config = await findConfig<{ plugins: CssPluginCollection }>( + dir, + 'postcss' + ) - // These plugins cannot be enabled by the user because they'll conflict with - // `css-loader`'s behavior to make us compatible with webpack. - ;[ - 'postcss-modules-values', - 'postcss-modules-scope', - 'postcss-modules-extract-imports', - 'postcss-modules-local-by-default', - 'postcss-modules', - ].forEach(plugin => { - if (!plugins.hasOwnProperty(plugin)) { - return - } - - console.warn( - `${chalk.yellow.bold('Warning')}: Please remove the ${chalk.underline( - plugin - )} plugin from your PostCSS configuration. ` + - `This plugin is automatically configured by Next.js.` - ) - delete plugins[plugin] - }) - - target = load(plugins as { [key: string]: object }) + if (config == null) { + config = { plugins: getDefaultPlugins() } } - return target + // Warn user about configuration keys which are not respected + const invalidKey = Object.keys(config).find(key => key !== 'plugins') + if (invalidKey) { + console.warn( + `${chalk.yellow.bold( + 'Warning' + )}: Your PostCSS configuration defines a field which is not supported (\`${invalidKey}\`). ` + + `Please remove this configuration value.` + ) + } + + // Enforce the user provided plugins if the configuration file is present + let plugins = config.plugins + if (plugins == null || typeof plugins !== 'object') { + throw new Error( + `Your custom PostCSS configuration must export a \`plugins\` key.` + ) + } + + if (!Array.isArray(plugins)) { + // Capture variable so TypeScript is happy + const pc = plugins + + plugins = Object.keys(plugins).reduce((acc, curr) => { + const p = pc[curr] + if (typeof p === 'undefined') { + console.error(getError_NullConfig(curr)) + throw new Error(genericErrorText) + } + + acc.push([curr, p]) + return acc + }, [] as CssPluginCollection_Array) + } + + const parsed: CssPluginShape[] = [] + plugins.forEach(plugin => { + if (plugin == null) { + console.warn( + `${chalk.yellow.bold('Warning')}: A ${chalk.bold( + 'null' + )} PostCSS plugin was provided. This entry will be ignored.` + ) + } else if (typeof plugin === 'string') { + parsed.push([plugin, true]) + } else if (Array.isArray(plugin)) { + const pluginName = plugin[0] + const pluginConfig = plugin[1] + if ( + typeof pluginName === 'string' && + (typeof pluginConfig === 'boolean' || typeof pluginConfig === 'object') + ) { + parsed.push([pluginName, pluginConfig]) + } else { + if (typeof pluginName !== 'string') { + console.error( + `${chalk.red.bold( + 'Error' + )}: A PostCSS Plugin must be provided as a ${chalk.bold( + 'string' + )}. Instead, we got: '${pluginName}'.` + ) + } else { + console.error( + `${chalk.red.bold( + 'Error' + )}: A PostCSS Plugin was passed as an array but did not provide its configuration ('${pluginName}').` + ) + } + throw new Error(genericErrorText) + } + } else { + console.error( + `${chalk.red.bold( + 'Error' + )}: An unknown PostCSS plugin was provided (${plugin}).` + ) + throw new Error(genericErrorText) + } + }) + + const resolved = await Promise.all( + parsed.map(p => loadPlugin(dir, p[0], p[1])) + ) + const filtered: import('postcss').AcceptedPlugin[] = resolved.filter( + Boolean + ) as import('postcss').AcceptedPlugin[] + + return filtered } diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-1/.postcssrc.json b/test/integration/css/fixtures/bad-custom-configuration-arr-1/.postcssrc.json new file mode 100644 index 0000000000..ab27508fd9 --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-1/.postcssrc.json @@ -0,0 +1,3 @@ +{ + "plugins": [["postcss-trolling"]] +} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-1/pages/_app.js b/test/integration/css/fixtures/bad-custom-configuration-arr-1/pages/_app.js new file mode 100644 index 0000000000..17a2196742 --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-1/pages/_app.js @@ -0,0 +1,12 @@ +import React from 'react' +import App from 'next/app' +import '../styles/global.css' + +class MyApp extends App { + render() { + const { Component, pageProps } = this.props + return + } +} + +export default MyApp diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-1/pages/index.js b/test/integration/css/fixtures/bad-custom-configuration-arr-1/pages/index.js new file mode 100644 index 0000000000..b3ba78da2d --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-1/pages/index.js @@ -0,0 +1,3 @@ +export default function Home() { + return
+} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-1/styles/global.css b/test/integration/css/fixtures/bad-custom-configuration-arr-1/styles/global.css new file mode 100644 index 0000000000..78d27f7edd --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-1/styles/global.css @@ -0,0 +1,14 @@ +/* this should pass through untransformed */ +@media (480px <= width < 768px) { + a::before { + content: ''; + } + ::placeholder { + color: green; + } +} + +/* this should be transformed to width/height */ +.video { + -xyz-max-size: 400rem 300rem; +} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-2/.postcssrc.json b/test/integration/css/fixtures/bad-custom-configuration-arr-2/.postcssrc.json new file mode 100644 index 0000000000..c6df57bdda --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-2/.postcssrc.json @@ -0,0 +1,3 @@ +{ + "plugins": [["postcss-trolling", null]] +} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-2/pages/_app.js b/test/integration/css/fixtures/bad-custom-configuration-arr-2/pages/_app.js new file mode 100644 index 0000000000..17a2196742 --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-2/pages/_app.js @@ -0,0 +1,12 @@ +import React from 'react' +import App from 'next/app' +import '../styles/global.css' + +class MyApp extends App { + render() { + const { Component, pageProps } = this.props + return + } +} + +export default MyApp diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-2/pages/index.js b/test/integration/css/fixtures/bad-custom-configuration-arr-2/pages/index.js new file mode 100644 index 0000000000..b3ba78da2d --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-2/pages/index.js @@ -0,0 +1,3 @@ +export default function Home() { + return
+} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-2/styles/global.css b/test/integration/css/fixtures/bad-custom-configuration-arr-2/styles/global.css new file mode 100644 index 0000000000..78d27f7edd --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-2/styles/global.css @@ -0,0 +1,14 @@ +/* this should pass through untransformed */ +@media (480px <= width < 768px) { + a::before { + content: ''; + } + ::placeholder { + color: green; + } +} + +/* this should be transformed to width/height */ +.video { + -xyz-max-size: 400rem 300rem; +} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-3/.postcssrc.json b/test/integration/css/fixtures/bad-custom-configuration-arr-3/.postcssrc.json new file mode 100644 index 0000000000..e886576085 --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-3/.postcssrc.json @@ -0,0 +1,3 @@ +{ + "plugins": [[5, null]] +} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-3/pages/_app.js b/test/integration/css/fixtures/bad-custom-configuration-arr-3/pages/_app.js new file mode 100644 index 0000000000..17a2196742 --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-3/pages/_app.js @@ -0,0 +1,12 @@ +import React from 'react' +import App from 'next/app' +import '../styles/global.css' + +class MyApp extends App { + render() { + const { Component, pageProps } = this.props + return + } +} + +export default MyApp diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-3/pages/index.js b/test/integration/css/fixtures/bad-custom-configuration-arr-3/pages/index.js new file mode 100644 index 0000000000..b3ba78da2d --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-3/pages/index.js @@ -0,0 +1,3 @@ +export default function Home() { + return
+} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-3/styles/global.css b/test/integration/css/fixtures/bad-custom-configuration-arr-3/styles/global.css new file mode 100644 index 0000000000..78d27f7edd --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-3/styles/global.css @@ -0,0 +1,14 @@ +/* this should pass through untransformed */ +@media (480px <= width < 768px) { + a::before { + content: ''; + } + ::placeholder { + color: green; + } +} + +/* this should be transformed to width/height */ +.video { + -xyz-max-size: 400rem 300rem; +} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-4/.postcssrc.json b/test/integration/css/fixtures/bad-custom-configuration-arr-4/.postcssrc.json new file mode 100644 index 0000000000..05e159725d --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-4/.postcssrc.json @@ -0,0 +1,3 @@ +{ + "plugins": [5] +} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-4/pages/_app.js b/test/integration/css/fixtures/bad-custom-configuration-arr-4/pages/_app.js new file mode 100644 index 0000000000..17a2196742 --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-4/pages/_app.js @@ -0,0 +1,12 @@ +import React from 'react' +import App from 'next/app' +import '../styles/global.css' + +class MyApp extends App { + render() { + const { Component, pageProps } = this.props + return + } +} + +export default MyApp diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-4/pages/index.js b/test/integration/css/fixtures/bad-custom-configuration-arr-4/pages/index.js new file mode 100644 index 0000000000..b3ba78da2d --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-4/pages/index.js @@ -0,0 +1,3 @@ +export default function Home() { + return
+} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-4/styles/global.css b/test/integration/css/fixtures/bad-custom-configuration-arr-4/styles/global.css new file mode 100644 index 0000000000..78d27f7edd --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-4/styles/global.css @@ -0,0 +1,14 @@ +/* this should pass through untransformed */ +@media (480px <= width < 768px) { + a::before { + content: ''; + } + ::placeholder { + color: green; + } +} + +/* this should be transformed to width/height */ +.video { + -xyz-max-size: 400rem 300rem; +} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-5/.postcssrc.json b/test/integration/css/fixtures/bad-custom-configuration-arr-5/.postcssrc.json new file mode 100644 index 0000000000..1e02e994a4 --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-5/.postcssrc.json @@ -0,0 +1,3 @@ +{ + "plugins": null +} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-5/pages/_app.js b/test/integration/css/fixtures/bad-custom-configuration-arr-5/pages/_app.js new file mode 100644 index 0000000000..17a2196742 --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-5/pages/_app.js @@ -0,0 +1,12 @@ +import React from 'react' +import App from 'next/app' +import '../styles/global.css' + +class MyApp extends App { + render() { + const { Component, pageProps } = this.props + return + } +} + +export default MyApp diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-5/pages/index.js b/test/integration/css/fixtures/bad-custom-configuration-arr-5/pages/index.js new file mode 100644 index 0000000000..b3ba78da2d --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-5/pages/index.js @@ -0,0 +1,3 @@ +export default function Home() { + return
+} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-5/styles/global.css b/test/integration/css/fixtures/bad-custom-configuration-arr-5/styles/global.css new file mode 100644 index 0000000000..78d27f7edd --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-5/styles/global.css @@ -0,0 +1,14 @@ +/* this should pass through untransformed */ +@media (480px <= width < 768px) { + a::before { + content: ''; + } + ::placeholder { + color: green; + } +} + +/* this should be transformed to width/height */ +.video { + -xyz-max-size: 400rem 300rem; +} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-6/.postcssrc.json b/test/integration/css/fixtures/bad-custom-configuration-arr-6/.postcssrc.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-6/.postcssrc.json @@ -0,0 +1 @@ +{} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-6/pages/_app.js b/test/integration/css/fixtures/bad-custom-configuration-arr-6/pages/_app.js new file mode 100644 index 0000000000..17a2196742 --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-6/pages/_app.js @@ -0,0 +1,12 @@ +import React from 'react' +import App from 'next/app' +import '../styles/global.css' + +class MyApp extends App { + render() { + const { Component, pageProps } = this.props + return + } +} + +export default MyApp diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-6/pages/index.js b/test/integration/css/fixtures/bad-custom-configuration-arr-6/pages/index.js new file mode 100644 index 0000000000..b3ba78da2d --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-6/pages/index.js @@ -0,0 +1,3 @@ +export default function Home() { + return
+} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-6/styles/global.css b/test/integration/css/fixtures/bad-custom-configuration-arr-6/styles/global.css new file mode 100644 index 0000000000..78d27f7edd --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-6/styles/global.css @@ -0,0 +1,14 @@ +/* this should pass through untransformed */ +@media (480px <= width < 768px) { + a::before { + content: ''; + } + ::placeholder { + color: green; + } +} + +/* this should be transformed to width/height */ +.video { + -xyz-max-size: 400rem 300rem; +} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-7/.postcssrc.json b/test/integration/css/fixtures/bad-custom-configuration-arr-7/.postcssrc.json new file mode 100644 index 0000000000..561d0cb76d --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-7/.postcssrc.json @@ -0,0 +1,3 @@ +{ + "plugins": [["postcss-trolling", 5]] +} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-7/pages/_app.js b/test/integration/css/fixtures/bad-custom-configuration-arr-7/pages/_app.js new file mode 100644 index 0000000000..17a2196742 --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-7/pages/_app.js @@ -0,0 +1,12 @@ +import React from 'react' +import App from 'next/app' +import '../styles/global.css' + +class MyApp extends App { + render() { + const { Component, pageProps } = this.props + return + } +} + +export default MyApp diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-7/pages/index.js b/test/integration/css/fixtures/bad-custom-configuration-arr-7/pages/index.js new file mode 100644 index 0000000000..b3ba78da2d --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-7/pages/index.js @@ -0,0 +1,3 @@ +export default function Home() { + return
+} diff --git a/test/integration/css/fixtures/bad-custom-configuration-arr-7/styles/global.css b/test/integration/css/fixtures/bad-custom-configuration-arr-7/styles/global.css new file mode 100644 index 0000000000..78d27f7edd --- /dev/null +++ b/test/integration/css/fixtures/bad-custom-configuration-arr-7/styles/global.css @@ -0,0 +1,14 @@ +/* this should pass through untransformed */ +@media (480px <= width < 768px) { + a::before { + content: ''; + } + ::placeholder { + color: green; + } +} + +/* this should be transformed to width/height */ +.video { + -xyz-max-size: 400rem 300rem; +} diff --git a/test/integration/css/fixtures/custom-configuration-arr/.postcssrc.json b/test/integration/css/fixtures/custom-configuration-arr/.postcssrc.json new file mode 100644 index 0000000000..def6136b4c --- /dev/null +++ b/test/integration/css/fixtures/custom-configuration-arr/.postcssrc.json @@ -0,0 +1,16 @@ +{ + // Use comments to test JSON5 support + "plugins": [ + "pixrem", + ["postcss-pseudoelements", true], + // Test a non-standard feature that wouldn't be normally enabled + [ + "postcss-short-size", + { + // Add a prefix to test that configuration is passed + "prefix": "xyz" + } + ], + ["postcss-trolling", false] + ] +} diff --git a/test/integration/css/fixtures/custom-configuration-arr/pages/_app.js b/test/integration/css/fixtures/custom-configuration-arr/pages/_app.js new file mode 100644 index 0000000000..17a2196742 --- /dev/null +++ b/test/integration/css/fixtures/custom-configuration-arr/pages/_app.js @@ -0,0 +1,12 @@ +import React from 'react' +import App from 'next/app' +import '../styles/global.css' + +class MyApp extends App { + render() { + const { Component, pageProps } = this.props + return + } +} + +export default MyApp diff --git a/test/integration/css/fixtures/custom-configuration-arr/pages/index.js b/test/integration/css/fixtures/custom-configuration-arr/pages/index.js new file mode 100644 index 0000000000..b3ba78da2d --- /dev/null +++ b/test/integration/css/fixtures/custom-configuration-arr/pages/index.js @@ -0,0 +1,3 @@ +export default function Home() { + return
+} diff --git a/test/integration/css/fixtures/custom-configuration-arr/styles/global.css b/test/integration/css/fixtures/custom-configuration-arr/styles/global.css new file mode 100644 index 0000000000..78d27f7edd --- /dev/null +++ b/test/integration/css/fixtures/custom-configuration-arr/styles/global.css @@ -0,0 +1,14 @@ +/* this should pass through untransformed */ +@media (480px <= width < 768px) { + a::before { + content: ''; + } + ::placeholder { + color: green; + } +} + +/* this should be transformed to width/height */ +.video { + -xyz-max-size: 400rem 300rem; +} diff --git a/test/integration/css/test/index.test.js b/test/integration/css/test/index.test.js index 95be6802f5..7f1affdf80 100644 --- a/test/integration/css/test/index.test.js +++ b/test/integration/css/test/index.test.js @@ -283,6 +283,73 @@ describe('CSS Support', () => { }) }) + describe('CSS Customization Array', () => { + const appDir = join(fixturesDir, 'custom-configuration-arr') + + beforeAll(async () => { + await remove(join(appDir, '.next')) + }) + + it('should build successfully', async () => { + await nextBuild(appDir) + }) + + it(`should've compiled and prefixed`, async () => { + const cssFolder = join(appDir, '.next/static/css') + + const files = await readdir(cssFolder) + const cssFiles = files.filter(f => /\.css$/.test(f)) + + expect(cssFiles.length).toBe(1) + const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8') + expect( + cssContent.replace(/\/\*.*?\*\//g, '').trim() + ).toMatchInlineSnapshot( + `"@media (480px <= width < 768px){a:before{content:\\"\\"}::placeholder{color:green}}.video{max-width:6400px;max-height:4800px;max-width:400rem;max-height:300rem}"` + ) + + // Contains a source map + expect(cssContent).toMatch(/\/\*#\s*sourceMappingURL=(.+\.map)\s*\*\//) + }) + + it(`should've emitted a source map`, async () => { + const cssFolder = join(appDir, '.next/static/css') + + const files = await readdir(cssFolder) + const cssMapFiles = files.filter(f => /\.css\.map$/.test(f)) + + expect(cssMapFiles.length).toBe(1) + const cssMapContent = ( + await readFile(join(cssFolder, cssMapFiles[0]), 'utf8') + ).trim() + + const { version, mappings, sourcesContent } = JSON.parse(cssMapContent) + expect({ version, mappings, sourcesContent }).toMatchInlineSnapshot(` + Object { + "mappings": "AACA,gCACE,SACE,UACF,CACA,cACE,WACF,CACF,CAGA,OACE,gBAA4B,CAA5B,iBAA4B,CAA5B,gBAA4B,CAA5B,iBACF", + "sourcesContent": Array [ + "/* this should pass through untransformed */ + @media (480px <= width < 768px) { + a::before { + content: ''; + } + ::placeholder { + color: green; + } + } + + /* this should be transformed to width/height */ + .video { + -xyz-max-size: 400rem 300rem; + } + ", + ], + "version": 3, + } + `) + }) + }) + describe('Bad CSS Customization', () => { const appDir = join(fixturesDir, 'bad-custom-configuration') @@ -333,6 +400,126 @@ describe('CSS Support', () => { }) }) + describe('Bad CSS Customization Array (1)', () => { + const appDir = join(fixturesDir, 'bad-custom-configuration-arr-1') + + beforeAll(async () => { + await remove(join(appDir, '.next')) + }) + + it('should fail the build', async () => { + const { stderr } = await nextBuild(appDir, [], { stderr: true }) + + expect(stderr).toMatch( + /A PostCSS Plugin was passed as an array but did not provide its configuration \('postcss-trolling'\)/ + ) + expect(stderr).toMatch(/Build error occurred/) + }) + }) + + describe('Bad CSS Customization Array (2)', () => { + const appDir = join(fixturesDir, 'bad-custom-configuration-arr-2') + + beforeAll(async () => { + await remove(join(appDir, '.next')) + }) + + it('should fail the build', async () => { + const { stderr } = await nextBuild(appDir, [], { stderr: true }) + + expect(stderr).toMatch( + /Error: Your PostCSS configuration for 'postcss-trolling' cannot have null configuration./ + ) + expect(stderr).toMatch( + /To disable 'postcss-trolling', pass false, otherwise, pass true or a configuration object./ + ) + expect(stderr).toMatch(/Build error occurred/) + }) + }) + + describe('Bad CSS Customization Array (3)', () => { + const appDir = join(fixturesDir, 'bad-custom-configuration-arr-3') + + beforeAll(async () => { + await remove(join(appDir, '.next')) + }) + + it('should fail the build', async () => { + const { stderr } = await nextBuild(appDir, [], { stderr: true }) + + expect(stderr).toMatch( + /A PostCSS Plugin must be provided as a string. Instead, we got: '5'/ + ) + expect(stderr).toMatch(/Build error occurred/) + }) + }) + + describe('Bad CSS Customization Array (4)', () => { + const appDir = join(fixturesDir, 'bad-custom-configuration-arr-4') + + beforeAll(async () => { + await remove(join(appDir, '.next')) + }) + + it('should fail the build', async () => { + const { stderr } = await nextBuild(appDir, [], { stderr: true }) + + expect(stderr).toMatch(/An unknown PostCSS plugin was provided \(5\)/) + expect(stderr).toMatch(/Build error occurred/) + }) + }) + + describe('Bad CSS Customization Array (5)', () => { + const appDir = join(fixturesDir, 'bad-custom-configuration-arr-5') + + beforeAll(async () => { + await remove(join(appDir, '.next')) + }) + + it('should fail the build', async () => { + const { stderr } = await nextBuild(appDir, [], { stderr: true }) + + expect(stderr).toMatch( + /Your custom PostCSS configuration must export a `plugins` key./ + ) + expect(stderr).toMatch(/Build error occurred/) + }) + }) + + describe('Bad CSS Customization Array (6)', () => { + const appDir = join(fixturesDir, 'bad-custom-configuration-arr-6') + + beforeAll(async () => { + await remove(join(appDir, '.next')) + }) + + it('should fail the build', async () => { + const { stderr } = await nextBuild(appDir, [], { stderr: true }) + + expect(stderr).toMatch( + /Your custom PostCSS configuration must export a `plugins` key./ + ) + expect(stderr).toMatch(/Build error occurred/) + }) + }) + + describe('Bad CSS Customization Array (7)', () => { + const appDir = join(fixturesDir, 'bad-custom-configuration-arr-7') + + beforeAll(async () => { + await remove(join(appDir, '.next')) + }) + + it('should fail the build', async () => { + const { stderr } = await nextBuild(appDir, [], { stderr: true }) + + expect(stderr).toMatch( + /A PostCSS Plugin was passed as an array but did not provide its configuration \('postcss-trolling'\)/ + ) + expect(stderr).toMatch(/Build error occurred/) + }) + }) + // Tests css ordering describe('Multi Global Support (reversed)', () => { const appDir = join(fixturesDir, 'multi-global-reversed') diff --git a/yarn.lock b/yarn.lock index a2a9103bba..39ee59be62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4164,7 +4164,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.0.0, browserslist@^4.6.0, browserslist@^4.6.3, browserslist@^4.6.4, browserslist@^4.7.1, browserslist@^4.7.3: +browserslist@^4.0.0, browserslist@^4.3.6, browserslist@^4.6.0, browserslist@^4.6.3, browserslist@^4.6.4, browserslist@^4.7.1, browserslist@^4.7.3: version "4.8.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.2.tgz#b45720ad5fbc8713b7253c20766f701c9a694289" integrity sha512-+M4oeaTplPm/f1pXDw84YohEv7B1i/2Aisei8s4s6k3QsoSHa7i5sz8u/cGQkkatCPxMASKxPualR4wwYgVboA== @@ -11792,6 +11792,15 @@ pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" +pixrem@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pixrem/-/pixrem-5.0.0.tgz#460c534fbc19e4e9fbf39012ae26c7107cd40bca" + integrity sha512-ugJ4Imy92u55zeznaN/5d7iqOBIZjZ7q10/T+dcd0IuFtbLlsGDvAUabFu1cafER+G9f0T1WtTqvzm4KAdcDgQ== + dependencies: + browserslist "^4.3.6" + postcss "^7.0.7" + reduce-css-calc "^2.1.5" + pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" @@ -12415,6 +12424,13 @@ postcss-pseudo-class-any-link@^6.0.0: postcss "^7.0.2" postcss-selector-parser "^5.0.0-rc.3" +postcss-pseudoelements@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudoelements/-/postcss-pseudoelements-5.0.0.tgz#eef194e8d524645ca520a949e95e518e812402cb" + integrity sha1-7vGU6NUkZFylIKlJ6V5RjoEkAss= + dependencies: + postcss "^6.0.0" + postcss-reduce-initial@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" @@ -12548,7 +12564,7 @@ postcss@^5.0.10: source-map "^0.5.6" supports-color "^3.2.3" -postcss@^6.0.1, postcss@^6.0.23, postcss@^6.0.9: +postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.23, postcss@^6.0.9: version "6.0.23" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== @@ -12584,6 +12600,15 @@ postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.18, postcss@^7.0 source-map "^0.6.1" supports-color "^6.1.0" +postcss@^7.0.7: + version "7.0.24" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.24.tgz#972c3c5be431b32e40caefe6c81b5a19117704c2" + integrity sha512-Xl0XvdNWg+CblAXzNvbSOUvgJXwSjmbAKORqyw9V2AlHrm1js2gFw9y3jibBAhpKZi8b5JzJCVh/FyzPsTtgTA== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + pre-commit@1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/pre-commit/-/pre-commit-1.2.2.tgz#dbcee0ee9de7235e57f79c56d7ce94641a69eec6" @@ -13155,7 +13180,7 @@ redent@^2.0.0: indent-string "^3.0.0" strip-indent "^2.0.0" -reduce-css-calc@^2.1.6: +reduce-css-calc@^2.1.5, reduce-css-calc@^2.1.6: version "2.1.7" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2" integrity sha512-fDnlZ+AybAS3C7Q9xDq5y8A2z+lT63zLbynew/lur/IR24OQF5x98tfNwf79mzEdfywZ0a2wpM860FhFfMxZlA==