From a8ea7630e4d14509f5698b571314263bd20c3f74 Mon Sep 17 00:00:00 2001
From: Romain Menke <11521496+romainmenke@users.noreply.github.com>
Date: Mon, 21 Aug 2023 22:27:19 +0200
Subject: [PATCH] `postcss-rebase-url` (#1082)
---
.github/ISSUE_TEMPLATE/css-issue.yml | 1 +
.github/ISSUE_TEMPLATE/plugin-issue.yml | 1 +
.github/labeler.yml | 4 +
package-lock.json | 32 +++
plugins/postcss-rebase-url/.gitignore | 6 +
plugins/postcss-rebase-url/.nvmrc | 1 +
plugins/postcss-rebase-url/.tape.mjs | 22 ++
plugins/postcss-rebase-url/CHANGELOG.md | 11 +
plugins/postcss-rebase-url/INSTALL.md | 235 ++++++++++++++++++
plugins/postcss-rebase-url/LICENSE.md | 18 ++
plugins/postcss-rebase-url/README.md | 70 ++++++
plugins/postcss-rebase-url/dist/index.cjs | 1 +
plugins/postcss-rebase-url/dist/index.d.ts | 5 +
plugins/postcss-rebase-url/dist/index.mjs | 1 +
.../dist/normalized-dir.d.ts | 7 +
.../dist/serialize-string.d.ts | 9 +
plugins/postcss-rebase-url/docs/README.md | 48 ++++
plugins/postcss-rebase-url/package.json | 85 +++++++
plugins/postcss-rebase-url/src/index.ts | 103 ++++++++
.../postcss-rebase-url/src/normalized-dir.ts | 18 ++
plugins/postcss-rebase-url/src/rebase.d.ts | 1 +
plugins/postcss-rebase-url/src/rebase.mjs | 42 ++++
.../src/serialize-string.ts | 42 ++++
plugins/postcss-rebase-url/test/_import.mjs | 6 +
plugins/postcss-rebase-url/test/_require.cjs | 6 +
plugins/postcss-rebase-url/test/basic.css | 9 +
.../postcss-rebase-url/test/basic.expect.css | 44 ++++
.../test/examples/example.css | 1 +
.../test/examples/example.expect.css | 3 +
.../test/examples/imports/basic.css | 3 +
.../postcss-rebase-url/test/images/green.png | Bin 0 -> 107 bytes
.../postcss-rebase-url/test/imports/basic.css | 36 +++
.../postcss-rebase-url/test/imports/green.png | Bin 0 -> 107 bytes
.../postcss-rebase-url/test/unit/basic.mjs | 92 +++++++
.../postcss-rebase-url/test/unit/ignored.mjs | 60 +++++
.../postcss-rebase-url/test/unit/index.mjs | 3 +
.../test/unit/unexpected-urls.mjs | 90 +++++++
plugins/postcss-rebase-url/tsconfig.json | 10 +
38 files changed, 1126 insertions(+)
create mode 100644 plugins/postcss-rebase-url/.gitignore
create mode 100644 plugins/postcss-rebase-url/.nvmrc
create mode 100644 plugins/postcss-rebase-url/.tape.mjs
create mode 100644 plugins/postcss-rebase-url/CHANGELOG.md
create mode 100644 plugins/postcss-rebase-url/INSTALL.md
create mode 100644 plugins/postcss-rebase-url/LICENSE.md
create mode 100644 plugins/postcss-rebase-url/README.md
create mode 100644 plugins/postcss-rebase-url/dist/index.cjs
create mode 100644 plugins/postcss-rebase-url/dist/index.d.ts
create mode 100644 plugins/postcss-rebase-url/dist/index.mjs
create mode 100644 plugins/postcss-rebase-url/dist/normalized-dir.d.ts
create mode 100644 plugins/postcss-rebase-url/dist/serialize-string.d.ts
create mode 100644 plugins/postcss-rebase-url/docs/README.md
create mode 100644 plugins/postcss-rebase-url/package.json
create mode 100644 plugins/postcss-rebase-url/src/index.ts
create mode 100644 plugins/postcss-rebase-url/src/normalized-dir.ts
create mode 100644 plugins/postcss-rebase-url/src/rebase.d.ts
create mode 100644 plugins/postcss-rebase-url/src/rebase.mjs
create mode 100644 plugins/postcss-rebase-url/src/serialize-string.ts
create mode 100644 plugins/postcss-rebase-url/test/_import.mjs
create mode 100644 plugins/postcss-rebase-url/test/_require.cjs
create mode 100644 plugins/postcss-rebase-url/test/basic.css
create mode 100644 plugins/postcss-rebase-url/test/basic.expect.css
create mode 100644 plugins/postcss-rebase-url/test/examples/example.css
create mode 100644 plugins/postcss-rebase-url/test/examples/example.expect.css
create mode 100644 plugins/postcss-rebase-url/test/examples/imports/basic.css
create mode 100644 plugins/postcss-rebase-url/test/images/green.png
create mode 100644 plugins/postcss-rebase-url/test/imports/basic.css
create mode 100644 plugins/postcss-rebase-url/test/imports/green.png
create mode 100644 plugins/postcss-rebase-url/test/unit/basic.mjs
create mode 100644 plugins/postcss-rebase-url/test/unit/ignored.mjs
create mode 100644 plugins/postcss-rebase-url/test/unit/index.mjs
create mode 100644 plugins/postcss-rebase-url/test/unit/unexpected-urls.mjs
create mode 100644 plugins/postcss-rebase-url/tsconfig.json
diff --git a/.github/ISSUE_TEMPLATE/css-issue.yml b/.github/ISSUE_TEMPLATE/css-issue.yml
index 73261a12a..c1670cf14 100644
--- a/.github/ISSUE_TEMPLATE/css-issue.yml
+++ b/.github/ISSUE_TEMPLATE/css-issue.yml
@@ -104,6 +104,7 @@ body:
- PostCSS Place
- PostCSS Progressive Custom Properties
- PostCSS Pseudo Class Any Link
+ - PostCSS Rebase URL
- PostCSS Rebeccapurple
- PostCSS Relative Color Syntax
- PostCSS Replace Overflow Wrap
diff --git a/.github/ISSUE_TEMPLATE/plugin-issue.yml b/.github/ISSUE_TEMPLATE/plugin-issue.yml
index 32c11380d..10d29080b 100644
--- a/.github/ISSUE_TEMPLATE/plugin-issue.yml
+++ b/.github/ISSUE_TEMPLATE/plugin-issue.yml
@@ -106,6 +106,7 @@ body:
- PostCSS Place
- PostCSS Progressive Custom Properties
- PostCSS Pseudo Class Any Link
+ - PostCSS Rebase URL
- PostCSS Rebeccapurple
- PostCSS Relative Color Syntax
- PostCSS Replace Overflow Wrap
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 4fe061eae..299ecd54b 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -226,6 +226,10 @@
- plugins/postcss-pseudo-class-any-link/**
- experimental/postcss-pseudo-class-any-link/**
+"plugins/postcss-rebase-url":
+ - plugins/postcss-rebase-url/**
+ - experimental/postcss-rebase-url/**
+
"plugins/postcss-relative-color-syntax":
- plugins/postcss-relative-color-syntax/**
- experimental/postcss-relative-color-syntax/**
diff --git a/package-lock.json b/package-lock.json
index bc2ff4823..0cace916a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2161,6 +2161,10 @@
"resolved": "plugins/postcss-progressive-custom-properties",
"link": true
},
+ "node_modules/@csstools/postcss-rebase-url": {
+ "resolved": "plugins/postcss-rebase-url",
+ "link": true
+ },
"node_modules/@csstools/postcss-relative-color-syntax": {
"resolved": "plugins/postcss-relative-color-syntax",
"link": true
@@ -14027,6 +14031,34 @@
"postcss": "^8.4"
}
},
+ "plugins/postcss-rebase-url": {
+ "version": "0.1.0-alpha.0",
+ "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.0",
+ "@csstools/css-tokenizer": "^2.1.1"
+ },
+ "devDependencies": {
+ "@csstools/postcss-tape": "*",
+ "postcss-import": "^15.1.0"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
"plugins/postcss-relative-color-syntax": {
"name": "@csstools/postcss-relative-color-syntax",
"version": "2.0.1",
diff --git a/plugins/postcss-rebase-url/.gitignore b/plugins/postcss-rebase-url/.gitignore
new file mode 100644
index 000000000..e5b28db4a
--- /dev/null
+++ b/plugins/postcss-rebase-url/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+package-lock.json
+yarn.lock
+*.result.css
+*.result.css.map
+*.result.html
diff --git a/plugins/postcss-rebase-url/.nvmrc b/plugins/postcss-rebase-url/.nvmrc
new file mode 100644
index 000000000..6ed5da955
--- /dev/null
+++ b/plugins/postcss-rebase-url/.nvmrc
@@ -0,0 +1 @@
+v20.2.0
diff --git a/plugins/postcss-rebase-url/.tape.mjs b/plugins/postcss-rebase-url/.tape.mjs
new file mode 100644
index 000000000..f3635dc8e
--- /dev/null
+++ b/plugins/postcss-rebase-url/.tape.mjs
@@ -0,0 +1,22 @@
+import { postcssTape } from '@csstools/postcss-tape';
+import plugin from '@csstools/postcss-rebase-url';
+import postcssImport from 'postcss-import';
+
+import './test/unit/index.mjs';
+
+await postcssTape(plugin)({
+ basic: {
+ message: "supports basic usage",
+ plugins: [
+ postcssImport(),
+ plugin()
+ ]
+ },
+ 'examples/example': {
+ message: 'minimal example',
+ plugins: [
+ postcssImport(),
+ plugin()
+ ]
+ }
+});
diff --git a/plugins/postcss-rebase-url/CHANGELOG.md b/plugins/postcss-rebase-url/CHANGELOG.md
new file mode 100644
index 000000000..f78e4fe20
--- /dev/null
+++ b/plugins/postcss-rebase-url/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changes to PostCSS Rebase URL
+
+### Unreleased (major)
+
+- Initial major version
+
+### 0.1.0-alpha.0
+
+_August 14, 2023_
+
+- Initial version
diff --git a/plugins/postcss-rebase-url/INSTALL.md b/plugins/postcss-rebase-url/INSTALL.md
new file mode 100644
index 000000000..7d0cdb056
--- /dev/null
+++ b/plugins/postcss-rebase-url/INSTALL.md
@@ -0,0 +1,235 @@
+# Installing PostCSS Rebase URL
+
+[PostCSS Rebase URL] 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 Rebase URL] to your project:
+
+```bash
+npm install postcss @csstools/postcss-rebase-url --save-dev
+```
+
+Use it as a [PostCSS] plugin:
+
+```js
+// commonjs
+const postcss = require('postcss');
+const postcssRebaseURL = require('@csstools/postcss-rebase-url');
+
+postcss([
+ postcssRebaseURL(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+```js
+// esm
+import postcss from 'postcss';
+import postcssRebaseURL from '@csstools/postcss-rebase-url';
+
+postcss([
+ postcssRebaseURL(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+## PostCSS CLI
+
+Add [PostCSS CLI] to your project:
+
+```bash
+npm install postcss-cli @csstools/postcss-rebase-url --save-dev
+```
+
+Use [PostCSS Rebase URL] in your `postcss.config.js` configuration file:
+
+```js
+const postcssRebaseURL = require('@csstools/postcss-rebase-url');
+
+module.exports = {
+ plugins: [
+ postcssRebaseURL(/* pluginOptions */)
+ ]
+}
+```
+
+## PostCSS Load Config
+
+If your framework/CLI supports [`postcss-load-config`](https://github.com/postcss/postcss-load-config).
+
+```bash
+npm install @csstools/postcss-rebase-url --save-dev
+```
+
+`package.json`:
+
+```json
+{
+ "postcss": {
+ "plugins": {
+ "@csstools/postcss-rebase-url": {}
+ }
+ }
+}
+```
+
+`.postcssrc.json`:
+
+```json
+{
+ "plugins": {
+ "@csstools/postcss-rebase-url": {}
+ }
+}
+```
+
+_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-rebase-url --save-dev
+```
+
+Use [PostCSS Rebase URL] 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-rebase-url",
+ {
+ // 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-rebase-url --save-dev
+```
+
+Use [PostCSS Rebase URL] in your `postcss.config.json` file:
+
+```json
+{
+ "plugins": [
+ "@csstools/postcss-rebase-url"
+ ]
+}
+```
+
+```json5
+{
+ "plugins": [
+ [
+ "@csstools/postcss-rebase-url",
+ {
+ // Optionally add plugin options
+ }
+ ]
+ ]
+}
+```
+
+## Gulp
+
+Add [Gulp PostCSS] to your project:
+
+```bash
+npm install gulp-postcss @csstools/postcss-rebase-url --save-dev
+```
+
+Use [PostCSS Rebase URL] in your Gulpfile:
+
+```js
+const postcss = require('gulp-postcss');
+const postcssRebaseURL = require('@csstools/postcss-rebase-url');
+
+gulp.task('css', function () {
+ var plugins = [
+ postcssRebaseURL(/* 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-rebase-url --save-dev
+```
+
+Use [PostCSS Rebase URL] in your Gruntfile:
+
+```js
+const postcssRebaseURL = require('@csstools/postcss-rebase-url');
+
+grunt.loadNpmTasks('grunt-postcss');
+
+grunt.initConfig({
+ postcss: {
+ options: {
+ processors: [
+ postcssRebaseURL(/* 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 Rebase URL]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-rebase-url
+[Next.js]: https://nextjs.org
diff --git a/plugins/postcss-rebase-url/LICENSE.md b/plugins/postcss-rebase-url/LICENSE.md
new file mode 100644
index 000000000..e8ae93b9f
--- /dev/null
+++ b/plugins/postcss-rebase-url/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/plugins/postcss-rebase-url/README.md b/plugins/postcss-rebase-url/README.md
new file mode 100644
index 000000000..3b40ecaad
--- /dev/null
+++ b/plugins/postcss-rebase-url/README.md
@@ -0,0 +1,70 @@
+# PostCSS Rebase URL [][PostCSS]
+
+[][npm-url] [][cli-url] [][discord]
+
+[PostCSS Rebase URL] rebases `url()` functions when transforming CSS.
+
+When bundling CSS, the location of the final stylesheet file will be different than the individual source files.
+[PostCSS Rebase URL] rewrites the contents of `url()` functions so that relative paths continue to work.
+
+Instead of manually mapping where the files will be in the final output you can use this plugin
+and simply use the relative paths to each source file.
+
+_If you need something with more knobs and dials, please checkout [`postcss-url`](https://www.npmjs.com/package/postcss-url)_
+
+```pcss
+/* when used with a bundler like `postcss-import` */
+
+/* test/examples/example.css */
+@import url("imports/basic.css");
+
+/* test/examples/imports/basic.css */
+.foo {
+ background: url('../../images/green.png');
+}
+
+/* becomes */
+
+/* test/examples/example.expect.css */
+.foo {
+ background: url("../images/green.png");
+}
+```
+
+## Usage
+
+Add [PostCSS Rebase URL] to your project:
+
+```bash
+npm install postcss @csstools/postcss-rebase-url --save-dev
+```
+
+Use it as a [PostCSS] plugin:
+
+```js
+const postcss = require('postcss');
+const postcssRebaseURL = require('@csstools/postcss-rebase-url');
+
+postcss([
+ postcssRebaseURL(/* pluginOptions */)
+]).process(YOUR_CSS /*, processOptions */);
+```
+
+[PostCSS Rebase URL] 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)
+
+[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-rebase-url
+
+[PostCSS]: https://github.com/postcss/postcss
+[PostCSS Rebase URL]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-rebase-url
diff --git a/plugins/postcss-rebase-url/dist/index.cjs b/plugins/postcss-rebase-url/dist/index.cjs
new file mode 100644
index 000000000..ac5172bb0
--- /dev/null
+++ b/plugins/postcss-rebase-url/dist/index.cjs
@@ -0,0 +1 @@
+"use strict";var e=require("@csstools/css-tokenizer"),r=require("@csstools/css-parser-algorithms"),t=require("path");const s=/^([a-z0-9.+-_]+:)?\/\//i;function rebase(e,r,i){if(e.startsWith("data:"))return!1;if(s.test(e))return!1;if(e.startsWith("/"))return e;if(e.startsWith("#"))return e;try{const r=new URL(e);if(r.port||r.protocol)return!1}catch{}const o=t.posix.resolve(t.posix.join(r,e));return t.posix.relative(i,o)}function serializeString(e){let r="";for(const t of e){const e=t.codePointAt(0);if(void 0!==e)switch(e){case 0:r+=String.fromCodePoint(65533);break;case 127:r+=`\\${e.toString(16)}`;break;case 34:case 39:case 92:r+=`\\${t}`;break;default:if(1<=e&&e<=31){r+=`\\${e.toString(16)} `;break}r+=t}else r+=String.fromCodePoint(65533)}return r}function normalizedDir(e){return t.parse(t.resolve(e.trim())).dir.split(t.sep).join(t.posix.sep)}const i=/url\(/i,o=/url/i,creator=()=>({postcssPlugin:"postcss-rebase-url",prepare(){const t=new WeakSet;return{Declaration(s,{result:n}){var a;if(t.has(s))return;const{from:u}=n.opts;if(!u)return;if(null==(a=s.source)||!a.input.from)return;if(!i.test(s.value))return;const l=normalizedDir(u),c=s.source.input.from.trim();if(!c)return;const f=normalizedDir(c),p=r.parseCommaSeparatedListOfComponentValues(e.tokenize({css:s.value})),v=r.replaceComponentValues(p,(t=>{if(r.isTokenNode(t)&&t.value[0]===e.TokenType.URL){const e=rebase(t.value[4].value.trim(),f,l);if(e)return t.value[4].value=e,t.value[1]=`url(${serializeString(e)})`,t}if(r.isFunctionNode(t)&&o.test(t.getName()))for(const s of t.value)if(!r.isWhitespaceNode(s)&&!r.isCommentNode(s)&&r.isTokenNode(s)&&s.value[0]===e.TokenType.String){const e=rebase(s.value[4].value.trim(),f,l);if(e)return s.value[4].value=e,s.value[1]=`"${serializeString(e)}"`,t;break}})),m=r.stringify(v);m!==s.value&&(s.value=m,t.add(s))}}}});creator.postcss=!0,module.exports=creator;
diff --git a/plugins/postcss-rebase-url/dist/index.d.ts b/plugins/postcss-rebase-url/dist/index.d.ts
new file mode 100644
index 000000000..877e03130
--- /dev/null
+++ b/plugins/postcss-rebase-url/dist/index.d.ts
@@ -0,0 +1,5 @@
+import type { PluginCreator } from 'postcss';
+/** postcss-rebase-url plugin options */
+export type pluginOptions = never;
+declare const creator: PluginCreator;
+export default creator;
diff --git a/plugins/postcss-rebase-url/dist/index.mjs b/plugins/postcss-rebase-url/dist/index.mjs
new file mode 100644
index 000000000..75c734492
--- /dev/null
+++ b/plugins/postcss-rebase-url/dist/index.mjs
@@ -0,0 +1 @@
+import{tokenize as r,TokenType as e}from"@csstools/css-tokenizer";import{parseCommaSeparatedListOfComponentValues as t,replaceComponentValues as s,isTokenNode as i,isFunctionNode as o,isWhitespaceNode as a,isCommentNode as n,stringify as u}from"@csstools/css-parser-algorithms";import l from"path";const c=/^([a-z0-9.+-_]+:)?\/\//i;function rebase(r,e,t){if(r.startsWith("data:"))return!1;if(c.test(r))return!1;if(r.startsWith("/"))return r;if(r.startsWith("#"))return r;try{const e=new URL(r);if(e.port||e.protocol)return!1}catch{}const s=l.posix.resolve(l.posix.join(e,r));return l.posix.relative(t,s)}function serializeString(r){let e="";for(const t of r){const r=t.codePointAt(0);if(void 0!==r)switch(r){case 0:e+=String.fromCodePoint(65533);break;case 127:e+=`\\${r.toString(16)}`;break;case 34:case 39:case 92:e+=`\\${t}`;break;default:if(1<=r&&r<=31){e+=`\\${r.toString(16)} `;break}e+=t}else e+=String.fromCodePoint(65533)}return e}function normalizedDir(r){return l.parse(l.resolve(r.trim())).dir.split(l.sep).join(l.posix.sep)}const f=/url\(/i,p=/url/i,creator=()=>({postcssPlugin:"postcss-rebase-url",prepare(){const l=new WeakSet;return{Declaration(c,{result:v}){var m;if(l.has(c))return;const{from:d}=v.opts;if(!d)return;if(null==(m=c.source)||!m.input.from)return;if(!f.test(c.value))return;const g=normalizedDir(d),b=c.source.input.from.trim();if(!b)return;const S=normalizedDir(b),h=t(r({css:c.value})),z=s(h,(r=>{if(i(r)&&r.value[0]===e.URL){const e=rebase(r.value[4].value.trim(),S,g);if(e)return r.value[4].value=e,r.value[1]=`url(${serializeString(e)})`,r}if(o(r)&&p.test(r.getName()))for(const t of r.value)if(!a(t)&&!n(t)&&i(t)&&t.value[0]===e.String){const e=rebase(t.value[4].value.trim(),S,g);if(e)return t.value[4].value=e,t.value[1]=`"${serializeString(e)}"`,r;break}})),k=u(z);k!==c.value&&(c.value=k,l.add(c))}}}});creator.postcss=!0;export{creator as default};
diff --git a/plugins/postcss-rebase-url/dist/normalized-dir.d.ts b/plugins/postcss-rebase-url/dist/normalized-dir.d.ts
new file mode 100644
index 000000000..70ed1c972
--- /dev/null
+++ b/plugins/postcss-rebase-url/dist/normalized-dir.d.ts
@@ -0,0 +1,7 @@
+/**
+ * Returns a posix path for the directory of the given file path.
+ *
+ * @param {string} x The file path to normalize.
+ * @returns {string} The normalized directory path.
+ */
+export declare function normalizedDir(x: string): string;
diff --git a/plugins/postcss-rebase-url/dist/serialize-string.d.ts b/plugins/postcss-rebase-url/dist/serialize-string.d.ts
new file mode 100644
index 000000000..ce83d6ae2
--- /dev/null
+++ b/plugins/postcss-rebase-url/dist/serialize-string.d.ts
@@ -0,0 +1,9 @@
+/**
+ * Serialize a string as a quoted CSS string.
+ *
+ * @param {string} str The contents for the string value.
+ * @returns {string} The quoted CSS string.
+ *
+ * @see https://www.w3.org/TR/cssom-1/#common-serializing-idioms
+ */
+export declare function serializeString(str: string): string;
diff --git a/plugins/postcss-rebase-url/docs/README.md b/plugins/postcss-rebase-url/docs/README.md
new file mode 100644
index 000000000..96d0ffcf2
--- /dev/null
+++ b/plugins/postcss-rebase-url/docs/README.md
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[] rebases `url()` functions when transforming CSS.
+
+When bundling CSS, the location of the final stylesheet file will be different than the individual source files.
+[] rewrites the contents of `url()` functions so that relative paths continue to work.
+
+Instead of manually mapping where the files will be in the final output you can use this plugin
+and simply use the relative paths to each source file.
+
+_If you need something with more knobs and dials, please checkout [`postcss-url`](https://www.npmjs.com/package/postcss-url)_
+
+```pcss
+/* when used with a bundler like `postcss-import` */
+
+/* test/examples/example.css */
+
+
+/* test/examples/imports/basic.css */
+
+
+/* becomes */
+
+/* test/examples/example.expect.css */
+
+```
+
+
+
+
+
+
diff --git a/plugins/postcss-rebase-url/package.json b/plugins/postcss-rebase-url/package.json
new file mode 100644
index 000000000..a9f6714bb
--- /dev/null
+++ b/plugins/postcss-rebase-url/package.json
@@ -0,0 +1,85 @@
+{
+ "name": "@csstools/postcss-rebase-url",
+ "description": "Rebase url() functions when transforming CSS",
+ "version": "0.1.0-alpha.0",
+ "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.0",
+ "@csstools/css-tokenizer": "^2.1.1"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ },
+ "devDependencies": {
+ "@csstools/postcss-tape": "*",
+ "postcss-import": "^15.1.0"
+ },
+ "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/plugins/postcss-rebase-url#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/csstools/postcss-plugins.git",
+ "directory": "plugins/postcss-rebase-url"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "postcss-plugin",
+ "rebase",
+ "url"
+ ],
+ "csstools": {
+ "exportName": "postcssRebaseURL",
+ "humanReadableName": "PostCSS Rebase URL"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/plugins/postcss-rebase-url/src/index.ts b/plugins/postcss-rebase-url/src/index.ts
new file mode 100644
index 000000000..796adacfc
--- /dev/null
+++ b/plugins/postcss-rebase-url/src/index.ts
@@ -0,0 +1,103 @@
+import type { PluginCreator } from 'postcss';
+import { TokenType, tokenize } from '@csstools/css-tokenizer';
+import { isCommentNode, isFunctionNode, isTokenNode, isWhitespaceNode, parseCommaSeparatedListOfComponentValues, replaceComponentValues, stringify } from '@csstools/css-parser-algorithms';
+import { rebase } from './rebase';
+import { serializeString } from './serialize-string';
+import { normalizedDir } from './normalized-dir';
+
+/** postcss-rebase-url plugin options */
+export type pluginOptions = never;
+
+const URL_FUNCTION_CALL = /url\(/i;
+const URL_FUNCTION_NAME = /url/i;
+
+const creator: PluginCreator = () => {
+ return {
+ postcssPlugin: 'postcss-rebase-url',
+ prepare() {
+ const visited = new WeakSet();
+
+ return {
+ Declaration(decl, { result }) {
+ if (visited.has(decl)) {
+ return;
+ }
+
+ const { from: fromEntryPoint } = result.opts;
+ if (!fromEntryPoint) {
+ return;
+ }
+
+ if (!decl.source?.input.from) {
+ return;
+ }
+
+ if (!URL_FUNCTION_CALL.test(decl.value)) {
+ return;
+ }
+
+ const fromEntryPointDir = normalizedDir(fromEntryPoint);
+
+ const from = decl.source.input.from.trim();
+ if (!from) {
+ return;
+ }
+
+ const fromDir = normalizedDir(from);
+
+ const componentValuesList = parseCommaSeparatedListOfComponentValues(tokenize({ css: decl.value }));
+ const modifiedComponentValuesList = replaceComponentValues(
+ componentValuesList,
+ (componentValue) => {
+ if (
+ isTokenNode(componentValue) &&
+ componentValue.value[0] === TokenType.URL
+ ) {
+ const rebased = rebase(componentValue.value[4].value.trim(), fromDir, fromEntryPointDir);
+ if (rebased) {
+ componentValue.value[4].value = rebased;
+
+ // Files with quotes
+ componentValue.value[1] = `url(${serializeString(rebased)})`;
+ return componentValue;
+ }
+ }
+
+ if (
+ isFunctionNode(componentValue) &&
+ URL_FUNCTION_NAME.test(componentValue.getName())
+ ) {
+ for (const x of componentValue.value) {
+ if (isWhitespaceNode(x) || isCommentNode(x)) {
+ continue;
+ }
+
+ if (isTokenNode(x) && x.value[0] === TokenType.String) {
+ const rebased = rebase(x.value[4].value.trim(), fromDir, fromEntryPointDir);
+ if (rebased) {
+ x.value[4].value = rebased;
+ x.value[1] = `"${serializeString(rebased)}"`;
+ return componentValue;
+ }
+
+ break;
+ }
+ }
+ }
+ },
+ );
+
+ const modifiedValue = stringify(modifiedComponentValuesList);
+ if (modifiedValue !== decl.value) {
+ decl.value = modifiedValue;
+ visited.add(decl);
+ }
+ },
+ };
+ },
+ };
+};
+
+creator.postcss = true;
+
+export default creator;
diff --git a/plugins/postcss-rebase-url/src/normalized-dir.ts b/plugins/postcss-rebase-url/src/normalized-dir.ts
new file mode 100644
index 000000000..746a58ae4
--- /dev/null
+++ b/plugins/postcss-rebase-url/src/normalized-dir.ts
@@ -0,0 +1,18 @@
+import path from 'path';
+
+/**
+ * Returns a posix path for the directory of the given file path.
+ *
+ * @param {string} x The file path to normalize.
+ * @returns {string} The normalized directory path.
+ */
+export function normalizedDir(x: string): string {
+ // Resolve the path to eliminate any relative path components.
+ const dir = path.parse(path.resolve(x.trim())).dir;
+ // Split the path by the native path separator
+ const dirPathComponents = dir.split(path.sep);
+ // Join the path components with the posix path separator
+ const posixDir = dirPathComponents.join(path.posix.sep);
+
+ return posixDir;
+}
diff --git a/plugins/postcss-rebase-url/src/rebase.d.ts b/plugins/postcss-rebase-url/src/rebase.d.ts
new file mode 100644
index 000000000..1147e26ae
--- /dev/null
+++ b/plugins/postcss-rebase-url/src/rebase.d.ts
@@ -0,0 +1 @@
+export function rebase(url: string, fromDir: string, fromEntryPointDir: string): string | false;
diff --git a/plugins/postcss-rebase-url/src/rebase.mjs b/plugins/postcss-rebase-url/src/rebase.mjs
new file mode 100644
index 000000000..d0ba02c76
--- /dev/null
+++ b/plugins/postcss-rebase-url/src/rebase.mjs
@@ -0,0 +1,42 @@
+import path from 'path';
+
+const hasProtocol = /^([a-z0-9.+-_]+:)?\/\//i;
+
+/**
+ * Rebase a URL from one directory to another.
+ *
+ * @param {string} url The URL to rebase.
+ * @param {string} fromDir The directory to rebase from.
+ * @param {string} fromEntryPointDir The directory of the entry point.
+ * @returns {string|false} The rebased URL, or `false` if the URL is absolute.
+ */
+export function rebase(url, fromDir, fromEntryPointDir) {
+ if (url.startsWith('data:')) {
+ return false;
+ }
+
+ if (hasProtocol.test(url)) {
+ return false;
+ }
+
+ if (url.startsWith('/')) {
+ return url;
+ }
+
+ if (url.startsWith('#')) {
+ return url;
+ }
+
+ try {
+ const x = new URL(url);
+ if (x.port || x.protocol) {
+ return false;
+ }
+ } catch { } // eslint-disable-line no-empty
+
+ const absPath = path.posix.resolve(
+ path.posix.join(fromDir, url),
+ );
+
+ return path.posix.relative(fromEntryPointDir, absPath);
+}
diff --git a/plugins/postcss-rebase-url/src/serialize-string.ts b/plugins/postcss-rebase-url/src/serialize-string.ts
new file mode 100644
index 000000000..8cd06f279
--- /dev/null
+++ b/plugins/postcss-rebase-url/src/serialize-string.ts
@@ -0,0 +1,42 @@
+/**
+ * Serialize a string as a quoted CSS string.
+ *
+ * @param {string} str The contents for the string value.
+ * @returns {string} The quoted CSS string.
+ *
+ * @see https://www.w3.org/TR/cssom-1/#common-serializing-idioms
+ */
+export function serializeString(str: string): string {
+ let out = '';
+
+ for (const codePoint of str) {
+ const codePointNumber = codePoint.codePointAt(0);
+ if (typeof codePointNumber === 'undefined') {
+ out += String.fromCodePoint(0xFFFD);
+ continue;
+ }
+
+ switch (codePointNumber) {
+ case 0x0000:
+ out += String.fromCodePoint(0xFFFD);
+ break;
+ case 0x007F:
+ out += `\\${codePointNumber.toString(16)}`;
+ break;
+ case 0x0022:
+ case 0x0027:
+ case 0x005C:
+ out += `\\${codePoint}`;
+ break;
+ default:
+ if (0x0001 <= codePointNumber && codePointNumber <= 0x001f) {
+ out += `\\${codePointNumber.toString(16)} `;
+ break;
+ }
+
+ out += codePoint;
+ }
+ }
+
+ return out;
+}
diff --git a/plugins/postcss-rebase-url/test/_import.mjs b/plugins/postcss-rebase-url/test/_import.mjs
new file mode 100644
index 000000000..4b0bced28
--- /dev/null
+++ b/plugins/postcss-rebase-url/test/_import.mjs
@@ -0,0 +1,6 @@
+import assert from 'assert';
+import plugin from '@csstools/postcss-rebase-url';
+plugin();
+
+assert.ok(plugin.postcss, 'should have "postcss flag"');
+assert.equal(typeof plugin, 'function', 'should return a function');
diff --git a/plugins/postcss-rebase-url/test/_require.cjs b/plugins/postcss-rebase-url/test/_require.cjs
new file mode 100644
index 000000000..d425856aa
--- /dev/null
+++ b/plugins/postcss-rebase-url/test/_require.cjs
@@ -0,0 +1,6 @@
+const assert = require('assert');
+const plugin = require('@csstools/postcss-rebase-url');
+plugin();
+
+assert.ok(plugin.postcss, 'should have "postcss flag"');
+assert.equal(typeof plugin, 'function', 'should return a function');
diff --git a/plugins/postcss-rebase-url/test/basic.css b/plugins/postcss-rebase-url/test/basic.css
new file mode 100644
index 000000000..806cb9f7f
--- /dev/null
+++ b/plugins/postcss-rebase-url/test/basic.css
@@ -0,0 +1,9 @@
+@import "imports/basic.css";
+
+.root {
+ background: url(./images/green.png);
+}
+
+.outside_root {
+ background: url("../../../images/green.png");
+}
diff --git a/plugins/postcss-rebase-url/test/basic.expect.css b/plugins/postcss-rebase-url/test/basic.expect.css
new file mode 100644
index 000000000..e0d3cb681
--- /dev/null
+++ b/plugins/postcss-rebase-url/test/basic.expect.css
@@ -0,0 +1,44 @@
+.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");
+ background: url("#foo");
+}
+
+.search {
+ background: url("images/green.png?foo=bar");
+}
+
+.leading_slash {
+ background: url("/test/images/green.png");
+}
+
+.full_url {
+ background: url("https://example.com/images/green.png");
+}
+
+.unknown_protocol {
+ background: url("foo://example.com/images/green.png");
+}
+
+.node_modules_protocol {
+ background: url("node_modules://images/green.png");
+}
+
+.root {
+ background: url(images/green.png);
+}
+
+.outside_root {
+ background: url("../../../images/green.png");
+}
diff --git a/plugins/postcss-rebase-url/test/examples/example.css b/plugins/postcss-rebase-url/test/examples/example.css
new file mode 100644
index 000000000..ec2d75f34
--- /dev/null
+++ b/plugins/postcss-rebase-url/test/examples/example.css
@@ -0,0 +1 @@
+@import url("imports/basic.css");
diff --git a/plugins/postcss-rebase-url/test/examples/example.expect.css b/plugins/postcss-rebase-url/test/examples/example.expect.css
new file mode 100644
index 000000000..d88049f89
--- /dev/null
+++ b/plugins/postcss-rebase-url/test/examples/example.expect.css
@@ -0,0 +1,3 @@
+.foo {
+ background: url("../images/green.png");
+}
diff --git a/plugins/postcss-rebase-url/test/examples/imports/basic.css b/plugins/postcss-rebase-url/test/examples/imports/basic.css
new file mode 100644
index 000000000..4504bf94c
--- /dev/null
+++ b/plugins/postcss-rebase-url/test/examples/imports/basic.css
@@ -0,0 +1,3 @@
+.foo {
+ background: url('../../images/green.png');
+}
diff --git a/plugins/postcss-rebase-url/test/images/green.png b/plugins/postcss-rebase-url/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/plugins/postcss-rebase-url/test/imports/basic.css b/plugins/postcss-rebase-url/test/imports/basic.css
new file mode 100644
index 000000000..566fcbbb2
--- /dev/null
+++ b/plugins/postcss-rebase-url/test/imports/basic.css
@@ -0,0 +1,36 @@
+.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");
+ background: url("#foo");
+}
+
+.search {
+ background: url("../images/green.png?foo=bar");
+}
+
+.leading_slash {
+ background: url("/test/images/green.png");
+}
+
+.full_url {
+ background: url("https://example.com/images/green.png");
+}
+
+.unknown_protocol {
+ background: url("foo://example.com/images/green.png");
+}
+
+.node_modules_protocol {
+ background: url("node_modules://images/green.png");
+}
diff --git a/plugins/postcss-rebase-url/test/imports/green.png b/plugins/postcss-rebase-url/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/plugins/postcss-rebase-url/test/unit/basic.mjs b/plugins/postcss-rebase-url/test/unit/basic.mjs
new file mode 100644
index 000000000..02ffe4596
--- /dev/null
+++ b/plugins/postcss-rebase-url/test/unit/basic.mjs
@@ -0,0 +1,92 @@
+import assert from 'assert';
+import { rebase } from '../../src/rebase.mjs';
+
+{
+ assert.equal(
+ rebase(
+ 'foo.png',
+ '/assets/css',
+ '/assets/css',
+ '/assets/css',
+ ),
+ 'foo.png',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ 'foo.png',
+ '/assets/css/components',
+ '/assets/css',
+ '/assets/css',
+ ),
+ 'components/foo.png',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ '/foo.png',
+ '/assets/css',
+ '/assets/css',
+ ),
+ '/foo.png',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ '/foo.png',
+ '/assets/css/components',
+ '/assets/css',
+ ),
+ '/foo.png',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ '../images/foo.png',
+ '/assets/css/components',
+ '/assets/css',
+ ),
+ 'images/foo.png',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ 'images/foo.png',
+ '/assets/css/components',
+ '/assets/css',
+ ),
+ 'components/images/foo.png',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ '../../images/foo.png',
+ '/assets/css/components',
+ '/assets/css',
+ ),
+ '../images/foo.png',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ '../../images/foo.png',
+ '/blocks/components',
+ '/assets/css',
+ ),
+ '../../images/foo.png',
+ );
+}
diff --git a/plugins/postcss-rebase-url/test/unit/ignored.mjs b/plugins/postcss-rebase-url/test/unit/ignored.mjs
new file mode 100644
index 000000000..614fc9e9f
--- /dev/null
+++ b/plugins/postcss-rebase-url/test/unit/ignored.mjs
@@ -0,0 +1,60 @@
+import assert from 'assert';
+import { rebase } from '../../src/rebase.mjs';
+
+{
+ assert.equal(
+ rebase(
+ 'https://example.com/foo.png',
+ '/assets/css/components',
+ '/assets/css',
+ '/assets/css',
+ ),
+ false,
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ '//example.com/foo.png',
+ '/assets/css/components',
+ '/assets/css',
+ '/assets/css',
+ ),
+ false,
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ 'example.com:8080/foo.png',
+ '/assets/css/components',
+ '/assets/css',
+ ),
+ false,
+ );
+}
+
+{
+ // Not distinguishable from a relative URL.
+ assert.equal(
+ rebase(
+ 'example.com/foo.png',
+ '/assets/css/components',
+ '/assets/css',
+ ),
+ 'components/example.com/foo.png',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==',
+ '/assets/css/components',
+ '/assets/css',
+ ),
+ false,
+ );
+}
diff --git a/plugins/postcss-rebase-url/test/unit/index.mjs b/plugins/postcss-rebase-url/test/unit/index.mjs
new file mode 100644
index 000000000..c07e3b140
--- /dev/null
+++ b/plugins/postcss-rebase-url/test/unit/index.mjs
@@ -0,0 +1,3 @@
+import './ignored.mjs';
+import './basic.mjs';
+import './unexpected-urls.mjs';
diff --git a/plugins/postcss-rebase-url/test/unit/unexpected-urls.mjs b/plugins/postcss-rebase-url/test/unit/unexpected-urls.mjs
new file mode 100644
index 000000000..348652310
--- /dev/null
+++ b/plugins/postcss-rebase-url/test/unit/unexpected-urls.mjs
@@ -0,0 +1,90 @@
+import assert from 'assert';
+import { rebase } from '../../src/rebase.mjs';
+
+{
+ assert.equal(
+ rebase(
+ '@foo/bar',
+ '/assets/css',
+ '/assets/css',
+ ),
+ '@foo/bar',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ '@foo/bar',
+ '/assets/css/components',
+ '/assets/css',
+ ),
+ 'components/@foo/bar',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ '~foo/bar',
+ '/assets/css',
+ '/assets/css',
+ ),
+ '~foo/bar',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ '~foo/bar',
+ '/assets/css/components',
+ '/assets/css',
+ ),
+ 'components/~foo/bar',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ '~/foo/bar',
+ '/assets/css',
+ '/assets/css',
+ ),
+ '~/foo/bar',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ '~/foo/bar',
+ '/assets/css/components',
+ '/assets/css',
+ ),
+ 'components/~/foo/bar',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ 'node_modules:/foo/bar',
+ '/assets/css',
+ '/assets/css',
+ ),
+ 'node_modules:/foo/bar',
+ );
+}
+
+{
+ assert.equal(
+ rebase(
+ 'node_modules:/foo/bar',
+ '/assets/css/components',
+ '/assets/css',
+ ),
+ 'components/node_modules:/foo/bar',
+ );
+}
diff --git a/plugins/postcss-rebase-url/tsconfig.json b/plugins/postcss-rebase-url/tsconfig.json
new file mode 100644
index 000000000..500af6d26
--- /dev/null
+++ b/plugins/postcss-rebase-url/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declarationDir": ".",
+ "strict": true
+ },
+ "include": ["./src/**/*"],
+ "exclude": ["dist"]
+}