diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd1423edd..66634b7eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ * Adds asset module option `options.modulePreloadPolyfill` (default `true`) to allow disabling the polyfill preload for e.g. external front-ends. * Adds `bundleMarkup` to the data sent to the external front-end, containing all markup for injecting Apostrophe UI in the front-end. +### Changes + +* Removes postcss plugin and webpack loader used for breakpoint preview mode. Uses instead the new `postcss-viewport-to-container-toggle` plugin in the webpack config. + ## 4.9.0 (2024-10-31) ### Adds diff --git a/modules/@apostrophecms/asset/index.js b/modules/@apostrophecms/asset/index.js index 3482ba9c33..bf6c5dba65 100644 --- a/modules/@apostrophecms/asset/index.js +++ b/modules/@apostrophecms/asset/index.js @@ -485,6 +485,7 @@ module.exports = { // - `types`: optional array, if present it represents the only entrypoint types (entrypoint.type) // that should be built. // - `sourcemaps`: if `true`, the source maps are generated in production. + // - `postcssViewportToContainerToggle`: the configuration for the breakpoint preview plugin. // // Note that this getter depends on the current build task arguments. You shouldn't // use that directly. @@ -500,7 +501,13 @@ module.exports = { hmr: self.hasHMR(), hmrPort: self.options.hmrPort, modulePreloadPolyfill: self.options.modulePreloadPolyfill, - sourcemaps: self.options.productionSourceMaps + sourcemaps: self.options.productionSourceMaps, + postcssViewportToContainerToggle: { + enable: self.options.breakpointPreviewMode?.enable === true, + debug: self.options.breakpointPreviewMode?.debug === true, + modifierAttr: 'data-breakpoint-preview-mode', + transform: self.options.breakpointPreviewMode?.transform + } }; options.devServer = !options.isTask && self.hasDevServer() ? self.options.hmr @@ -633,7 +640,7 @@ module.exports = { return; } - // Hidrate the entrypoints with the saved manifest data and + // Hydrate the entrypoints with the saved manifest data and // set the current build manifest data. const buildOptions = self.getBuildOptions(); const entrypoints = await self.getBuildModule().entrypoints(buildOptions); diff --git a/modules/@apostrophecms/asset/lib/webpack/apos/webpack.scss.js b/modules/@apostrophecms/asset/lib/webpack/apos/webpack.scss.js index 8cde57c689..bb9dab4198 100644 --- a/modules/@apostrophecms/asset/lib/webpack/apos/webpack.scss.js +++ b/modules/@apostrophecms/asset/lib/webpack/apos/webpack.scss.js @@ -1,25 +1,17 @@ -const path = require('path'); -const postcssReplaceViewportUnitsPlugin = require('../postcss-replace-viewport-units-plugin'); +const postcssViewportToContainerToggle = require('postcss-viewport-to-container-toggle'); module.exports = (options, apos) => { const postcssPlugins = [ + ...apos.asset.options.breakpointPreviewMode?.enable === true ? [ + postcssViewportToContainerToggle({ + modifierAttr: 'data-breakpoint-preview-mode', + debug: apos.asset.options.breakpointPreviewMode?.debug === true, + transform: apos.asset.options.breakpointPreviewMode?.transform || null + }) + ] : [], 'autoprefixer', {} ]; - let mediaToContainerQueriesLoader = ''; - - if (apos.asset.options.breakpointPreviewMode?.enable === true) { - postcssPlugins.unshift( - postcssReplaceViewportUnitsPlugin() - ); - mediaToContainerQueriesLoader = { - loader: path.resolve(__dirname, '../media-to-container-queries-loader.js'), - options: { - debug: apos.asset.options.breakpointPreviewMode?.debug === true, - transform: apos.asset.options.breakpointPreviewMode?.transform || null - } - }; - } return { module: { @@ -28,15 +20,22 @@ module.exports = (options, apos) => { test: /\.css$/, use: [ 'vue-style-loader', - mediaToContainerQueriesLoader, - 'css-loader' + 'css-loader', + { + loader: 'postcss-loader', + options: { + sourceMap: true, + postcssOptions: { + plugins: [ postcssPlugins ] + } + } + } ] }, { test: /\.s[ac]ss$/, use: [ 'vue-style-loader', - mediaToContainerQueriesLoader, 'css-loader', { loader: 'postcss-loader', diff --git a/modules/@apostrophecms/asset/lib/webpack/media-to-container-queries-loader.js b/modules/@apostrophecms/asset/lib/webpack/media-to-container-queries-loader.js deleted file mode 100644 index 2da34a1bd9..0000000000 --- a/modules/@apostrophecms/asset/lib/webpack/media-to-container-queries-loader.js +++ /dev/null @@ -1,94 +0,0 @@ -const postcss = require('postcss'); - -module.exports = function (source) { - const schema = { - title: 'Media to Container Queries Loader options', - type: 'object', - properties: { - debug: { - type: 'boolean' - }, - transform: { - anyOf: [ - { type: 'null' }, - { instanceof: 'Function' } - ] - } - } - }; - const options = this.getOptions(schema); - - const mediaQueryRegex = /@media[^{]*{([\s\S]*?})\s*(\\n)*}/g; - - const convertToContainerQuery = (mediaFeature) => { - // NOTE: media queries does not work with the combo - // - min-width, max-width, min-height, max-height - // - lower than equal, greater than equal - const DESCRIPTORS = [ - 'min-width', - 'max-width', - 'min-height', - 'max-height' - ]; - const OPERATORS = [ - '>=', - '<=' - ]; - - const containerFeature = typeof options.transform === 'function' - ? options.transform(mediaFeature) - : mediaFeature; - - if ( - options.debug && - DESCRIPTORS.some(descriptor => containerFeature.includes(descriptor)) && - OPERATORS.some(operator => containerFeature.includes(operator)) - ) { - console.warn('[mediaToContainerQueryLoader] Unsupported media query', containerFeature); - } - - return containerFeature; - }; - - // Prepend container query to media queries - const modifiedSource = source.replace(mediaQueryRegex, (match) => { - const root = postcss.parse(match.replaceAll(/(? { - if ( - atRule.params.includes('print') && - (!atRule.params.includes('all') || !atRule.params.includes('screen')) - ) { - return; - } - - // Container query - const containerAtRule = atRule.clone({ - name: 'container', - params: convertToContainerQuery(atRule.params) - .replaceAll(/(only\s*)?(all|screen|print)(,)?(\s)*(and\s*)?/g, '') - }); - - // Media query - // Only apply when data-breakpoint-preview-mode is not set - atRule.walkRules(rule => { - const newRule = rule.clone({ - selectors: rule.selectors.map(selector => { - if (selector.startsWith('body')) { - return selector.replace('body', ':where(body:not([data-breakpoint-preview-mode]))'); - } - - return `:where(body:not([data-breakpoint-preview-mode])) ${selector}`; - }) - }); - - rule.replaceWith(newRule); - }); - - root.append(containerAtRule); - }); - - return root.toString(); - }); - - return modifiedSource; -}; diff --git a/modules/@apostrophecms/asset/lib/webpack/postcss-replace-viewport-units-plugin.js b/modules/@apostrophecms/asset/lib/webpack/postcss-replace-viewport-units-plugin.js deleted file mode 100644 index 723380cd2f..0000000000 --- a/modules/@apostrophecms/asset/lib/webpack/postcss-replace-viewport-units-plugin.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @type {import('postcss').PluginCreator} - */ -module.exports = (opts = {}) => { - return { - postcssPlugin: 'postcss-replace-viewport-units-plugin', - Once (root, postcss) { - root.walkRules(rule => { - const declsToCopy = []; - - rule.walkDecls(decl => { - if (!decl.value.includes('vh') && !decl.value.includes('vw')) { - return; - } - - const clonedDeclWithContainerQueryUnits = decl.clone({ - value: decl.value - .replaceAll('vh', 'cqh') - .replaceAll('vw', 'cqw') - }); - - declsToCopy.push(clonedDeclWithContainerQueryUnits); - }); - - if (!declsToCopy.length) { - return; - } - - const prefixedRule = new postcss.Rule({ - selector: `:where(body[data-breakpoint-preview-mode]) ${rule.selector}`, - nodes: declsToCopy - }); - - root.insertAfter(rule, prefixedRule); - }); - } - }; -}; - -module.exports.postcss = true; diff --git a/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js b/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js index abbf07cb90..6e03e34f4a 100644 --- a/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js +++ b/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js @@ -1,26 +1,18 @@ -const path = require('path'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const postcssReplaceViewportUnitsPlugin = require('../postcss-replace-viewport-units-plugin'); +const postcssViewportToContainerToggle = require('postcss-viewport-to-container-toggle'); module.exports = (options, apos, srcBuildNames) => { const postcssPlugins = [ + ...apos.asset.options.breakpointPreviewMode?.enable === true ? [ + postcssViewportToContainerToggle({ + modifierAttr: 'data-breakpoint-preview-mode', + debug: apos.asset.options.breakpointPreviewMode?.debug === true, + transform: apos.asset.options.breakpointPreviewMode?.transform || null + }) + ] : [], 'autoprefixer', {} ]; - let mediaToContainerQueriesLoader = ''; - - if (apos.asset.options.breakpointPreviewMode?.enable === true) { - postcssPlugins.unshift( - postcssReplaceViewportUnitsPlugin() - ); - mediaToContainerQueriesLoader = { - loader: path.resolve(__dirname, '../media-to-container-queries-loader.js'), - options: { - debug: apos.asset.options.breakpointPreviewMode?.debug === true, - transform: apos.asset.options.breakpointPreviewMode?.transform || null - } - }; - } return { module: { @@ -30,8 +22,8 @@ module.exports = (options, apos, srcBuildNames) => { use: [ // Instead of style-loader, to avoid FOUC MiniCssExtractPlugin.loader, - mediaToContainerQueriesLoader, - // Parses CSS imports and make css-loader ignore urls. Urls will still be handled by webpack + // Parses CSS imports and make css-loader ignore urls. + // Urls will still be handled by webpack { loader: 'css-loader', options: { url: false } diff --git a/package.json b/package.json index d08aa00b8b..31ef3121c3 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "postcss-html": "^1.3.0", "postcss-loader": "^8.1.1", "postcss-scss": "^4.0.3", + "postcss-viewport-to-container-toggle": "^1.0.0", "prompts": "^2.4.1", "qs": "^6.10.1", "regexp-quote": "0.0.0", diff --git a/test/postcss.js b/test/postcss.js deleted file mode 100644 index 32dbfc6eec..0000000000 --- a/test/postcss.js +++ /dev/null @@ -1,64 +0,0 @@ -const postcss = require('postcss'); -const { equal, deepEqual } = require('node:assert'); - -describe('postcss-replace-viewport-units-plugin', () => { - const plugin = require('../modules/@apostrophecms/asset/lib/webpack/postcss-replace-viewport-units-plugin.js'); - - it('should map `vh` values to `cqh` in a rule that applies only on breakpoint preview ', async () => { - const input = '.hello { width: 100vh; }'; - const output = '.hello { width: 100vh; }\n:where(body[data-breakpoint-preview-mode]) .hello { width: 100cqh; }'; - - await run(plugin, input, output, { }); - }); - - it('should map `vw` values to `cqw` in a rule that applies only on breakpoint preview ', async () => { - const input = '.hello { width: 100vw; }'; - const output = '.hello { width: 100vw; }\n:where(body[data-breakpoint-preview-mode]) .hello { width: 100cqw; }'; - - await run(plugin, input, output, { }); - }); - - it('should map `vh` and `vw` values used in `calc` to `cqh` and `cqw` in a rule that applies only on breakpoint preview', async () => { - const input = ` -.hello { height: calc(100vh - 50px); width: calc(100vw - 10px); }`; - const output = ` -.hello { height: calc(100vh - 50px); width: calc(100vw - 10px); } -:where(body[data-breakpoint-preview-mode]) .hello { height: calc(100cqh - 50px); width: calc(100cqw - 10px); }`; - - await run(plugin, input, output, { }); - }); - - it('should add only declarations containing `vh` and `vw` values in a rule that applies only on breakpoint preview', async () => { - const input = ` -.hello { - position: absolute; - top: 0; - left: 0; - width: 100vw; - height: calc(100vh - 50px); -}`; - - const output = ` -.hello { - position: absolute; - top: 0; - left: 0; - width: 100vw; - height: calc(100vh - 50px); -} -:where(body[data-breakpoint-preview-mode]) .hello { - width: 100cqw; - height: calc(100cqh - 50px); -}`; - - await run(plugin, input, output, { }); - }); -}); - -// From https://github.com/postcss/postcss-plugin-boilerplate/blob/main/template/index.test.t.js -async function run(plugin, input, output, opts = {}) { - const result = await postcss([ plugin(opts) ]).process(input, { from: undefined }); - - equal(result.css, output); - deepEqual(result.warnings(), []); -}