From f0efd4a25e5d5206504669d1de0213454f30a177 Mon Sep 17 00:00:00 2001 From: Romain Menke <11521496+romainmenke@users.noreply.github.com> Date: Thu, 24 Aug 2023 15:33:03 +0200 Subject: [PATCH] `postcss-bundler` (#1090) --- .github/ISSUE_TEMPLATE/css-issue.yml | 1 + .github/ISSUE_TEMPLATE/plugin-issue.yml | 1 + .github/labeler.yml | 3 + package-lock.json | 49 ++++ plugin-packs/postcss-bundler/.gitignore | 6 + plugin-packs/postcss-bundler/.nvmrc | 1 + plugin-packs/postcss-bundler/.tape.mjs | 21 ++ plugin-packs/postcss-bundler/CHANGELOG.md | 47 ++++ plugin-packs/postcss-bundler/INSTALL.md | 235 ++++++++++++++++++ plugin-packs/postcss-bundler/LICENSE.md | 18 ++ plugin-packs/postcss-bundler/README.md | 83 +++++++ plugin-packs/postcss-bundler/dist/index.cjs | 1 + plugin-packs/postcss-bundler/dist/index.d.ts | 5 + plugin-packs/postcss-bundler/dist/index.mjs | 1 + .../dist/postcss-import/index.d.ts | 5 + .../postcss-import/lib/apply-conditions.d.ts | 3 + .../dist/postcss-import/lib/apply-styles.d.ts | 3 + .../dist/postcss-import/lib/conditions.d.ts | 5 + .../dist/postcss-import/lib/data-url.d.ts | 2 + .../lib/format-import-prelude.d.ts | 1 + .../dist/postcss-import/lib/load-content.d.ts | 1 + .../dist/postcss-import/lib/names.d.ts | 6 + .../dist/postcss-import/lib/noop-plugin.d.ts | 8 + .../postcss-import/lib/parse-at-import.d.ts | 7 + .../postcss-import/lib/parse-statements.d.ts | 4 + .../dist/postcss-import/lib/parse-styles.d.ts | 4 + .../dist/postcss-import/lib/resolve-id.d.ts | 2 + .../dist/postcss-import/lib/statement.d.ts | 34 +++ plugin-packs/postcss-bundler/docs/README.md | 57 +++++ plugin-packs/postcss-bundler/package.json | 89 +++++++ plugin-packs/postcss-bundler/src/index.ts | 20 ++ .../src/postcss-import/index.ts | 30 +++ .../postcss-import/lib/apply-conditions.ts | 127 ++++++++++ .../src/postcss-import/lib/apply-styles.ts | 19 ++ .../src/postcss-import/lib/conditions.ts | 5 + .../src/postcss-import/lib/data-url.ts | 22 ++ .../lib/format-import-prelude.ts | 26 ++ .../src/postcss-import/lib/load-content.ts | 10 + .../src/postcss-import/lib/names.ts | 6 + .../src/postcss-import/lib/noop-plugin.ts | 13 + .../src/postcss-import/lib/parse-at-import.ts | 164 ++++++++++++ .../postcss-import/lib/parse-statements.ts | 159 ++++++++++++ .../src/postcss-import/lib/parse-styles.ts | 196 +++++++++++++++ .../src/postcss-import/lib/resolve-id.ts | 22 ++ .../src/postcss-import/lib/statement.ts | 56 +++++ plugin-packs/postcss-bundler/test/_import.mjs | 6 + .../postcss-bundler/test/_require.cjs | 6 + plugin-packs/postcss-bundler/test/basic.css | 4 + .../postcss-bundler/test/basic.expect.css | 47 ++++ .../postcss-bundler/test/does-not-exist.css | 1 + .../test/does-not-exist.expect.css | 1 + .../postcss-bundler/test/examples/example.css | 2 + .../test/examples/example.expect.css | 6 + .../test/examples/imports/basic.css | 3 + .../postcss-bundler/test/images/green.png | Bin 0 -> 107 bytes .../postcss-bundler/test/imports/basic.css | 19 ++ .../postcss-bundler/test/imports/green.png | Bin 0 -> 107 bytes .../test/imports/minified-source.css | 3 + .../postcss-bundler/test/leading-slash.css | 1 + .../test/leading-slash.expect.css | 1 + plugin-packs/postcss-bundler/tsconfig.json | 10 + rollup/configs/externals.mjs | 4 + 62 files changed, 1692 insertions(+) create mode 100644 plugin-packs/postcss-bundler/.gitignore create mode 100644 plugin-packs/postcss-bundler/.nvmrc create mode 100644 plugin-packs/postcss-bundler/.tape.mjs create mode 100644 plugin-packs/postcss-bundler/CHANGELOG.md create mode 100644 plugin-packs/postcss-bundler/INSTALL.md create mode 100644 plugin-packs/postcss-bundler/LICENSE.md create mode 100644 plugin-packs/postcss-bundler/README.md create mode 100644 plugin-packs/postcss-bundler/dist/index.cjs create mode 100644 plugin-packs/postcss-bundler/dist/index.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/index.mjs create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/index.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/apply-conditions.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/apply-styles.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/conditions.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/data-url.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/format-import-prelude.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/load-content.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/names.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/noop-plugin.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/parse-at-import.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/parse-statements.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/parse-styles.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/resolve-id.d.ts create mode 100644 plugin-packs/postcss-bundler/dist/postcss-import/lib/statement.d.ts create mode 100644 plugin-packs/postcss-bundler/docs/README.md create mode 100644 plugin-packs/postcss-bundler/package.json create mode 100644 plugin-packs/postcss-bundler/src/index.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/index.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/apply-conditions.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/apply-styles.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/conditions.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/data-url.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/format-import-prelude.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/load-content.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/names.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/noop-plugin.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/parse-at-import.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/parse-statements.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/parse-styles.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/resolve-id.ts create mode 100644 plugin-packs/postcss-bundler/src/postcss-import/lib/statement.ts create mode 100644 plugin-packs/postcss-bundler/test/_import.mjs create mode 100644 plugin-packs/postcss-bundler/test/_require.cjs create mode 100644 plugin-packs/postcss-bundler/test/basic.css create mode 100644 plugin-packs/postcss-bundler/test/basic.expect.css create mode 100644 plugin-packs/postcss-bundler/test/does-not-exist.css create mode 100644 plugin-packs/postcss-bundler/test/does-not-exist.expect.css create mode 100644 plugin-packs/postcss-bundler/test/examples/example.css create mode 100644 plugin-packs/postcss-bundler/test/examples/example.expect.css create mode 100644 plugin-packs/postcss-bundler/test/examples/imports/basic.css create mode 100644 plugin-packs/postcss-bundler/test/images/green.png create mode 100644 plugin-packs/postcss-bundler/test/imports/basic.css create mode 100644 plugin-packs/postcss-bundler/test/imports/green.png create mode 100644 plugin-packs/postcss-bundler/test/imports/minified-source.css create mode 100644 plugin-packs/postcss-bundler/test/leading-slash.css create mode 100644 plugin-packs/postcss-bundler/test/leading-slash.expect.css create mode 100644 plugin-packs/postcss-bundler/tsconfig.json diff --git a/.github/ISSUE_TEMPLATE/css-issue.yml b/.github/ISSUE_TEMPLATE/css-issue.yml index c1670cf14..6700d9358 100644 --- a/.github/ISSUE_TEMPLATE/css-issue.yml +++ b/.github/ISSUE_TEMPLATE/css-issue.yml @@ -59,6 +59,7 @@ body: multiple: true options: - PostCSS Preset Env + - PostCSS Bundler - CSS All Property - CSS Blank Pseudo - CSS Has Pseudo diff --git a/.github/ISSUE_TEMPLATE/plugin-issue.yml b/.github/ISSUE_TEMPLATE/plugin-issue.yml index 10d29080b..5dc15842c 100644 --- a/.github/ISSUE_TEMPLATE/plugin-issue.yml +++ b/.github/ISSUE_TEMPLATE/plugin-issue.yml @@ -61,6 +61,7 @@ body: multiple: true options: - PostCSS Preset Env + - PostCSS Bundler - CSS All Property - CSS Blank Pseudo - CSS Has Pseudo diff --git a/.github/labeler.yml b/.github/labeler.yml index 299ecd54b..872846e0a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -17,6 +17,9 @@ "plugin-packs/postcss-preset-env": - plugin-packs/postcss-preset-env/** +"plugin-packs/postcss-bundler": + - plugin-packs/postcss-bundler/** + "cli": - cli/csstools-cli/** - packages/base-cli/** diff --git a/package-lock.json b/package-lock.json index 0cace916a..17b7fce58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2057,6 +2057,10 @@ "resolved": "plugins/postcss-base-plugin", "link": true }, + "node_modules/@csstools/postcss-bundler": { + "resolved": "plugin-packs/postcss-bundler", + "link": true + }, "node_modules/@csstools/postcss-cascade-layers": { "resolved": "plugins/postcss-cascade-layers", "link": true @@ -3470,6 +3474,18 @@ "node": ">=12" } }, + "node_modules/@rmenke/css-package-conditional-3": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rmenke/css-package-conditional-3/-/css-package-conditional-3-1.0.0.tgz", + "integrity": "sha512-pzscCcvYWHGFtvXwvEq2vCP9mORwuVii2jIT2077nHr6uQlcdr8orvAVRc4WxJMRNNIW14AiomxXNGKzidDu8g==", + "dev": true + }, + "node_modules/@rmenke/css-package-main": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rmenke/css-package-main/-/css-package-main-1.0.0.tgz", + "integrity": "sha512-mWJukMz6LzLiMKdN9y1rZtgiY3W9vV6pRLUHWStlLAFYEXAP2mpnueHHcuqNIdd5/hU8sknTk0+Ohx712VQm1A==", + "dev": true + }, "node_modules/@rmenke/css-tokenizer-tests": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@rmenke/css-tokenizer-tests/-/css-tokenizer-tests-1.1.3.tgz", @@ -12553,6 +12569,38 @@ "postcss-selector-parser": "^6.0.13" } }, + "plugin-packs/postcss-bundler": { + "name": "@csstools/postcss-bundler", + "version": "0.1.0-alpha.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "2.3.1", + "@csstools/css-tokenizer": "^2.2.0", + "@csstools/postcss-rebase-url": "^0.1.0-alpha.0" + }, + "devDependencies": { + "@csstools/postcss-tape": "*", + "@rmenke/css-package-conditional-3": "^1.0.0", + "@rmenke/css-package-main": "^1.0.0", + "open-props": "^1.5.11" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "plugin-packs/postcss-preset-env": { "version": "9.1.1", "funding": [ @@ -14032,6 +14080,7 @@ } }, "plugins/postcss-rebase-url": { + "name": "@csstools/postcss-rebase-url", "version": "0.1.0-alpha.0", "funding": [ { diff --git a/plugin-packs/postcss-bundler/.gitignore b/plugin-packs/postcss-bundler/.gitignore new file mode 100644 index 000000000..e5b28db4a --- /dev/null +++ b/plugin-packs/postcss-bundler/.gitignore @@ -0,0 +1,6 @@ +node_modules +package-lock.json +yarn.lock +*.result.css +*.result.css.map +*.result.html diff --git a/plugin-packs/postcss-bundler/.nvmrc b/plugin-packs/postcss-bundler/.nvmrc new file mode 100644 index 000000000..6ed5da955 --- /dev/null +++ b/plugin-packs/postcss-bundler/.nvmrc @@ -0,0 +1 @@ +v20.2.0 diff --git a/plugin-packs/postcss-bundler/.tape.mjs b/plugin-packs/postcss-bundler/.tape.mjs new file mode 100644 index 000000000..40daa3c7f --- /dev/null +++ b/plugin-packs/postcss-bundler/.tape.mjs @@ -0,0 +1,21 @@ +import { postcssTape } from '@csstools/postcss-tape'; +import plugin from '@csstools/postcss-bundler'; + +const testCases = { + basic: { + message: "supports basic usage", + }, + 'leading-slash': { + message: "does not infer a root to resolve leading slash imports", + exception: /Failed to find \'\/imports\/basic.css\'/, + }, + 'does-not-exist': { + message: "throws on files that don't exist", + exception: /Failed to find 'imports\/does-not-exist.css'/, + }, + 'examples/example': { + message: 'minimal example', + }, +} + +postcssTape(plugin)(testCases); diff --git a/plugin-packs/postcss-bundler/CHANGELOG.md b/plugin-packs/postcss-bundler/CHANGELOG.md new file mode 100644 index 000000000..973d1b17a --- /dev/null +++ b/plugin-packs/postcss-bundler/CHANGELOG.md @@ -0,0 +1,47 @@ +# Changes to PostCSS Bundler + +### Unreleased (major) + +- Initial major version + +### 0.1.0-alpha.6 + +_August 16, 2023_ + +- Reduce filesystem access + +### 0.1.0-alpha.5 + +_August 15, 2023_ + +- Stricter parsing algorithm + +### 0.1.0-alpha.4 + +_August 15, 2023_ + +- Fix sourcemaps + +### 0.1.0-alpha.3 + +_August 14, 2023_ + +- Fix the path from which node modules are resolved + +### 0.1.0-alpha.2 + +_August 14, 2023_ + +- Use the `node_modules:` scheme to reference imports from a `node_modules` package + +### 0.1.0-alpha.1 + +_August 14, 2023_ + +- Use the `npm:` scheme to reference imports from a `node_modules` package + +### 0.1.0-alpha.0 + +_August 14, 2023_ + +- Initial version diff --git a/plugin-packs/postcss-bundler/INSTALL.md b/plugin-packs/postcss-bundler/INSTALL.md new file mode 100644 index 000000000..615d45900 --- /dev/null +++ b/plugin-packs/postcss-bundler/INSTALL.md @@ -0,0 +1,235 @@ +# Installing PostCSS Bundler + +[PostCSS Bundler] runs in all Node environments, with special instructions for: + +- [Node](#node) +- [PostCSS CLI](#postcss-cli) +- [PostCSS Load Config](#postcss-load-config) +- [Webpack](#webpack) +- [Next.js](#nextjs) +- [Gulp](#gulp) +- [Grunt](#grunt) + + + +## Node + +Add [PostCSS Bundler] to your project: + +```bash +npm install postcss @csstools/postcss-bundler --save-dev +``` + +Use it as a [PostCSS] plugin: + +```js +// commonjs +const postcss = require('postcss'); +const postcssBundler = require('@csstools/postcss-bundler'); + +postcss([ + postcssBundler(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +```js +// esm +import postcss from 'postcss'; +import postcssBundler from '@csstools/postcss-bundler'; + +postcss([ + postcssBundler(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +## PostCSS CLI + +Add [PostCSS CLI] to your project: + +```bash +npm install postcss-cli @csstools/postcss-bundler --save-dev +``` + +Use [PostCSS Bundler] in your `postcss.config.js` configuration file: + +```js +const postcssBundler = require('@csstools/postcss-bundler'); + +module.exports = { + plugins: [ + postcssBundler(/* pluginOptions */) + ] +} +``` + +## PostCSS Load Config + +If your framework/CLI supports [`postcss-load-config`](https://github.com/postcss/postcss-load-config). + +```bash +npm install @csstools/postcss-bundler --save-dev +``` + +`package.json`: + +```json +{ + "postcss": { + "plugins": { + "@csstools/postcss-bundler": {} + } + } +} +``` + +`.postcssrc.json`: + +```json +{ + "plugins": { + "@csstools/postcss-bundler": {} + } +} +``` + +_See the [README of `postcss-load-config`](https://github.com/postcss/postcss-load-config#usage) for more usage options._ + +## Webpack + +_Webpack version 5_ + +Add [PostCSS Loader] to your project: + +```bash +npm install postcss-loader @csstools/postcss-bundler --save-dev +``` + +Use [PostCSS Bundler] in your Webpack configuration: + +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + use: [ + "style-loader", + { + loader: "css-loader", + options: { importLoaders: 1 }, + }, + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: [ + // Other plugins, + [ + "@csstools/postcss-bundler", + { + // Options + }, + ], + ], + }, + }, + }, + ], + }, + ], + }, +}; +``` + +## Next.js + +Read the instructions on how to [customize the PostCSS configuration in Next.js](https://nextjs.org/docs/advanced-features/customizing-postcss-config) + +```bash +npm install @csstools/postcss-bundler --save-dev +``` + +Use [PostCSS Bundler] in your `postcss.config.json` file: + +```json +{ + "plugins": [ + "@csstools/postcss-bundler" + ] +} +``` + +```json5 +{ + "plugins": [ + [ + "@csstools/postcss-bundler", + { + // Optionally add plugin options + } + ] + ] +} +``` + +## Gulp + +Add [Gulp PostCSS] to your project: + +```bash +npm install gulp-postcss @csstools/postcss-bundler --save-dev +``` + +Use [PostCSS Bundler] in your Gulpfile: + +```js +const postcss = require('gulp-postcss'); +const postcssBundler = require('@csstools/postcss-bundler'); + +gulp.task('css', function () { + var plugins = [ + postcssBundler(/* pluginOptions */) + ]; + + return gulp.src('./src/*.css') + .pipe(postcss(plugins)) + .pipe(gulp.dest('.')); +}); +``` + +## Grunt + +Add [Grunt PostCSS] to your project: + +```bash +npm install grunt-postcss @csstools/postcss-bundler --save-dev +``` + +Use [PostCSS Bundler] in your Gruntfile: + +```js +const postcssBundler = require('@csstools/postcss-bundler'); + +grunt.loadNpmTasks('grunt-postcss'); + +grunt.initConfig({ + postcss: { + options: { + processors: [ + postcssBundler(/* pluginOptions */) + ] + }, + dist: { + src: '*.css' + } + } +}); +``` + +[Gulp PostCSS]: https://github.com/postcss/gulp-postcss +[Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss +[PostCSS]: https://github.com/postcss/postcss +[PostCSS CLI]: https://github.com/postcss/postcss-cli +[PostCSS Loader]: https://github.com/postcss/postcss-loader +[PostCSS Bundler]: https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-bundler +[Next.js]: https://nextjs.org diff --git a/plugin-packs/postcss-bundler/LICENSE.md b/plugin-packs/postcss-bundler/LICENSE.md new file mode 100644 index 000000000..e8ae93b9f --- /dev/null +++ b/plugin-packs/postcss-bundler/LICENSE.md @@ -0,0 +1,18 @@ +MIT No Attribution (MIT-0) + +Copyright © CSSTools Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugin-packs/postcss-bundler/README.md b/plugin-packs/postcss-bundler/README.md new file mode 100644 index 000000000..a922b50d4 --- /dev/null +++ b/plugin-packs/postcss-bundler/README.md @@ -0,0 +1,83 @@ +# PostCSS Bundler [PostCSS Logo][PostCSS] + +[npm version][npm-url] [Build Status][cli-url] [Discord][discord] + +[PostCSS Bundler] bundles your CSS without changing the way you write CSS. + +This plugin pack contains : +- a bundler based on standard CSS `@import` statements. +- a rebaser that rewrites URLs in your CSS. + +Goal and focus : +- if your CSS works without bundling it **should** work with [PostCSS Bundler] +- if your CSS works as a bundle it **must** work without bundling + +`examples/example.css` : +```pcss +@import url("imports/basic.css"); +@import url("node_modules:open-props/red"); +``` + +`examples/imports/basic.css`: +```pcss +.foo { + background: url('../../images/green.png'); +} +``` + +when bundled : +```pcss +/* imports/basic.css */ +.foo { + background: url("../images/green.png"); +} +/* node_modules:open-props/red */ +:where(html){--red-0:#fff5f5;--red-1:#ffe3e3;--red-2:#ffc9c9;--red-3:#ffa8a8;--red-4:#ff8787;--red-5:#ff6b6b;--red-6:#fa5252;--red-7:#f03e3e;--red-8:#e03131;--red-9:#c92a2a;--red-10:#b02525;--red-11:#962020;--red-12:#7d1a1a} +``` + +## Usage + +Add [PostCSS Bundler] to your project: + +```bash +npm install postcss @csstools/postcss-bundler --save-dev +``` + +Use it as a [PostCSS] plugin: + +```js +const postcss = require('postcss'); +const postcssBundler = require('@csstools/postcss-bundler'); + +postcss([ + postcssBundler(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +[PostCSS Bundler] runs in all Node environments, with special +instructions for: + +- [Node](INSTALL.md#node) +- [PostCSS CLI](INSTALL.md#postcss-cli) +- [PostCSS Load Config](INSTALL.md#postcss-load-config) +- [Webpack](INSTALL.md#webpack) +- [Next.js](INSTALL.md#nextjs) +- [Gulp](INSTALL.md#gulp) +- [Grunt](INSTALL.md#grunt) + +## `postcss-import` + +[`postcss-import`](https://github.com/postcss/postcss-import) is also a CSS bundler and parts of [PostCSS Bundler] are based on it. +While creating this plugin we also submitted patches to [`postcss-import`](https://github.com/postcss/postcss-import) where possible. + +[PostCSS Bundler] is tuned differently and lacks configuration options that are present in [`postcss-import`](https://github.com/postcss/postcss-import). + +[PostCSS Bundler] is intended to just work and to be a drop-in replacement for native CSS `@import` statements. + +[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test + +[discord]: https://discord.gg/bUadyRwkJS +[npm-url]: https://www.npmjs.com/package/@csstools/postcss-bundler + +[PostCSS]: https://github.com/postcss/postcss +[PostCSS Bundler]: https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-bundler diff --git a/plugin-packs/postcss-bundler/dist/index.cjs b/plugin-packs/postcss-bundler/dist/index.cjs new file mode 100644 index 000000000..f3a11e940 --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/index.cjs @@ -0,0 +1 @@ +"use strict";var e=require("path"),t=require("@csstools/css-parser-algorithms"),n=require("@csstools/css-tokenizer"),s=require("module"),r=require("fs/promises"),o=require("@csstools/postcss-rebase-url");function isWarning(e){return"warning"===e.type}function isCharsetStatement(e){return"charset"===e.type}function isImportStatement(e){return"import"===e.type}const i=/^data:text\/css(?:;(base64|plain))?,/i,a=/^data:text\/css;base64,/i,p=/^data:text\/css;plain,/i;function isValidDataURL(e){return!!e&&i.test(e)}const l=/^charset$/i,u=/^import$/i,c=/^url$/i,d=/^layer$/i,m=/^supports$/i;function parseAtImport(e){const s=t.parseListOfComponentValues(n.tokenize({css:e}));let r="",o="";const i=[],a=[],p=[];e:for(let e=0;e0||p.length>0)return!1;i.push("");continue}if(t.isFunctionNode(l)&&d.test(l.getName())){if(i.length>0||p.length>0)return!1;i.push(t.stringify([trim(l.value)]));continue}if(t.isFunctionNode(l)&&m.test(l.getName())){if(p.length>0)return!1;p.push(t.stringify([trim(l.value)]));continue}const u=trim(s.slice(e)).flatMap((e=>e.tokens())),f=t.parseCommaSeparatedListOfComponentValues(u).map((e=>t.stringify([trim(e)])));a.push(...f);break}return r=stripHash(r),!!r&&{uri:r,fullUri:o,layer:i,media:a,supports:p}}function trim(e){let n=0,s=e.length;for(let s=0;s=0;n--){const r=e[n];if(!t.isWhitespaceNode(r)&&!t.isCommentNode(r)){s=n+1;break}}return e.slice(n,s)}function stripHash(e){try{const t=new URL(e,"http://example.com");return t.hash?e.slice(0,e.length-t.hash.length):e}catch{return e}}function parseStatements(e,t,n,s,r){const o=[];if("document"===t.type)return t.each((t=>{o.push(...parseStatements(e,t,n,s,r))})),o;let i=[];return t.each((t=>{let a;"atrule"===t.type&&(u.test(t.name)?a=parseImport(e,t,n,s,r):l.test(t.name)&&(a=parseCharset(e,t,n,s,r))),a?(i.length&&(o.push({type:"nodes",nodes:i,conditions:[...s],from:r,importingNode:n}),i=[]),o.push(a)):i.push(t)})),i.length&&o.push({type:"nodes",nodes:i,conditions:[...s],from:r,importingNode:n}),o}function parseCharset(e,t,n,s,r){return t.prev()?e.warn("@charset must precede all other statements",{node:t}):{type:"charset",node:t,conditions:[...s],from:r,importingNode:n}}function parseImport(e,t,n,s,r){let o=t.prev();for(;o;)if("comment"!==o.type){if("atrule"!==o.type||!u.test(o.name))break;o=o.prev()}else o=o.prev();for(;o;)if("comment"!==o.type)if("atrule"===o.type&&l.test(o.name))o=o.prev();else{if("atrule"!==o.type||!d.test(o.name)||o.nodes)return e.warn("@import must precede all other statements (besides @charset or empty @layer)",{node:t});o=o.prev()}else o=o.prev();if(t.nodes)return e.warn("It looks like you didn't end your @import statement correctly. Child nodes are attached to it.",{node:t});const i={type:"import",uri:"",fullUri:"",node:t,conditions:[...s],from:r,importingNode:n},a=parseAtImport(t.params);if(!a)return e.warn(`Invalid @import statement in '${t.toString()}'`,{node:t});const{layer:p,media:c,supports:m,uri:f,fullUri:h}=a;return i.uri=f,i.fullUri=h,(c.length>0||p.length>0||m.length>0)&&i.conditions.push({layer:p,media:c,supports:m}),i}function resolveId(t,n,r){let o="";if(t.startsWith("node_modules:"))try{o=s.createRequire(n).resolve(t.slice(13))}catch(e){throw r.error(`Failed to find '${t}'`)}else o=e.resolve(n,t);return o}async function loadContent(e){return isValidDataURL(e)?(t=e,a.test(t)?Buffer.from(t.slice(21),"base64").toString():p.test(t)?decodeURIComponent(t.slice(20)):decodeURIComponent(t.slice(14))):r.readFile(e,"utf-8");var t}const noopPlugin=()=>({postcssPlugin:"noop-plugin",Once(){}});async function parseStyles(e,t,n,s,r,o){const i=parseStatements(e,t,n,s,r);for(const t of i)isImportStatement(t)&&isProcessableURL(t.uri)&&await resolveImportId(e,t,o);let a=null;const p=[],l=[];function handleCharset(e){if(a){if(e.node.params.toLowerCase()!==a.node.params.toLowerCase()){var t,n;throw e.node.error(`Incompatible @charset statements:\n ${e.node.params} specified in ${null==(t=e.node.source)?void 0:t.input.file}\n ${a.node.params} specified in ${null==(n=a.node.source)?void 0:n.input.file}`)}}else a=e}return i.forEach((e=>{isCharsetStatement(e)?handleCharset(e):isImportStatement(e)?e.children?e.children.forEach((e=>{isImportStatement(e)?p.push(e):isCharsetStatement(e)?handleCharset(e):l.push(e)})):p.push(e):"nodes"===e.type&&l.push(e)})),a?[a,...p.concat(l)]:p.concat(l)}async function resolveImportId(t,n,s){var r;if(isValidDataURL(n.uri))return void(n.children=await loadImportContent(t,n,n.uri,s));if(isValidDataURL(n.from[n.from.length-1]))return n.children=[],void t.warn(`Unable to import '${n.uri}' from a stylesheet that is embedded in a data url`,{node:n.node});const o=n.node;let i;if(null==(r=o.source)||null==(r=r.input)||!r.file)return n.children=[],void t.warn("The current PostCSS AST Node is lacking a source file reference. This is most likely a bug in a PostCSS plugin.",{node:n.node});i=o.source.input.file;const a=e.dirname(i),p=resolveId(n.uri,a,o);t.messages.push({type:"dependency",plugin:"postcss-bundler",resolved:p,parent:i});const l=await loadImportContent(t,n,p,s);n.children=l??[]}async function loadImportContent(e,t,n,s){var r,o;const{conditions:i,from:a,node:p}=t;if(a.includes(n))return;let u;try{u=await loadContent(n)}catch{throw p.error(`Failed to find '${t.uri}'`)}const c=await s([noopPlugin()]).process(u,{from:n,parser:(null==(r=e.opts.syntax)?void 0:r.parse)??e.opts.parser??void 0}),d=c.root;return e.messages=e.messages.concat(c.messages),"atrule"===(null==(o=d.first)?void 0:o.type)&&l.test(d.first.name)?d.first.after(s.comment({text:`${t.uri}`,source:p.source})):d.prepend(s.comment({text:`${t.uri}`,source:p.source})),parseStyles(e,d,p,i,[...a,n],s)}function isProcessableURL(e){if(/^(?:[a-z]+:)?\/\//i.test(e))return!1;try{if(new URL(e,"https://example.com").search)return!1}catch{}return!0}function formatImportPrelude(e,t,n){const s=[];if(e.length){const t=e.join(".");let n="layer";t&&(n=`layer(${t})`),s.push(n)}return 1===n.length?s.push(`supports(${n[0]})`):n.length>0&&s.push(`supports(${n.map((e=>`(${e})`)).join(" and ")})`),t.length&&s.push(t.join(", ")),s.join(" ")}function applyConditions(e,t){e.forEach(((n,s)=>{if(isWarning(n))return;if(!n.conditions.length||isCharsetStatement(n))return;if(isImportStatement(n)){if(1===n.conditions.length)n.node.params=`${n.fullUri} ${formatImportPrelude(n.conditions[0].layer,n.conditions[0].media,n.conditions[0].supports)}`;else if(n.conditions.length>1){const e=n.conditions.slice().reverse(),t=e.pop();let s=`${n.fullUri} ${formatImportPrelude(t.layer,t.media,t.supports)}`;for(const t of e)s=`'data:text/css;base64,${Buffer.from(`@import ${s}`).toString("base64")}' ${formatImportPrelude(t.layer,t.media,t.supports)}`;n.node.params=s}return}const{nodes:r}=n;if(!r.length)return;const{parent:o}=r[0];if(!o)return;const i=[];for(const e of n.conditions){if(e.media.length>0){var a;const s=t({name:"media",params:e.media.join(", "),source:(null==(a=n.importingNode)?void 0:a.source)??o.source});i.push(s)}if(e.supports.length>0){var p;const s=t({name:"supports",params:1===e.supports.length?`(${e.supports[0]})`:e.supports.map((e=>`(${e})`)).join(" and "),source:(null==(p=n.importingNode)?void 0:p.source)??o.source});i.push(s)}if(e.layer.length>0){var l;const s=t({name:"layer",params:e.layer.join("."),source:(null==(l=n.importingNode)?void 0:l.source)??o.source});i.push(s)}}const u=i[0];if(!u)return;for(let e=0;e{e.parent=void 0})),r[0].raws.before=r[0].raws.before||"\n",c.append(r),e[s]={type:"nodes",nodes:[u],conditions:n.conditions,from:n.from,importingNode:n.importingNode}}))}function applyStyles(e,t){t.nodes=[],e.forEach((e=>{isCharsetStatement(e)||isImportStatement(e)?(e.node.parent=void 0,t.append(e.node)):"nodes"===e.type&&e.nodes.forEach((e=>{e.parent=void 0,t.append(e)}))}))}noopPlugin.postcss=!0;const creator$1=()=>({postcssPlugin:"postcss-bundler",async Once(e,{result:t,atRule:n,postcss:s}){const r=await parseStyles(t,e,null,[],[],s);applyConditions(r,n),applyStyles(r,e)}});creator$1.postcss=!0;const creator=()=>({postcssPlugin:"postcss-bundler",plugins:[creator$1(),o()]});creator.postcss=!0,module.exports=creator; diff --git a/plugin-packs/postcss-bundler/dist/index.d.ts b/plugin-packs/postcss-bundler/dist/index.d.ts new file mode 100644 index 000000000..4ea42dfa5 --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/index.d.ts @@ -0,0 +1,5 @@ +import type { PluginCreator } from 'postcss'; +/** postcss-bundler plugin options */ +export type pluginOptions = never; +declare const creator: PluginCreator; +export default creator; diff --git a/plugin-packs/postcss-bundler/dist/index.mjs b/plugin-packs/postcss-bundler/dist/index.mjs new file mode 100644 index 000000000..8f62ff449 --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/index.mjs @@ -0,0 +1 @@ +import e from"path";import{parseListOfComponentValues as t,isWhitespaceNode as n,isCommentNode as r,isTokenNode as o,isFunctionNode as s,stringify as i,parseCommaSeparatedListOfComponentValues as a}from"@csstools/css-parser-algorithms";import{tokenize as l,TokenType as p}from"@csstools/css-tokenizer";import u from"module";import c from"fs/promises";import d from"@csstools/postcss-rebase-url";function isWarning(e){return"warning"===e.type}function isCharsetStatement(e){return"charset"===e.type}function isImportStatement(e){return"import"===e.type}const m=/^data:text\/css(?:;(base64|plain))?,/i,f=/^data:text\/css;base64,/i,h=/^data:text\/css;plain,/i;function isValidDataURL(e){return!!e&&m.test(e)}const g=/^charset$/i,y=/^import$/i,v=/^url$/i,I=/^layer$/i,S=/^supports$/i;function parseAtImport(e){const u=t(l({css:e}));let c="",d="";const m=[],f=[],h=[];e:for(let e=0;e0||h.length>0)return!1;m.push("");continue}if(s(t)&&I.test(t.getName())){if(m.length>0||h.length>0)return!1;m.push(i([trim(t.value)]));continue}if(s(t)&&S.test(t.getName())){if(h.length>0)return!1;h.push(i([trim(t.value)]));continue}const l=trim(u.slice(e)).flatMap((e=>e.tokens())),g=a(l).map((e=>i([trim(e)])));f.push(...g);break}return c=stripHash(c),!!c&&{uri:c,fullUri:d,layer:m,media:f,supports:h}}function trim(e){let t=0,o=e.length;for(let o=0;o=0;t--){const s=e[t];if(!n(s)&&!r(s)){o=t+1;break}}return e.slice(t,o)}function stripHash(e){try{const t=new URL(e,"http://example.com");return t.hash?e.slice(0,e.length-t.hash.length):e}catch{return e}}function parseStatements(e,t,n,r,o){const s=[];if("document"===t.type)return t.each((t=>{s.push(...parseStatements(e,t,n,r,o))})),s;let i=[];return t.each((t=>{let a;"atrule"===t.type&&(y.test(t.name)?a=parseImport(e,t,n,r,o):g.test(t.name)&&(a=parseCharset(e,t,n,r,o))),a?(i.length&&(s.push({type:"nodes",nodes:i,conditions:[...r],from:o,importingNode:n}),i=[]),s.push(a)):i.push(t)})),i.length&&s.push({type:"nodes",nodes:i,conditions:[...r],from:o,importingNode:n}),s}function parseCharset(e,t,n,r,o){return t.prev()?e.warn("@charset must precede all other statements",{node:t}):{type:"charset",node:t,conditions:[...r],from:o,importingNode:n}}function parseImport(e,t,n,r,o){let s=t.prev();for(;s;)if("comment"!==s.type){if("atrule"!==s.type||!y.test(s.name))break;s=s.prev()}else s=s.prev();for(;s;)if("comment"!==s.type)if("atrule"===s.type&&g.test(s.name))s=s.prev();else{if("atrule"!==s.type||!I.test(s.name)||s.nodes)return e.warn("@import must precede all other statements (besides @charset or empty @layer)",{node:t});s=s.prev()}else s=s.prev();if(t.nodes)return e.warn("It looks like you didn't end your @import statement correctly. Child nodes are attached to it.",{node:t});const i={type:"import",uri:"",fullUri:"",node:t,conditions:[...r],from:o,importingNode:n},a=parseAtImport(t.params);if(!a)return e.warn(`Invalid @import statement in '${t.toString()}'`,{node:t});const{layer:l,media:p,supports:u,uri:c,fullUri:d}=a;return i.uri=c,i.fullUri=d,(p.length>0||l.length>0||u.length>0)&&i.conditions.push({layer:l,media:p,supports:u}),i}function resolveId(t,n,r){let o="";if(t.startsWith("node_modules:"))try{o=u.createRequire(n).resolve(t.slice(13))}catch(e){throw r.error(`Failed to find '${t}'`)}else o=e.resolve(n,t);return o}async function loadContent(e){return isValidDataURL(e)?(t=e,f.test(t)?Buffer.from(t.slice(21),"base64").toString():h.test(t)?decodeURIComponent(t.slice(20)):decodeURIComponent(t.slice(14))):c.readFile(e,"utf-8");var t}const noopPlugin=()=>({postcssPlugin:"noop-plugin",Once(){}});async function parseStyles(e,t,n,r,o,s){const i=parseStatements(e,t,n,r,o);for(const t of i)isImportStatement(t)&&isProcessableURL(t.uri)&&await resolveImportId(e,t,s);let a=null;const l=[],p=[];function handleCharset(e){if(a){if(e.node.params.toLowerCase()!==a.node.params.toLowerCase()){var t,n;throw e.node.error(`Incompatible @charset statements:\n ${e.node.params} specified in ${null==(t=e.node.source)?void 0:t.input.file}\n ${a.node.params} specified in ${null==(n=a.node.source)?void 0:n.input.file}`)}}else a=e}return i.forEach((e=>{isCharsetStatement(e)?handleCharset(e):isImportStatement(e)?e.children?e.children.forEach((e=>{isImportStatement(e)?l.push(e):isCharsetStatement(e)?handleCharset(e):p.push(e)})):l.push(e):"nodes"===e.type&&p.push(e)})),a?[a,...l.concat(p)]:l.concat(p)}async function resolveImportId(t,n,r){var o;if(isValidDataURL(n.uri))return void(n.children=await loadImportContent(t,n,n.uri,r));if(isValidDataURL(n.from[n.from.length-1]))return n.children=[],void t.warn(`Unable to import '${n.uri}' from a stylesheet that is embedded in a data url`,{node:n.node});const s=n.node;let i;if(null==(o=s.source)||null==(o=o.input)||!o.file)return n.children=[],void t.warn("The current PostCSS AST Node is lacking a source file reference. This is most likely a bug in a PostCSS plugin.",{node:n.node});i=s.source.input.file;const a=e.dirname(i),l=resolveId(n.uri,a,s);t.messages.push({type:"dependency",plugin:"postcss-bundler",resolved:l,parent:i});const p=await loadImportContent(t,n,l,r);n.children=p??[]}async function loadImportContent(e,t,n,r){var o,s;const{conditions:i,from:a,node:l}=t;if(a.includes(n))return;let p;try{p=await loadContent(n)}catch{throw l.error(`Failed to find '${t.uri}'`)}const u=await r([noopPlugin()]).process(p,{from:n,parser:(null==(o=e.opts.syntax)?void 0:o.parse)??e.opts.parser??void 0}),c=u.root;return e.messages=e.messages.concat(u.messages),"atrule"===(null==(s=c.first)?void 0:s.type)&&g.test(c.first.name)?c.first.after(r.comment({text:`${t.uri}`,source:l.source})):c.prepend(r.comment({text:`${t.uri}`,source:l.source})),parseStyles(e,c,l,i,[...a,n],r)}function isProcessableURL(e){if(/^(?:[a-z]+:)?\/\//i.test(e))return!1;try{if(new URL(e,"https://example.com").search)return!1}catch{}return!0}function formatImportPrelude(e,t,n){const r=[];if(e.length){const t=e.join(".");let n="layer";t&&(n=`layer(${t})`),r.push(n)}return 1===n.length?r.push(`supports(${n[0]})`):n.length>0&&r.push(`supports(${n.map((e=>`(${e})`)).join(" and ")})`),t.length&&r.push(t.join(", ")),r.join(" ")}function applyConditions(e,t){e.forEach(((n,r)=>{if(isWarning(n))return;if(!n.conditions.length||isCharsetStatement(n))return;if(isImportStatement(n)){if(1===n.conditions.length)n.node.params=`${n.fullUri} ${formatImportPrelude(n.conditions[0].layer,n.conditions[0].media,n.conditions[0].supports)}`;else if(n.conditions.length>1){const e=n.conditions.slice().reverse(),t=e.pop();let r=`${n.fullUri} ${formatImportPrelude(t.layer,t.media,t.supports)}`;for(const t of e)r=`'data:text/css;base64,${Buffer.from(`@import ${r}`).toString("base64")}' ${formatImportPrelude(t.layer,t.media,t.supports)}`;n.node.params=r}return}const{nodes:o}=n;if(!o.length)return;const{parent:s}=o[0];if(!s)return;const i=[];for(const e of n.conditions){if(e.media.length>0){var a;const r=t({name:"media",params:e.media.join(", "),source:(null==(a=n.importingNode)?void 0:a.source)??s.source});i.push(r)}if(e.supports.length>0){var l;const r=t({name:"supports",params:1===e.supports.length?`(${e.supports[0]})`:e.supports.map((e=>`(${e})`)).join(" and "),source:(null==(l=n.importingNode)?void 0:l.source)??s.source});i.push(r)}if(e.layer.length>0){var p;const r=t({name:"layer",params:e.layer.join("."),source:(null==(p=n.importingNode)?void 0:p.source)??s.source});i.push(r)}}const u=i[0];if(!u)return;for(let e=0;e{e.parent=void 0})),o[0].raws.before=o[0].raws.before||"\n",c.append(o),e[r]={type:"nodes",nodes:[u],conditions:n.conditions,from:n.from,importingNode:n.importingNode}}))}function applyStyles(e,t){t.nodes=[],e.forEach((e=>{isCharsetStatement(e)||isImportStatement(e)?(e.node.parent=void 0,t.append(e.node)):"nodes"===e.type&&e.nodes.forEach((e=>{e.parent=void 0,t.append(e)}))}))}noopPlugin.postcss=!0;const creator$1=()=>({postcssPlugin:"postcss-bundler",async Once(e,{result:t,atRule:n,postcss:r}){const o=await parseStyles(t,e,null,[],[],r);applyConditions(o,n),applyStyles(o,e)}});creator$1.postcss=!0;const creator=()=>({postcssPlugin:"postcss-bundler",plugins:[creator$1(),d()]});creator.postcss=!0;export{creator as default}; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/index.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/index.d.ts new file mode 100644 index 000000000..ebde316c0 --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/index.d.ts @@ -0,0 +1,5 @@ +import type { PluginCreator } from 'postcss'; +/** postcss-import plugin options */ +export type pluginOptions = never; +declare const creator: PluginCreator; +export default creator; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/apply-conditions.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/apply-conditions.d.ts new file mode 100644 index 000000000..450069752 --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/apply-conditions.d.ts @@ -0,0 +1,3 @@ +import type { AtRule, AtRuleProps } from 'postcss'; +import { Statement } from './statement'; +export declare function applyConditions(bundle: Array, atRule: (defaults?: AtRuleProps) => AtRule): void; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/apply-styles.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/apply-styles.d.ts new file mode 100644 index 000000000..b538e915c --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/apply-styles.d.ts @@ -0,0 +1,3 @@ +import type { Document, Root } from 'postcss'; +import { Statement } from './statement'; +export declare function applyStyles(bundle: Array, styles: Root | Document): void; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/conditions.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/conditions.d.ts new file mode 100644 index 000000000..4772aadb0 --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/conditions.d.ts @@ -0,0 +1,5 @@ +export type Condition = { + layer: Array; + media: Array; + supports: Array; +}; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/data-url.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/data-url.d.ts new file mode 100644 index 000000000..54aa5ba4c --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/data-url.d.ts @@ -0,0 +1,2 @@ +export declare function isValidDataURL(url?: string): boolean; +export declare function dataURLContents(url: string): string; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/format-import-prelude.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/format-import-prelude.d.ts new file mode 100644 index 000000000..5875aad8e --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/format-import-prelude.d.ts @@ -0,0 +1 @@ +export declare function formatImportPrelude(layer: Array, media: Array, supports: Array): string; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/load-content.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/load-content.d.ts new file mode 100644 index 000000000..30257103e --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/load-content.d.ts @@ -0,0 +1 @@ +export declare function loadContent(filename: string): Promise; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/names.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/names.d.ts new file mode 100644 index 000000000..231220c98 --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/names.d.ts @@ -0,0 +1,6 @@ +export declare const IS_CHARSET: RegExp; +export declare const IS_IMPORT: RegExp; +export declare const IS_URL: RegExp; +export declare const IS_LAYER: RegExp; +export declare const IS_SUPPORTS: RegExp; +export declare const IS_MEDIA: RegExp; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/noop-plugin.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/noop-plugin.d.ts new file mode 100644 index 000000000..44b4ea068 --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/noop-plugin.d.ts @@ -0,0 +1,8 @@ +declare const noopPlugin: { + (): { + postcssPlugin: string; + Once(): void; + }; + postcss: boolean; +}; +export default noopPlugin; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/parse-at-import.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/parse-at-import.d.ts new file mode 100644 index 000000000..b6c31fb53 --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/parse-at-import.d.ts @@ -0,0 +1,7 @@ +export declare function parseAtImport(params: string): false | { + uri: string; + fullUri: string; + layer: string[]; + media: string[]; + supports: string[]; +}; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/parse-statements.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/parse-statements.d.ts new file mode 100644 index 000000000..4a2bdb2bd --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/parse-statements.d.ts @@ -0,0 +1,4 @@ +import type { AtRule, Document, Result, Root } from 'postcss'; +import { Condition } from './conditions'; +import { Statement } from './statement'; +export declare function parseStatements(result: Result, styles: Root | Document, importingNode: AtRule | null, conditions: Array, from: Array): Array; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/parse-styles.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/parse-styles.d.ts new file mode 100644 index 000000000..86beb9c50 --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/parse-styles.d.ts @@ -0,0 +1,4 @@ +import type { Document, Postcss, Result, Root, AtRule } from 'postcss'; +import { Statement } from './statement'; +import { Condition } from './conditions'; +export declare function parseStyles(result: Result, styles: Root | Document, importingNode: AtRule | null, conditions: Array, from: Array, postcss: Postcss): Promise; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/resolve-id.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/resolve-id.d.ts new file mode 100644 index 000000000..e40b3d302 --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/resolve-id.d.ts @@ -0,0 +1,2 @@ +import type { Node } from 'postcss'; +export declare function resolveId(id: string, base: string, node: Node): string; diff --git a/plugin-packs/postcss-bundler/dist/postcss-import/lib/statement.d.ts b/plugin-packs/postcss-bundler/dist/postcss-import/lib/statement.d.ts new file mode 100644 index 000000000..a5bf2629c --- /dev/null +++ b/plugin-packs/postcss-bundler/dist/postcss-import/lib/statement.d.ts @@ -0,0 +1,34 @@ +import type { AtRule, ChildNode, Warning } from 'postcss'; +import { Condition } from './conditions'; +export type Statement = ImportStatement | CharsetStatement | NodesStatement | Warning; +export type NodesStatement = { + type: string; + nodes: Array; + conditions: Array; + from: Array; + parent?: Statement; + importingNode: AtRule | null; +}; +export type CharsetStatement = { + type: string; + node: AtRule; + conditions: Array; + from: Array; + parent?: Statement; + importingNode: AtRule | null; +}; +export type ImportStatement = { + type: string; + uri: string; + fullUri: string; + node: AtRule; + conditions: Array; + from: Array; + parent?: Statement; + children?: Array; + importingNode: AtRule | null; +}; +export declare function isWarning(stmt: Statement): stmt is Warning; +export declare function isNodesStatement(stmt: Statement): stmt is NodesStatement; +export declare function isCharsetStatement(stmt: Statement): stmt is CharsetStatement; +export declare function isImportStatement(stmt: Statement): stmt is ImportStatement; diff --git a/plugin-packs/postcss-bundler/docs/README.md b/plugin-packs/postcss-bundler/docs/README.md new file mode 100644 index 000000000..bbc49adcb --- /dev/null +++ b/plugin-packs/postcss-bundler/docs/README.md @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + +
+ +[] bundles your CSS without changing the way you write CSS. + +This plugin pack contains : +- a bundler based on standard CSS `@import` statements. +- a rebaser that rewrites URLs in your CSS. + +Goal and focus : +- if your CSS works without bundling it **should** work with [] +- if your CSS works as a bundle it **must** work without bundling + +`examples/example.css` : +```pcss + +``` + +`examples/imports/basic.css`: +```pcss + +``` + +when bundled : +```pcss + +``` + + + + + +## `postcss-import` + +[`postcss-import`](https://github.com/postcss/postcss-import) is also a CSS bundler and parts of [] are based on it. +While creating this plugin we also submitted patches to [`postcss-import`](https://github.com/postcss/postcss-import) where possible. + +[] is tuned differently and lacks configuration options that are present in [`postcss-import`](https://github.com/postcss/postcss-import). + +[] is intended to just work and to be a drop-in replacement for native CSS `@import` statements. + + diff --git a/plugin-packs/postcss-bundler/package.json b/plugin-packs/postcss-bundler/package.json new file mode 100644 index 000000000..5cedb898a --- /dev/null +++ b/plugin-packs/postcss-bundler/package.json @@ -0,0 +1,89 @@ +{ + "name": "@csstools/postcss-bundler", + "description": "Bundle CSS", + "version": "0.1.0-alpha.6", + "contributors": [ + { + "name": "Antonio Laguna", + "email": "antonio@laguna.es", + "url": "https://antonio.laguna.es" + }, + { + "name": "Romain Menke", + "email": "romainmenke@gmail.com" + } + ], + "license": "MIT-0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "main": "dist/index.cjs", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs", + "default": "./dist/index.mjs" + } + }, + "files": [ + "CHANGELOG.md", + "LICENSE.md", + "README.md", + "dist" + ], + "dependencies": { + "@csstools/css-parser-algorithms": "2.3.1", + "@csstools/css-tokenizer": "^2.2.0", + "@csstools/postcss-rebase-url": "^0.1.0-alpha.0" + }, + "peerDependencies": { + "postcss": "^8.4" + }, + "devDependencies": { + "@csstools/postcss-tape": "*", + "@rmenke/css-package-conditional-3": "^1.0.0", + "@rmenke/css-package-main": "^1.0.0", + "open-props": "^1.5.11" + }, + "scripts": { + "build": "rollup -c ../../rollup/default.mjs", + "docs": "node ../../.github/bin/generate-docs/install.mjs && node ../../.github/bin/generate-docs/readme.mjs", + "lint": "node ../../.github/bin/format-package-json.mjs", + "prepublishOnly": "npm run build && npm run test", + "test": "node .tape.mjs && node ./test/_import.mjs && node ./test/_require.cjs", + "test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.mjs" + }, + "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-bundler#readme", + "repository": { + "type": "git", + "url": "https://github.com/csstools/postcss-plugins.git", + "directory": "plugins/postcss-bundler" + }, + "bugs": "https://github.com/csstools/postcss-plugins/issues", + "keywords": [ + "bundler", + "import", + "postcss-plugin", + "url" + ], + "csstools": { + "exportName": "postcssBundler", + "humanReadableName": "PostCSS Bundler" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/plugin-packs/postcss-bundler/src/index.ts b/plugin-packs/postcss-bundler/src/index.ts new file mode 100644 index 000000000..74fb0c688 --- /dev/null +++ b/plugin-packs/postcss-bundler/src/index.ts @@ -0,0 +1,20 @@ +import type { PluginCreator } from 'postcss'; +import postcssImport from './postcss-import/index'; +import postcssRebaseURL from '@csstools/postcss-rebase-url'; + +/** postcss-bundler plugin options */ +export type pluginOptions = never; + +const creator: PluginCreator = () => { + return { + postcssPlugin: 'postcss-bundler', + plugins: [ + postcssImport(), + postcssRebaseURL(), + ], + }; +}; + +creator.postcss = true; + +export default creator; diff --git a/plugin-packs/postcss-bundler/src/postcss-import/index.ts b/plugin-packs/postcss-bundler/src/postcss-import/index.ts new file mode 100644 index 000000000..931a09327 --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/index.ts @@ -0,0 +1,30 @@ +import type { PluginCreator } from 'postcss'; +import { parseStyles } from './lib/parse-styles'; +import { applyConditions } from './lib/apply-conditions'; +import { applyStyles } from './lib/apply-styles'; + +/** postcss-import plugin options */ +export type pluginOptions = never; + +const creator: PluginCreator = () => { + return { + postcssPlugin: 'postcss-bundler', + async Once(styles, { result, atRule, postcss }) { + const bundle = await parseStyles( + result, + styles, + null, + [], + [], + postcss, + ); + + applyConditions(bundle, atRule); + applyStyles(bundle, styles); + }, + }; +}; + +creator.postcss = true; + +export default creator; diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/apply-conditions.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/apply-conditions.ts new file mode 100644 index 000000000..697e064bc --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/apply-conditions.ts @@ -0,0 +1,127 @@ +import type { AtRule, AtRuleProps } from 'postcss'; +import { Statement, isCharsetStatement, isImportStatement, isWarning } from './statement'; +import { formatImportPrelude } from './format-import-prelude'; + +export function applyConditions(bundle: Array, atRule: (defaults?: AtRuleProps) => AtRule) { + bundle.forEach((stmt, index) => { + if (isWarning(stmt)) { + return; + } + + if (!stmt.conditions.length || isCharsetStatement(stmt)) { + return; + } + + if (isImportStatement(stmt)) { + if (stmt.conditions.length === 1) { + stmt.node.params = `${stmt.fullUri} ${formatImportPrelude( + stmt.conditions[0].layer, + stmt.conditions[0].media, + stmt.conditions[0].supports, + )}`; + } else if (stmt.conditions.length > 1) { + const reverseConditions = stmt.conditions.slice().reverse(); + const first = reverseConditions.pop()!; + let params = `${stmt.fullUri} ${formatImportPrelude( + first.layer, + first.media, + first.supports, + )}`; + + for (const condition of reverseConditions) { + params = `'data:text/css;base64,${Buffer.from( + `@import ${params}`, + ).toString('base64')}' ${formatImportPrelude( + condition.layer, + condition.media, + condition.supports, + )}`; + } + + stmt.node.params = params; + } + + return; + } + + const { nodes } = stmt; + if (!nodes.length) { + return; + } + + const { parent } = nodes[0]; + if (!parent) { + return; + } + + const atRules = []; + + // Convert conditions to at-rules + for (const condition of stmt.conditions) { + if (condition.media.length > 0) { + const mediaNode = atRule({ + name: 'media', + params: condition.media.join(', '), + source: stmt.importingNode?.source ?? parent.source, + }); + + atRules.push(mediaNode); + } + + if (condition.supports.length > 0) { + const supportsNode = atRule({ + name: 'supports', + params: + condition.supports.length === 1 + ? `(${condition.supports[0]})` + : condition.supports.map(x => `(${x})`).join(' and '), + source: stmt.importingNode?.source ?? parent.source, + }); + + atRules.push(supportsNode); + } + + if (condition.layer.length > 0) { + const layerNode = atRule({ + name: 'layer', + params: condition.layer.join('.'), + source: stmt.importingNode?.source ?? parent.source, + }); + + atRules.push(layerNode); + } + } + + // Add nodes to AST + const outerAtRule = atRules[0]; + if (!outerAtRule) { + return; + } + + for (let i = 0; i < atRules.length - 1; i++) { + atRules[i].append(atRules[i + 1]); + } + const innerAtRule = atRules[atRules.length - 1]; + + parent.insertBefore(nodes[0], outerAtRule); + + // remove nodes + nodes.forEach(node => { + node.parent = undefined; + }); + + // better output + nodes[0].raws.before = nodes[0].raws.before || '\n'; + + // wrap new rules with media query and/or layer at rule + innerAtRule.append(nodes); + + bundle[index] = { + type: 'nodes', + nodes: [outerAtRule], + conditions: stmt.conditions, + from: stmt.from, + importingNode: stmt.importingNode, + }; + }); +} diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/apply-styles.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/apply-styles.ts new file mode 100644 index 000000000..cee15000c --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/apply-styles.ts @@ -0,0 +1,19 @@ +import type { Document, Root } from 'postcss'; +import { Statement, isCharsetStatement, isImportStatement } from './statement'; + +export function applyStyles(bundle: Array, styles: Root | Document) { + styles.nodes = []; + + // Strip additional statements. + bundle.forEach(stmt => { + if (isCharsetStatement(stmt) || isImportStatement(stmt)) { + stmt.node.parent = undefined; + styles.append(stmt.node); + } else if (stmt.type === 'nodes') { + stmt.nodes.forEach(node => { + node.parent = undefined; + styles.append(node); + }); + } + }); +} diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/conditions.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/conditions.ts new file mode 100644 index 000000000..e74c5099a --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/conditions.ts @@ -0,0 +1,5 @@ +export type Condition = { + layer: Array + media: Array + supports: Array +} diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/data-url.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/data-url.ts new file mode 100644 index 000000000..987e0661d --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/data-url.ts @@ -0,0 +1,22 @@ +const anyDataURLRegexp = /^data:text\/css(?:;(base64|plain))?,/i; +const base64DataURLRegexp = /^data:text\/css;base64,/i; +const plainDataURLRegexp = /^data:text\/css;plain,/i; + +export function isValidDataURL(url?: string): boolean { + return !!url && anyDataURLRegexp.test(url); +} + +export function dataURLContents(url: string): string { + if (base64DataURLRegexp.test(url)) { + // "data:text/css;base64,".length === 21 + return Buffer.from(url.slice(21), 'base64').toString(); + } + + if (plainDataURLRegexp.test(url)) { + // "data:text/css;plain,".length === 20 + return decodeURIComponent(url.slice(20)); + } + + // "data:text/css,".length === 14 + return decodeURIComponent(url.slice(14)); +} diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/format-import-prelude.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/format-import-prelude.ts new file mode 100644 index 000000000..18817b44b --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/format-import-prelude.ts @@ -0,0 +1,26 @@ +export function formatImportPrelude(layer: Array, media: Array, supports: Array): string { + const parts = []; + + if (layer.length) { + const layerName = layer.join('.'); + + let layerParams = 'layer'; + if (layerName) { + layerParams = `layer(${layerName})`; + } + + parts.push(layerParams); + } + + if (supports.length === 1) { + parts.push(`supports(${supports[0]})`); + } else if (supports.length > 0) { + parts.push(`supports(${supports.map(x => `(${x})`).join(' and ')})`); + } + + if (media.length) { + parts.push(media.join(', ')); + } + + return parts.join(' '); +} diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/load-content.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/load-content.ts new file mode 100644 index 000000000..234e9013e --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/load-content.ts @@ -0,0 +1,10 @@ +import fs from 'fs/promises'; +import { dataURLContents, isValidDataURL } from './data-url'; + +export async function loadContent(filename: string) { + if (isValidDataURL(filename)) { + return dataURLContents(filename); + } + + return fs.readFile(filename, 'utf-8'); +} diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/names.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/names.ts new file mode 100644 index 000000000..9f9c37016 --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/names.ts @@ -0,0 +1,6 @@ +export const IS_CHARSET = /^charset$/i; +export const IS_IMPORT = /^import$/i; +export const IS_URL = /^url$/i; +export const IS_LAYER = /^layer$/i; +export const IS_SUPPORTS = /^supports$/i; +export const IS_MEDIA = /^media$/i; diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/noop-plugin.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/noop-plugin.ts new file mode 100644 index 000000000..b5845908e --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/noop-plugin.ts @@ -0,0 +1,13 @@ +// https://github.com/postcss/postcss/issues/1869 +const noopPlugin = () => { + return { + postcssPlugin: 'noop-plugin', + Once() { + // do nothing + }, + }; +}; + +noopPlugin.postcss = true; + +export default noopPlugin; diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/parse-at-import.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/parse-at-import.ts new file mode 100644 index 000000000..15e826eeb --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/parse-at-import.ts @@ -0,0 +1,164 @@ +import { ComponentValue, isCommentNode, isFunctionNode, isTokenNode, isWhitespaceNode, parseCommaSeparatedListOfComponentValues, parseListOfComponentValues, stringify } from '@csstools/css-parser-algorithms'; +import { TokenType, tokenize } from '@csstools/css-tokenizer'; +import { IS_LAYER, IS_SUPPORTS, IS_URL } from './names'; + +export function parseAtImport(params: string) { + const componentValues = parseListOfComponentValues( + tokenize({ css: params }), + ); + + let uri = ''; + let fullUri = ''; + const layer = []; + const media = []; + const supports = []; + + PARSING_LOOP: + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (isWhitespaceNode(componentValue) || isCommentNode(componentValue)) { + continue; + } + + if ( + isTokenNode(componentValue) && + ( + componentValue.value[0] === TokenType.String || + componentValue.value[0] === TokenType.URL + ) + ) { + if (uri) { + return false; + } + + uri = componentValue.value[4].value; + fullUri = componentValue.value[1]; + continue; + } + + if ( + isFunctionNode(componentValue) && + IS_URL.test(componentValue.getName()) + ) { + if (uri) { + return false; + } + + for (let j = 0; j < componentValue.value.length; j++) { + const childComponentValue = componentValue.value[j]; + if (isWhitespaceNode(childComponentValue) || isCommentNode(childComponentValue)) { + continue; + } + + if ( + isTokenNode(childComponentValue) && + childComponentValue.value[0] === TokenType.String + ) { + uri = childComponentValue.value[4].value; + fullUri = stringify([[componentValue]]); + continue PARSING_LOOP; + } + + return false; + } + } + + if ( + isTokenNode(componentValue) && + componentValue.value[0] === TokenType.Ident && + IS_LAYER.test(componentValue.value[4].value) + ) { + if (layer.length > 0 || supports.length > 0) { + return false; + } + + layer.push(''); + continue; + } + + if ( + isFunctionNode(componentValue) && + IS_LAYER.test(componentValue.getName()) + ) { + if (layer.length > 0 || supports.length > 0) { + return false; + } + + layer.push(stringify([trim(componentValue.value)])); + continue; + } + + if ( + isFunctionNode(componentValue) && + IS_SUPPORTS.test(componentValue.getName()) + ) { + if (supports.length > 0) { + return false; + } + + supports.push(stringify([trim(componentValue.value)])); + continue; + } + + const remainder = trim(componentValues.slice(i)); + const remainderTokens = remainder.flatMap(x => x.tokens()); + const list = parseCommaSeparatedListOfComponentValues(remainderTokens); + const serializedList = list.map((x) => stringify([trim(x)])); + media.push(...serializedList); + break; + } + + uri = stripHash(uri); + + if (!uri) { + return false; + } + + return { + uri, + fullUri, + layer, + media, + supports, + }; +} + +function trim(componentValues: Array) { + let start = 0; + let end = componentValues.length; + + for (let i = 0; i < componentValues.length; i++) { + const componentValue = componentValues[i]; + if (isWhitespaceNode(componentValue) || isCommentNode(componentValue)) { + continue; + } + + start = i; + break; + } + + for (let i = componentValues.length - 1; i >= 0; i--) { + const componentValue = componentValues[i]; + if (isWhitespaceNode(componentValue) || isCommentNode(componentValue)) { + continue; + } + + end = i + 1; + break; + } + + return componentValues.slice(start, end); +} + +function stripHash(str: string) { + try { + const url = new URL(str, 'http://example.com'); + if (!url.hash) { + return str; + } + + return str.slice(0, str.length - url.hash.length); + } catch { + return str; + } +} diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/parse-statements.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/parse-statements.ts new file mode 100644 index 000000000..5ff21a4e4 --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/parse-statements.ts @@ -0,0 +1,159 @@ +import type { AtRule, ChildNode, Document, Result, Root, Warning } from 'postcss'; +import { Condition } from './conditions'; +import { CharsetStatement, ImportStatement, Statement } from './statement'; +import { IS_CHARSET, IS_IMPORT, IS_LAYER } from './names'; +import { parseAtImport } from './parse-at-import'; + +export function parseStatements(result: Result, styles: Root | Document, importingNode: AtRule | null, conditions: Array, from: Array): Array { + const statements: Array = []; + + if (styles.type === 'document') { + styles.each((root) => { + statements.push( + ...parseStatements(result, root, importingNode, conditions, from), + ); + }); + + return statements; + } + + let nodes: Array = []; + + styles.each(node => { + let stmt; + if (node.type === 'atrule') { + if (IS_IMPORT.test(node.name)) { + stmt = parseImport(result, node, importingNode, conditions, from); + } else if (IS_CHARSET.test(node.name)) { + stmt = parseCharset(result, node, importingNode, conditions, from); + } + } + + if (stmt) { + if (nodes.length) { + statements.push({ + type: 'nodes', + nodes, + conditions: [...conditions], + from, + importingNode, + }); + nodes = []; + } + statements.push(stmt); + } else { + nodes.push(node); + } + }); + + if (nodes.length) { + statements.push({ + type: 'nodes', + nodes, + conditions: [...conditions], + from, + importingNode, + }); + } + + return statements; +} + +function parseCharset(result: Result, atRule: AtRule, importingNode: AtRule | null, conditions: Array, from: Array): Warning | CharsetStatement { + if (atRule.prev()) { + return result.warn('@charset must precede all other statements', { + node: atRule, + }); + } + + return { + type: 'charset', + node: atRule, + conditions: [...conditions], + from, + importingNode, + }; +} + +function parseImport(result: Result, atRule: AtRule, importingNode: AtRule | null, conditions: Array, from: Array): Warning | ImportStatement { + let prev = atRule.prev(); + + // `@import` statements may follow other `@import` statements. + while (prev) { + if (prev.type === 'comment') { + prev = prev.prev(); + continue; + } + + if (prev.type === 'atrule' && IS_IMPORT.test(prev.name)) { + prev = prev.prev(); + continue; + } + + break; + } + + // All `@import` statements may be preceded by `@charset` or `@layer` statements. + // But the `@import` statements must be consecutive. + while (prev) { + if (prev.type === 'comment') { + prev = prev.prev(); + continue; + } + + if (prev.type === 'atrule' && IS_CHARSET.test(prev.name)) { + prev = prev.prev(); + continue; + } + + if (prev.type === 'atrule' && IS_LAYER.test(prev.name) && !prev.nodes) { + prev = prev.prev(); + continue; + } + + return result.warn( + '@import must precede all other statements (besides @charset or empty @layer)', + { node: atRule }, + ); + } + + if (atRule.nodes) { + return result.warn( + 'It looks like you didn\'t end your @import statement correctly. ' + + 'Child nodes are attached to it.', + { node: atRule }, + ); + } + + const stmt = { + type: 'import', + uri: '', + fullUri: '', + node: atRule, + conditions: [...conditions], + from, + importingNode, + }; + + const parsed = parseAtImport(atRule.params); + if (!parsed) { + return result.warn(`Invalid @import statement in '${atRule.toString()}'`, { + node: atRule, + }); + } + + const { layer, media, supports, uri, fullUri } = parsed; + + stmt.uri = uri; + stmt.fullUri = fullUri; + + if (media.length > 0 || layer.length > 0 || supports.length > 0) { + stmt.conditions.push({ + layer, + media, + supports, + }); + } + + return stmt; +} diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/parse-styles.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/parse-styles.ts new file mode 100644 index 000000000..2cad50158 --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/parse-styles.ts @@ -0,0 +1,196 @@ +import path from 'path'; +import type { Document, Postcss, Result, Root, AtRule } from 'postcss'; +import { CharsetStatement, ImportStatement, Statement, isCharsetStatement, isImportStatement } from './statement'; +import { Condition } from './conditions'; +import { isValidDataURL } from './data-url'; +import { parseStatements } from './parse-statements'; +import { resolveId } from './resolve-id'; +import { loadContent } from './load-content'; +import noopPlugin from './noop-plugin'; +import { IS_CHARSET } from './names'; + +export async function parseStyles( + result: Result, + styles: Root | Document, + importingNode: AtRule | null, + conditions: Array, + from: Array, + postcss: Postcss, +) { + const statements = parseStatements(result, styles, importingNode, conditions, from); + + for (const stmt of statements) { + if (!isImportStatement(stmt) || !isProcessableURL(stmt.uri)) { + continue; + } + + await resolveImportId(result, stmt, postcss); + } + + let charset: CharsetStatement | null = null; + const imports: Array = []; + const bundle: Array = []; + + function handleCharset(stmt: CharsetStatement) { + if (!charset) { + charset = stmt; + } else if ( + stmt.node.params.toLowerCase() !== charset.node.params.toLowerCase() + ) { + throw stmt.node.error( + `Incompatible @charset statements: + ${stmt.node.params} specified in ${stmt.node.source?.input.file} + ${charset.node.params} specified in ${charset.node.source?.input.file}`, + ); + } + } + + // squash statements and their children + statements.forEach(stmt => { + if (isCharsetStatement(stmt)) { + handleCharset(stmt); + } else if (isImportStatement(stmt)) { + if (stmt.children) { + stmt.children.forEach((child) => { + if (isImportStatement(child)) { + imports.push(child); + } else if (isCharsetStatement(child)) { + handleCharset(child); + } else { + bundle.push(child); + } + }); + } else { + imports.push(stmt); + } + } else if (stmt.type === 'nodes') { + bundle.push(stmt); + } + }); + + return charset ? [charset, ...imports.concat(bundle)] : imports.concat(bundle); +} + +async function resolveImportId(result: Result, stmt: ImportStatement, postcss: Postcss) { + if (isValidDataURL(stmt.uri)) { + // eslint-disable-next-line require-atomic-updates + stmt.children = await loadImportContent( + result, + stmt, + stmt.uri, + postcss, + ); + + return; + } else if (isValidDataURL(stmt.from[stmt.from.length - 1])) { + // Data urls can't be used as a base url to resolve imports. + // Skip inlining and warn. + stmt.children = []; + result.warn( + `Unable to import '${stmt.uri}' from a stylesheet that is embedded in a data url`, + { + node: stmt.node, + }, + ); + return; + } + + const atRule = stmt.node; + let sourceFile: string; + if (atRule.source?.input?.file) { + sourceFile = atRule.source.input.file; + } else { + stmt.children = []; + result.warn( + 'The current PostCSS AST Node is lacking a source file reference. This is most likely a bug in a PostCSS plugin.', + { + node: stmt.node, + }, + ); + return; + } + + const base = path.dirname(sourceFile); + const resolved = resolveId(stmt.uri, base, atRule); + + result.messages.push({ + type: 'dependency', + plugin: 'postcss-bundler', + resolved, + parent: sourceFile, + }); + + const importedContent = await loadImportContent(result, stmt, resolved, postcss); + stmt.children = importedContent ?? []; +} + +async function loadImportContent( + result: Result, + stmt: ImportStatement, + filename: string, + postcss: Postcss, +) { + const { conditions, from, node } = stmt; + + if (from.includes(filename)) { + return; + } + + let content: string; + + try { + content = await loadContent(filename); + } catch { + throw node.error( + `Failed to find '${stmt.uri}'`, + ); + } + + const importedResult = await postcss([noopPlugin()]).process( + content, + { + from: filename, + parser: result.opts.syntax?.parse ?? result.opts.parser ?? undefined, + }, + ); + + const styles = importedResult.root; + result.messages = result.messages.concat(importedResult.messages); + + if (styles.first?.type === 'atrule' && IS_CHARSET.test(styles.first.name)) { + styles.first.after(postcss.comment({ text: `${stmt.uri}`, source: node.source })); + } else { + styles.prepend(postcss.comment({ text: `${stmt.uri}`, source: node.source })); + } + + // recursion: import @import from imported file + return parseStyles( + result, + styles, + node, + conditions, + [...from, filename], + postcss, + ); +} + +function isProcessableURL(uri: string): boolean { + // skip protocol base uri (protocol://url) or protocol-relative + if (/^(?:[a-z]+:)?\/\//i.test(uri)) { + return false; + } + + // check for fragment or query + try { + // needs a base to parse properly + const url = new URL(uri, 'https://example.com'); + + if (url.search) { + return false; + } + } catch { + // Ignore + } + + return true; +} diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/resolve-id.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/resolve-id.ts new file mode 100644 index 000000000..d06d2691f --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/resolve-id.ts @@ -0,0 +1,22 @@ +import type { Node } from 'postcss'; +import path from 'path'; +import module from 'module'; + +export function resolveId(id: string, base: string, node: Node): string { + let resolvedPath = ''; + if (id.startsWith('node_modules:')) { + try { + const require = module.createRequire(base); + + resolvedPath = require.resolve(id.slice(13)); + } catch (e) { + throw node.error( + `Failed to find '${id}'`, + ); + } + } else { + resolvedPath = path.resolve(base, id); + } + + return resolvedPath; +} diff --git a/plugin-packs/postcss-bundler/src/postcss-import/lib/statement.ts b/plugin-packs/postcss-bundler/src/postcss-import/lib/statement.ts new file mode 100644 index 000000000..661b69e31 --- /dev/null +++ b/plugin-packs/postcss-bundler/src/postcss-import/lib/statement.ts @@ -0,0 +1,56 @@ +import type { AtRule, ChildNode, Warning } from 'postcss'; +import { Condition } from './conditions'; + +export type Statement = ImportStatement | CharsetStatement | NodesStatement | Warning; + +export type NodesStatement = { + type: string + nodes: Array + conditions: Array + from: Array + + parent?: Statement + + importingNode: AtRule | null +} + +export type CharsetStatement = { + type: string + node: AtRule + conditions: Array + from: Array + + parent?: Statement + + importingNode: AtRule | null +} + +export type ImportStatement = { + type: string + uri: string + fullUri: string + node: AtRule + conditions: Array + from: Array + + parent?: Statement + children?: Array + + importingNode: AtRule | null +} + +export function isWarning(stmt: Statement): stmt is Warning { + return stmt.type === 'warning'; +} + +export function isNodesStatement(stmt: Statement): stmt is NodesStatement { + return stmt.type === 'nodes'; +} + +export function isCharsetStatement(stmt: Statement): stmt is CharsetStatement { + return stmt.type === 'charset'; +} + +export function isImportStatement(stmt: Statement): stmt is ImportStatement { + return stmt.type === 'import'; +} diff --git a/plugin-packs/postcss-bundler/test/_import.mjs b/plugin-packs/postcss-bundler/test/_import.mjs new file mode 100644 index 000000000..142e3aa2e --- /dev/null +++ b/plugin-packs/postcss-bundler/test/_import.mjs @@ -0,0 +1,6 @@ +import assert from 'assert'; +import plugin from '@csstools/postcss-bundler'; +plugin(); + +assert.ok(plugin.postcss, 'should have "postcss flag"'); +assert.equal(typeof plugin, 'function', 'should return a function'); diff --git a/plugin-packs/postcss-bundler/test/_require.cjs b/plugin-packs/postcss-bundler/test/_require.cjs new file mode 100644 index 000000000..fa79ec287 --- /dev/null +++ b/plugin-packs/postcss-bundler/test/_require.cjs @@ -0,0 +1,6 @@ +const assert = require('assert'); +const plugin = require('@csstools/postcss-bundler'); +plugin(); + +assert.ok(plugin.postcss, 'should have "postcss flag"'); +assert.equal(typeof plugin, 'function', 'should return a function'); diff --git a/plugin-packs/postcss-bundler/test/basic.css b/plugin-packs/postcss-bundler/test/basic.css new file mode 100644 index 000000000..ae04c4128 --- /dev/null +++ b/plugin-packs/postcss-bundler/test/basic.css @@ -0,0 +1,4 @@ +@import "imports/basic.css"; +@import url(./imports/minified-source.css); +@import url("node_modules:@rmenke/css-package-main"); +@import url("node_modules:@rmenke/css-package-conditional-3/styles"); diff --git a/plugin-packs/postcss-bundler/test/basic.expect.css b/plugin-packs/postcss-bundler/test/basic.expect.css new file mode 100644 index 000000000..a8280dfbc --- /dev/null +++ b/plugin-packs/postcss-bundler/test/basic.expect.css @@ -0,0 +1,47 @@ +/* imports/basic.css */ + +.foo { + background: url("images/green.png"); +} + +.bar { + background: url(imports/green.png); +} + +.url-modifier { + background: url("images/green.png" something); +} + +.fragment { + background: url("images/green.png#foo"); +} + +.search { + background: url("images/green.png?foo=bar"); +} + +/* ./imports/minified-source.css */ + +.foo { --var: /* a comment */; bar: red } + +.foo { color: red } + +.foo { color: red } + +.foo { color: rgb(0 0 0) } + +.foo { color: rgb(0 0 0) } + +.foo { color: rgb(0 0 0) } + +/* node_modules:@rmenke/css-package-main */ + +.box { + background-color: green; +} + +/* node_modules:@rmenke/css-package-conditional-3/styles */ + +.box { + background-color: green; +} diff --git a/plugin-packs/postcss-bundler/test/does-not-exist.css b/plugin-packs/postcss-bundler/test/does-not-exist.css new file mode 100644 index 000000000..5a533d25f --- /dev/null +++ b/plugin-packs/postcss-bundler/test/does-not-exist.css @@ -0,0 +1 @@ +@import "imports/does-not-exist.css"; diff --git a/plugin-packs/postcss-bundler/test/does-not-exist.expect.css b/plugin-packs/postcss-bundler/test/does-not-exist.expect.css new file mode 100644 index 000000000..5a533d25f --- /dev/null +++ b/plugin-packs/postcss-bundler/test/does-not-exist.expect.css @@ -0,0 +1 @@ +@import "imports/does-not-exist.css"; diff --git a/plugin-packs/postcss-bundler/test/examples/example.css b/plugin-packs/postcss-bundler/test/examples/example.css new file mode 100644 index 000000000..cc036edc6 --- /dev/null +++ b/plugin-packs/postcss-bundler/test/examples/example.css @@ -0,0 +1,2 @@ +@import url("imports/basic.css"); +@import url("node_modules:open-props/red"); diff --git a/plugin-packs/postcss-bundler/test/examples/example.expect.css b/plugin-packs/postcss-bundler/test/examples/example.expect.css new file mode 100644 index 000000000..089a7b932 --- /dev/null +++ b/plugin-packs/postcss-bundler/test/examples/example.expect.css @@ -0,0 +1,6 @@ +/* imports/basic.css */ +.foo { + background: url("../images/green.png"); +} +/* node_modules:open-props/red */ +:where(html){--red-0:#fff5f5;--red-1:#ffe3e3;--red-2:#ffc9c9;--red-3:#ffa8a8;--red-4:#ff8787;--red-5:#ff6b6b;--red-6:#fa5252;--red-7:#f03e3e;--red-8:#e03131;--red-9:#c92a2a;--red-10:#b02525;--red-11:#962020;--red-12:#7d1a1a} diff --git a/plugin-packs/postcss-bundler/test/examples/imports/basic.css b/plugin-packs/postcss-bundler/test/examples/imports/basic.css new file mode 100644 index 000000000..4504bf94c --- /dev/null +++ b/plugin-packs/postcss-bundler/test/examples/imports/basic.css @@ -0,0 +1,3 @@ +.foo { + background: url('../../images/green.png'); +} diff --git a/plugin-packs/postcss-bundler/test/images/green.png b/plugin-packs/postcss-bundler/test/images/green.png new file mode 100644 index 0000000000000000000000000000000000000000..3171cfbaff7e291406850547f61e7ed77614d24e GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0y~yV6*{ZGe%~h$gzMu%0P-az$e6&p@Ct}&!rQATxCxe t$B>G+w+9UwfxJTszn5=2;+Fto!vUY+G6qK9&zW{05l>e?mvv4FO#rrO7&QO@ literal 0 HcmV?d00001 diff --git a/plugin-packs/postcss-bundler/test/imports/basic.css b/plugin-packs/postcss-bundler/test/imports/basic.css new file mode 100644 index 000000000..bad1b2da9 --- /dev/null +++ b/plugin-packs/postcss-bundler/test/imports/basic.css @@ -0,0 +1,19 @@ +.foo { + background: url('../images/green.png'); +} + +.bar { + background: url(green.png); +} + +.url-modifier { + background: url("../images/green.png" something); +} + +.fragment { + background: url("../images/green.png#foo"); +} + +.search { + background: url("../images/green.png?foo=bar"); +} diff --git a/plugin-packs/postcss-bundler/test/imports/green.png b/plugin-packs/postcss-bundler/test/imports/green.png new file mode 100644 index 0000000000000000000000000000000000000000..3171cfbaff7e291406850547f61e7ed77614d24e GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0y~yV6*{ZGe%~h$gzMu%0P-az$e6&p@Ct}&!rQATxCxe t$B>G+w+9UwfxJTszn5=2;+Fto!vUY+G6qK9&zW{05l>e?mvv4FO#rrO7&QO@ literal 0 HcmV?d00001 diff --git a/plugin-packs/postcss-bundler/test/imports/minified-source.css b/plugin-packs/postcss-bundler/test/imports/minified-source.css new file mode 100644 index 000000000..1902d5de4 --- /dev/null +++ b/plugin-packs/postcss-bundler/test/imports/minified-source.css @@ -0,0 +1,3 @@ +.foo { --var: /* a comment */; bar: red } .foo { color: red } .foo { color: red } .foo { color: rgb(0 0 0) } .foo { color: rgb(0 0 0) } .foo { color: rgb(0 0 0) } + +/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImJhc2ljLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUNFLHNCQUFzQixFQUFFLFNBQzFCLEVBRUEsT0FDQyxXQUNELEVBRUEsT0FDQyxXQUNELEVBRUEsT0FDQyxrQkFDRCxFQUVBLE9BQ0Msa0JBQ0QsRUFFQSxPQUNDLGtCQUNEIiwiZmlsZSI6ImJhc2ljLm1pbi5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyIuZm9vIHtcbiAgLS12YXI6IC8qIGEgY29tbWVudCAqLzsgYmFyOiByZWQ7XG59XG5cbi5mb28ge1xuXHRjb2xvcjogcmVkO1xufVxuXG4uZm9vIHtcblx0Y29sb3I6ICByZWQ7XG59XG5cbi5mb28ge1xuXHRjb2xvcjogcmdiKDAgIDAgMCk7XG59XG5cbi5mb28ge1xuXHRjb2xvcjogcmdiKDAgLyogYSBjb21tZW50ICovIDAgMCk7XG59XG5cbi5mb28ge1xuXHRjb2xvcjogcmdiKDAgLyogYSBjb21tZW50ICovIC8qIG1vcmUgY29tbWVudHMgKi8gMCAwKTtcbn1cbiJdfQ== */ diff --git a/plugin-packs/postcss-bundler/test/leading-slash.css b/plugin-packs/postcss-bundler/test/leading-slash.css new file mode 100644 index 000000000..39784dcb6 --- /dev/null +++ b/plugin-packs/postcss-bundler/test/leading-slash.css @@ -0,0 +1 @@ +@import "/imports/basic.css"; diff --git a/plugin-packs/postcss-bundler/test/leading-slash.expect.css b/plugin-packs/postcss-bundler/test/leading-slash.expect.css new file mode 100644 index 000000000..39784dcb6 --- /dev/null +++ b/plugin-packs/postcss-bundler/test/leading-slash.expect.css @@ -0,0 +1 @@ +@import "/imports/basic.css"; diff --git a/plugin-packs/postcss-bundler/tsconfig.json b/plugin-packs/postcss-bundler/tsconfig.json new file mode 100644 index 000000000..500af6d26 --- /dev/null +++ b/plugin-packs/postcss-bundler/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": ".", + "strict": true + }, + "include": ["./src/**/*"], + "exclude": ["dist"] +} diff --git a/rollup/configs/externals.mjs b/rollup/configs/externals.mjs index 2cc7ff1fe..c2441dc9e 100644 --- a/rollup/configs/externals.mjs +++ b/rollup/configs/externals.mjs @@ -1,5 +1,6 @@ export const externalsForCLI = [ 'fs', + 'fs/promises', 'https', 'path', 'url', @@ -35,6 +36,7 @@ export const externalsForCLI = [ '@csstools/postcss-normalize-display-values', '@csstools/postcss-oklab-function', '@csstools/postcss-progressive-custom-properties', + '@csstools/postcss-rebase-url', '@csstools/postcss-relative-color-syntax', '@csstools/postcss-scope-pseudo-class', '@csstools/postcss-stepped-value-functions', @@ -85,6 +87,7 @@ export const externalsForCLI = [ export const externalsForPlugin = [ 'assert', 'fs', + 'fs/promises', 'https', 'module', 'path', @@ -120,6 +123,7 @@ export const externalsForPlugin = [ '@csstools/postcss-normalize-display-values', '@csstools/postcss-oklab-function', '@csstools/postcss-progressive-custom-properties', + '@csstools/postcss-rebase-url', '@csstools/postcss-relative-color-syntax', '@csstools/postcss-scope-pseudo-class', '@csstools/postcss-stepped-value-functions',