From 070fb5ea3f6ea77de76fd846eb1b01389a63f691 Mon Sep 17 00:00:00 2001 From: Romain Menke Date: Wed, 20 Aug 2025 10:04:46 +0200 Subject: [PATCH] postcss-global-data: late remover --- plugins/postcss-global-data/CHANGELOG.md | 5 + plugins/postcss-global-data/README.md | 44 ++++++- plugins/postcss-global-data/dist/index.cjs | 2 +- plugins/postcss-global-data/dist/index.d.ts | 4 + plugins/postcss-global-data/dist/index.mjs | 2 +- plugins/postcss-global-data/docs/README.md | 44 ++++++- plugins/postcss-global-data/src/index.ts | 124 +++++++++++++----- plugins/postcss-global-data/test/_tape.mjs | 116 ++++++++++++++++ .../test/basic.append-implicit.expect.css | 23 ++++ .../test/basic.append.expect.css | 23 ++++ .../test/basic.prepend.expect.css | 23 ++++ .../test/fixtures/fixture.css | 2 +- .../postcss-global-data/test/late-remover.css | 1 + .../test/late-remover.expect.css | 3 + 14 files changed, 381 insertions(+), 35 deletions(-) create mode 100644 plugins/postcss-global-data/test/basic.append-implicit.expect.css create mode 100644 plugins/postcss-global-data/test/basic.append.expect.css create mode 100644 plugins/postcss-global-data/test/basic.prepend.expect.css create mode 100644 plugins/postcss-global-data/test/late-remover.css create mode 100644 plugins/postcss-global-data/test/late-remover.expect.css diff --git a/plugins/postcss-global-data/CHANGELOG.md b/plugins/postcss-global-data/CHANGELOG.md index d84e237b9f..13f1c0f0a6 100644 --- a/plugins/postcss-global-data/CHANGELOG.md +++ b/plugins/postcss-global-data/CHANGELOG.md @@ -1,5 +1,10 @@ # Changes to PostCSS global-data +### Unreleased (minor) + +- Add `prepend` plugin option +- Add `lateRemover` plugin option + ### 3.0.0 _August 3, 2024_ diff --git a/plugins/postcss-global-data/README.md b/plugins/postcss-global-data/README.md index f23b678c05..ce075dbaee 100644 --- a/plugins/postcss-global-data/README.md +++ b/plugins/postcss-global-data/README.md @@ -48,7 +48,7 @@ instructions for: ## Options -### files +### `files` The `files` option determines which files to inject into PostCSS. @@ -61,6 +61,48 @@ postcssGlobalData({ }); ``` +### Plugin order control with `lateRemover` + +The `lateRemover` option gives you more options when ordering plugins. + +```js +// esm +import postcss from 'postcss'; +import postcssGlobalData from '@csstools/postcss-global-data'; + +const [globalData, globalDataLateRemover] = postcssGlobalData({ + files: [ + './src/css/variables.css', + './src/css/media-queries.css', + ], + lateRemover: true +}).plugins + +postcss([ + globalData, + /* other plugins */ + globalDataLateRemover +]).process(YOUR_CSS /*, processOptions */); +``` + +### `prepend` + +The `prepend` option determines if injected CSS is appended or prepended. +Defaults to `false`. + +> [!Warning] +> Prepending styles before `@import` statements will create broken stylesheets. + +```js +postcssGlobalData({ + files: [ + './src/css/variables.css', + './src/css/media-queries.css', + ], + prepend: true +}); +``` + [cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test [discord]: https://discord.gg/bUadyRwkJS diff --git a/plugins/postcss-global-data/dist/index.cjs b/plugins/postcss-global-data/dist/index.cjs index 788f586001..ec5bb5e064 100644 --- a/plugins/postcss-global-data/dist/index.cjs +++ b/plugins/postcss-global-data/dist/index.cjs @@ -1 +1 @@ -"use strict";var e=require("node:path"),s=require("node:fs"),r=require("node:module");function parseImport(t,o,a,n){let c="";try{if(a.startsWith("node_modules://")){c=r.createRequire(process.cwd()).resolve(a.slice(15))}else if(a.startsWith("node_modules:")){c=r.createRequire(process.cwd()).resolve(a.slice(13))}else c=e.resolve(a)}catch(e){throw new Error(`Failed to read ${a} with error ${e instanceof Error?e.message:e}`)}if(n.has(c))return!1;n.add(c),o.result.messages.push({type:"dependency",plugin:"postcss-global-data",file:c,parent:t.source?.input?.file});const i=s.readFileSync(c,"utf8");return o.postcss.parse(i,{from:c})}const creator=e=>{const s=Object.assign({files:[]},e);return{postcssPlugin:"postcss-global-data",prepare(){let e=new Set,r=new Set;return{postcssPlugin:"postcss-global-data",Once(t,o){s.files.forEach(s=>{if(e.has(s))return;const a=parseImport(t,o,s,e);a&&a.each(e=>{t.append(e),r.add(e)})})},OnceExit(){r.forEach(e=>{e.remove()}),r=new Set,e=new Set}}}}};creator.postcss=!0,module.exports=creator; +"use strict";var e=require("node:path"),r=require("node:fs"),s=require("node:module");function parseImport(t,o,n,c){let a="";try{if(n.startsWith("node_modules://")){a=s.createRequire(process.cwd()).resolve(n.slice(15))}else if(n.startsWith("node_modules:")){a=s.createRequire(process.cwd()).resolve(n.slice(13))}else a=e.resolve(n)}catch(e){throw new Error(`Failed to read ${n} with error ${e instanceof Error?e.message:e}`)}if(c.has(a))return!1;c.add(a),o.result.messages.push({type:"dependency",plugin:"postcss-global-data",file:a,parent:t.source?.input?.file});const i=r.readFileSync(a,"utf8");return o.postcss.parse(i,{from:a})}const t="postcss-global-data",creator=e=>{const r=Object.assign({files:[],lateRemover:!1,prepend:!1},e);function insert(e,s,t){if(!r.prepend)return void s.each(r=>{e.append(r),t.add(r)});const o=Array.from(s.nodes);o.reverse(),o.forEach(r=>{e.prepend(r),t.add(r)})}if(!r.lateRemover)return{postcssPlugin:t,prepare(){const e=new Set,s=new Set;return{postcssPlugin:t,Once(t,o){r.files.forEach(r=>{const n=parseImport(t,o,r,s);n&&insert(t,n,e)})},OnceExit(){e.forEach(e=>{e.remove()}),s.clear(),e.clear()}}}};const s=new WeakSet;return{postcssPlugin:t,plugins:[{postcssPlugin:t,prepare(){const e=new Set;return{postcssPlugin:t,Once(t,o){r.files.forEach(r=>{const n=parseImport(t,o,r,e);n&&insert(t,n,s)})},OnceExit(){e.clear()}}}},{postcssPlugin:t+"/late-remover",OnceExit(e){e.each(e=>{s.has(e)&&e.remove()})}}]}};creator.postcss=!0,module.exports=creator; diff --git a/plugins/postcss-global-data/dist/index.d.ts b/plugins/postcss-global-data/dist/index.d.ts index 4311d5a993..92d4432d17 100644 --- a/plugins/postcss-global-data/dist/index.d.ts +++ b/plugins/postcss-global-data/dist/index.d.ts @@ -7,6 +7,10 @@ export default creator; export declare type pluginOptions = { /** List of files to be used as context */ files?: Array; + /** Remove nodes in a separate plugin object, this object can be added later in your list of plugins */ + lateRemover?: boolean; + /** Add global CSS to the start of files, defaults to `false` */ + prepend?: boolean; }; export { } diff --git a/plugins/postcss-global-data/dist/index.mjs b/plugins/postcss-global-data/dist/index.mjs index 5201bf6d29..a5a5e2ed7a 100644 --- a/plugins/postcss-global-data/dist/index.mjs +++ b/plugins/postcss-global-data/dist/index.mjs @@ -1 +1 @@ -import e from"node:path";import s from"node:fs";import r from"node:module";function parseImport(t,o,a,n){let c="";try{if(a.startsWith("node_modules://")){c=r.createRequire(process.cwd()).resolve(a.slice(15))}else if(a.startsWith("node_modules:")){c=r.createRequire(process.cwd()).resolve(a.slice(13))}else c=e.resolve(a)}catch(e){throw new Error(`Failed to read ${a} with error ${e instanceof Error?e.message:e}`)}if(n.has(c))return!1;n.add(c),o.result.messages.push({type:"dependency",plugin:"postcss-global-data",file:c,parent:t.source?.input?.file});const l=s.readFileSync(c,"utf8");return o.postcss.parse(l,{from:c})}const creator=e=>{const s=Object.assign({files:[]},e);return{postcssPlugin:"postcss-global-data",prepare(){let e=new Set,r=new Set;return{postcssPlugin:"postcss-global-data",Once(t,o){s.files.forEach(s=>{if(e.has(s))return;const a=parseImport(t,o,s,e);a&&a.each(e=>{t.append(e),r.add(e)})})},OnceExit(){r.forEach(e=>{e.remove()}),r=new Set,e=new Set}}}}};creator.postcss=!0;export{creator as default}; +import e from"node:path";import r from"node:fs";import s from"node:module";function parseImport(t,o,n,c){let a="";try{if(n.startsWith("node_modules://")){a=s.createRequire(process.cwd()).resolve(n.slice(15))}else if(n.startsWith("node_modules:")){a=s.createRequire(process.cwd()).resolve(n.slice(13))}else a=e.resolve(n)}catch(e){throw new Error(`Failed to read ${n} with error ${e instanceof Error?e.message:e}`)}if(c.has(a))return!1;c.add(a),o.result.messages.push({type:"dependency",plugin:"postcss-global-data",file:a,parent:t.source?.input?.file});const p=r.readFileSync(a,"utf8");return o.postcss.parse(p,{from:a})}const t="postcss-global-data",creator=e=>{const r=Object.assign({files:[],lateRemover:!1,prepend:!1},e);function insert(e,s,t){if(!r.prepend)return void s.each(r=>{e.append(r),t.add(r)});const o=Array.from(s.nodes);o.reverse(),o.forEach(r=>{e.prepend(r),t.add(r)})}if(!r.lateRemover)return{postcssPlugin:t,prepare(){const e=new Set,s=new Set;return{postcssPlugin:t,Once(t,o){r.files.forEach(r=>{const n=parseImport(t,o,r,s);n&&insert(t,n,e)})},OnceExit(){e.forEach(e=>{e.remove()}),s.clear(),e.clear()}}}};const s=new WeakSet;return{postcssPlugin:t,plugins:[{postcssPlugin:t,prepare(){const e=new Set;return{postcssPlugin:t,Once(t,o){r.files.forEach(r=>{const n=parseImport(t,o,r,e);n&&insert(t,n,s)})},OnceExit(){e.clear()}}}},{postcssPlugin:t+"/late-remover",OnceExit(e){e.each(e=>{s.has(e)&&e.remove()})}}]}};creator.postcss=!0;export{creator as default}; diff --git a/plugins/postcss-global-data/docs/README.md b/plugins/postcss-global-data/docs/README.md index efb3752db5..51e1e249d9 100644 --- a/plugins/postcss-global-data/docs/README.md +++ b/plugins/postcss-global-data/docs/README.md @@ -33,7 +33,7 @@ can actually use it. ## Options -### files +### `files` The `files` option determines which files to inject into PostCSS. @@ -46,4 +46,46 @@ The `files` option determines which files to inject into PostCSS. }); ``` +### Plugin order control with `lateRemover` + +The `lateRemover` option gives you more options when ordering plugins. + +```js +// esm +import postcss from 'postcss'; +import from ''; + +const [globalData, globalDataLateRemover] = ({ + files: [ + './src/css/variables.css', + './src/css/media-queries.css', + ], + lateRemover: true +}).plugins + +postcss([ + globalData, + /* other plugins */ + globalDataLateRemover +]).process(YOUR_CSS /*, processOptions */); +``` + +### `prepend` + +The `prepend` option determines if injected CSS is appended or prepended. +Defaults to `false`. + +> [!Warning] +> Prepending styles before `@import` statements will create broken stylesheets. + +```js +({ + files: [ + './src/css/variables.css', + './src/css/media-queries.css', + ], + prepend: true +}); +``` + diff --git a/plugins/postcss-global-data/src/index.ts b/plugins/postcss-global-data/src/index.ts index 1e77c67f2c..e84d39d97f 100644 --- a/plugins/postcss-global-data/src/index.ts +++ b/plugins/postcss-global-data/src/index.ts @@ -1,57 +1,121 @@ -import type { ChildNode, Plugin, PluginCreator } from 'postcss'; +import type { ChildNode, Plugin, PluginCreator, Root } from 'postcss'; import { parseImport } from './parse-import'; /** postcss-global-data plugin options */ export type pluginOptions = { /** List of files to be used as context */ files?: Array, + /** Remove nodes in a separate plugin object, this object can be added later in your list of plugins */ + lateRemover?: boolean, + /** Add global CSS to the start of files, defaults to `false` */ + prepend?: boolean, }; +const pluginName = 'postcss-global-data'; + const creator: PluginCreator = (opts?: pluginOptions) => { const options = Object.assign( // Default options { files: [], + lateRemover: false, + prepend: false, }, // Provided options opts, ); - return { - postcssPlugin: 'postcss-global-data', - prepare(): Plugin { - let importedFiles = new Set(); - let importedCSS = new Set(); - - return { - postcssPlugin: 'postcss-global-data', - Once(root, postcssHelpers): void { - options.files.forEach((file) => { - if (importedFiles.has(file)) { - return; - } + function insert(destination: Root, source: Root, set: { add: (_: ChildNode) => void }): void { + if (!options.prepend) { + source.each((node) => { + destination.append(node); + set.add(node); + }); - const newCSS = parseImport(root, postcssHelpers, file, importedFiles); - if (!newCSS) { - return; - } + return; + } + + const nodes = Array.from(source.nodes); + nodes.reverse(); - newCSS.each((node) => { - root.append(node); - importedCSS.add(node); + nodes.forEach((node) => { + destination.prepend(node); + set.add(node); + }); + } + + if (!options.lateRemover) { + return { + postcssPlugin: pluginName, + prepare(): Plugin { + const importedCSS = new Set(); + const importedFiles = new Set(); + + return { + postcssPlugin: pluginName, + Once(root, postcssHelpers): void { + options.files.forEach((file) => { + const newCSS = parseImport(root, postcssHelpers, file, importedFiles); + if (!newCSS) { + return; + } + + insert(root, newCSS, importedCSS); }); - }); + }, + OnceExit(): void { + importedCSS.forEach((node) => { + node.remove(); + }); + + importedFiles.clear(); + importedCSS.clear(); + }, + }; + }, + } + } + + const importedCSS = new WeakSet(); + + return { + postcssPlugin: pluginName, + plugins: [ + { + postcssPlugin: pluginName, + prepare(): Plugin { + const importedFiles = new Set(); + + return { + postcssPlugin: pluginName, + Once(root, postcssHelpers): void { + options.files.forEach((file) => { + const newCSS = parseImport(root, postcssHelpers, file, importedFiles); + if (!newCSS) { + return; + } + + insert(root, newCSS, importedCSS); + }); + }, + OnceExit(): void { + importedFiles.clear(); + }, + }; }, - OnceExit(): void { - importedCSS.forEach((node) => { - node.remove(); + }, + { + postcssPlugin: pluginName + '/late-remover', + OnceExit(root): void { + root.each((node) => { + if (importedCSS.has(node)) { + node.remove(); + } }); - importedCSS = new Set(); - importedFiles = new Set(); }, - }; - }, - }; + } + ] + } }; creator.postcss = true; diff --git a/plugins/postcss-global-data/test/_tape.mjs b/plugins/postcss-global-data/test/_tape.mjs index 752e911973..5cb67a84e0 100644 --- a/plugins/postcss-global-data/test/_tape.mjs +++ b/plugins/postcss-global-data/test/_tape.mjs @@ -15,6 +15,122 @@ postcssTape(plugin)({ postcssCustomMedia(), ], }, + 'late-remover': { + message: 'supports late removal', + plugins: (() => { + const [early, late] = plugin({ + files: [ + './test/fixtures/fixture.css', + ], + lateRemover: true, + }).plugins; + + const somethingOnOnceExit = () => { + return { + postcssPlugin: 'something-on-once-exit', + OnceExit: (root) => { + root.walkRules((rule) => { + rule.cloneBefore({ selector: '.bar' }); + }); + }, + }; + }; + + somethingOnOnceExit.postcss = true; + + return [ + early, + somethingOnOnceExit, + late, + ]; + })(), + }, + 'basic:append': { + message: 'append', + plugins: (() => { + const lastColor = () => { + return { + postcssPlugin: 'last-color', + Declaration: (decl) => { + if (decl.prop !== 'background' || decl.parent.selector !== '.foo') { + return; + } + + decl.parent.cloneBefore({ selector: '.bar' }); + }, + }; + }; + + lastColor.postcss = true; + + return [ + plugin({ + files: [ + './test/fixtures/fixture.css', + ], + prepend: false, + }), + lastColor, + ]; + })(), + }, + 'basic:append-implicit': { + message: 'append (implicit)', + plugins: (() => { + const lastColor = () => { + return { + postcssPlugin: 'last-color', + Declaration: (decl) => { + if (decl.prop !== 'background' || decl.parent.selector !== '.foo') { + return; + } + + decl.parent.cloneBefore({ selector: '.bar' }); + }, + }; + }; + + lastColor.postcss = true; + + return [ + plugin({ + files: [ + './test/fixtures/fixture.css', + ], + }), + lastColor, + ]; + })(), + }, + 'basic:prepend': { + message: 'prepend', + plugins: (() => { + const lastColor = () => { + return { + postcssPlugin: 'last-color', + Declaration: (decl) => { + if (decl.prop !== 'background' || decl.parent.selector !== '.foo') { + return; + } + + decl.parent.cloneBefore({ selector: '.bar' }); + }, + }; + }; + + lastColor.postcss = true; + + return [ + plugin({ + files: [ + './test/fixtures/fixture.css', + ], + prepend: true, + }), + lastColor, + ]; + })(), + }, 'open-props': { message: 'supports open-props usage', plugins: [ diff --git a/plugins/postcss-global-data/test/basic.append-implicit.expect.css b/plugins/postcss-global-data/test/basic.append-implicit.expect.css new file mode 100644 index 0000000000..1bc987c172 --- /dev/null +++ b/plugins/postcss-global-data/test/basic.append-implicit.expect.css @@ -0,0 +1,23 @@ +@media (--mq-a) { + body { + order: 1; + } +} + +@media not all and (--mq-a) { + body { + order: 1; + } +} + +.bar { + background: green; +} + +.foo { + background: green; +} + +.bar { + background: red; +} diff --git a/plugins/postcss-global-data/test/basic.append.expect.css b/plugins/postcss-global-data/test/basic.append.expect.css new file mode 100644 index 0000000000..1bc987c172 --- /dev/null +++ b/plugins/postcss-global-data/test/basic.append.expect.css @@ -0,0 +1,23 @@ +@media (--mq-a) { + body { + order: 1; + } +} + +@media not all and (--mq-a) { + body { + order: 1; + } +} + +.bar { + background: green; +} + +.foo { + background: green; +} + +.bar { + background: red; +} diff --git a/plugins/postcss-global-data/test/basic.prepend.expect.css b/plugins/postcss-global-data/test/basic.prepend.expect.css new file mode 100644 index 0000000000..c610b4b05a --- /dev/null +++ b/plugins/postcss-global-data/test/basic.prepend.expect.css @@ -0,0 +1,23 @@ +.bar { + background: red; +} + +@media (--mq-a) { + body { + order: 1; + } +} + +@media not all and (--mq-a) { + body { + order: 1; + } +} + +.bar { + background: green; +} + +.foo { + background: green; +} diff --git a/plugins/postcss-global-data/test/fixtures/fixture.css b/plugins/postcss-global-data/test/fixtures/fixture.css index 11fc27f6a4..9cd62473e9 100644 --- a/plugins/postcss-global-data/test/fixtures/fixture.css +++ b/plugins/postcss-global-data/test/fixtures/fixture.css @@ -1,5 +1,5 @@ @custom-media --mq-a (max-width: 30em); .foo { - background-color: red; + background: red; } diff --git a/plugins/postcss-global-data/test/late-remover.css b/plugins/postcss-global-data/test/late-remover.css new file mode 100644 index 0000000000..cc1f61908e --- /dev/null +++ b/plugins/postcss-global-data/test/late-remover.css @@ -0,0 +1 @@ +/* expect .bar */ diff --git a/plugins/postcss-global-data/test/late-remover.expect.css b/plugins/postcss-global-data/test/late-remover.expect.css new file mode 100644 index 0000000000..b17a3f3d50 --- /dev/null +++ b/plugins/postcss-global-data/test/late-remover.expect.css @@ -0,0 +1,3 @@ +/* expect .bar */.bar { + background: red; +}