diff --git a/__tests__/index.spec.ts b/__tests__/index.spec.ts
index b12c4a2..70501d2 100644
--- a/__tests__/index.spec.ts
+++ b/__tests__/index.spec.ts
@@ -10,6 +10,10 @@ describe('node-detective-postcss', () => {
assert('@import "foo.css"', ['foo.css']);
});
+ it('works with single quotes', () => {
+ assert("@import 'foo.css'", ['foo.css']);
+ });
+
describe('url()', () => {
it('works with url()', () => {
assert('@import url("navigation.css");', ['navigation.css']);
@@ -53,6 +57,14 @@ describe('node-detective-postcss', () => {
);
});
+ it('ignores protocol-relative URLs', () => {
+ assert('@import url("//example.com/style.css");', []);
+ });
+
+ it('ignores protocol-relative URLs without url()', () => {
+ assert('@import "//example.com/style.css"', []);
+ });
+
it('does not touch the paths', () => {
assert('@import "../../././bla.css"', ['../../././bla.css']);
});
@@ -85,6 +97,10 @@ describe('node-detective-postcss', () => {
]);
});
+ it('ignores absolute URLs', () => {
+ assert("@value primary from 'https://example.com/colors.css';", []);
+ });
+
it('leaves simple definitions alone', () => {
assert('@value mine: #fff;', []);
});
@@ -131,6 +147,24 @@ describe('node-detective-postcss', () => {
assert('@value x: url(bummer.png)', ['bummer.png'], { url: true });
});
+ it('ignores absolute urls', () => {
+ assert('.x { background: url(https://example.com/img.png) }', [], {
+ url: true,
+ });
+ });
+
+ it('ignores protocol-relative urls', () => {
+ assert('.x { background: url(//example.com/img.png) }', [], {
+ url: true,
+ });
+ });
+
+ it('finds multiple url() in one declaration', () => {
+ assert('.x { background: url(a.png), url(b.png) }', ['a.png', 'b.png'], {
+ url: true,
+ });
+ });
+
it('ignores base64 data: urls', () => {
assert(
'.x { background: url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}',
@@ -140,8 +174,8 @@ describe('node-detective-postcss', () => {
it('ignores SVG data: urls', () => {
const css = `svg {
- -webkit-mask-image: url('data:image/svg+xml;utf8,');
- }`;
+ -webkit-mask-image: url('data:image/svg+xml;utf8,');
+ }`;
assert(css, []);
});
});
diff --git a/package-lock.json b/package-lock.json
index c3d9e4b..4bca9c7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,11 +9,10 @@
"version": "7.0.1",
"license": "MIT",
"dependencies": {
- "is-url": "^1.2.4",
+ "is-url-superb": "^4.0.0",
"postcss-values-parser": "^6.0.2"
},
"devDependencies": {
- "@types/is-url": "^1.2.32",
"@types/jest": "^30.0.0",
"jest": "^30.2.0",
"lint-staged": "^16.3.1",
@@ -1117,13 +1116,6 @@
"@babel/types": "^7.28.2"
}
},
- "node_modules/@types/is-url": {
- "version": "1.2.32",
- "resolved": "https://registry.npmjs.org/@types/is-url/-/is-url-1.2.32.tgz",
- "integrity": "sha512-46VLdbWI8Sc+hPexQ6NLNR2YpoDyDZIpASHkJQ2Yr+Kf9Giw6LdCTkwOdsnHKPQeh7xTjTmSnxbE8qpxYuCiHA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -2591,12 +2583,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/is-url": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
- "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
- "license": "MIT"
- },
"node_modules/is-url-superb": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz",
diff --git a/package.json b/package.json
index b8f7af6..b5158e4 100644
--- a/package.json
+++ b/package.json
@@ -26,11 +26,10 @@
"postcss": "^8.4.47"
},
"dependencies": {
- "is-url": "^1.2.4",
+ "is-url-superb": "^4.0.0",
"postcss-values-parser": "^6.0.2"
},
"devDependencies": {
- "@types/is-url": "^1.2.32",
"@types/jest": "^30.0.0",
"jest": "^30.2.0",
"lint-staged": "^16.3.1",
diff --git a/src/index.ts b/src/index.ts
index c504820..36c0e47 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,5 @@
import { debuglog } from 'util';
-import isUrl = require('is-url');
+import isUrl = require('is-url-superb');
import { parse, AtRule } from 'postcss';
import {
parse as postCssParseValue,
@@ -47,7 +47,7 @@ function detective(src, options: detective.Options = { url: false }) {
if (options.url && isUrlNode(lastNode)) {
file = getValueOrUrl(lastNode);
if (file) {
- debug('found %s of %s', 'url() with import', file);
+ debug('found %s of %s', 'url() in @value', file);
}
}
}
@@ -61,26 +61,41 @@ function detective(src, options: detective.Options = { url: false }) {
const { nodes } = postCssParseValue(decl.value);
const files = nodes
.filter((node) => isUrlNode(node))
- .map((node) => getValueOrUrl(node));
+ .map((node) => getValueOrUrl(node))
+ .filter((file): file is string => Boolean(file));
- if (files) {
- for (const file of files) {
- debug('found %s of %s', 'url() with import', file);
- }
-
- references = references.concat(files);
+ for (const file of files) {
+ debug('found %s of %s', 'url() in declaration', file);
}
+
+ references = references.concat(files);
});
return references;
}
-function getValueOrUrl(node: ChildNode) {
- // ['file']
- const ret = isUrlNode(node) ? getValue(node.nodes[0]) : getValue(node);
+function getValueOrUrl(node: ChildNode): string | false {
+ const ret = isUrlNode(node) ? getUrlContent(node) : getValue(node);
+
+ // is-url-superb uses new URL() which doesn't accept protocol-relative URLs;
+ // prepend http: so they get correctly identified and filtered out
+ return !isUrl(ret.startsWith('//') ? `http:${ret}` : ret) && ret;
+}
+
+function getUrlContent(urlNode: Func): string {
+ const first = urlNode.nodes[0];
+
+ // Quoted: url('foo.css') or url("foo.css")
+ if (first && first.type === 'quoted') {
+ return first.contents;
+ }
- // is-url sometimes gets data: URLs wrong
- return !isUrl(ret) && !ret.startsWith('data:') && ret;
+ // Unquoted: reconstruct the full string from all child nodes (handles
+ // absolute URLs like url(https://...) which parse as multiple tokens)
+ return urlNode.nodes
+ .filter((n) => isNodeWithValue(n))
+ .map((n) => getValue(n))
+ .join('');
}
function getValue(node: ChildNode) {