fetch-css 4.1.3 → 4.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # fetch-css
2
- [![](https://img.shields.io/npm/v/fetch-css.svg?style=flat)](https://www.npmjs.org/package/fetch-css) [![](https://img.shields.io/npm/dm/fetch-css.svg)](https://www.npmjs.org/package/fetch-css)
2
+ [![](https://img.shields.io/npm/v/fetch-css.svg?style=flat)](https://www.npmjs.org/package/fetch-css) [![](https://img.shields.io/npm/dm/fetch-css.svg)](https://www.npmjs.org/package/fetch-css) [![](https://packagephobia.com/badge?p=fetch-css)](https://packagephobia.com/result?p=fetch-css)
3
3
  > Extract CSS from websites and browser extensions
4
4
 
5
5
  ## Usage
@@ -0,0 +1,22 @@
1
+ import { FetchOpts } from "fetch-enhanced";
2
+
3
+ //#region index.d.ts
4
+ /** A single CSS source to fetch from a website, file or browser extension. */
5
+ type Source = {
6
+ /** An absolute URL pointing to a website or directly to a CSS or JS file, or an array of such URLs. */url?: string | Array<string>; /** Options passed to `fetch`. */
7
+ fetchOpts?: FetchOpts; /** A Chrome extension id. */
8
+ crx?: string; /** Whether to pull only content scripts from an extension. Default: `false`. */
9
+ contentScriptsOnly?: boolean; /** Whether to throw an error if a fetch fails. Default: `false`. */
10
+ strict?: boolean; /** The extracted CSS, present on the resolved sources. */
11
+ css?: string; /** The resolved stylesheet URLs, present on the resolved sources. */
12
+ urls?: Array<string>; /** The extracted inline `<style>` tag contents, present on the resolved sources. */
13
+ styleTags?: Array<string>;
14
+ };
15
+ /**
16
+ * Extract CSS from websites and browser extensions.
17
+ *
18
+ * Returns the given `sources` array with an additional `css` property present on each source.
19
+ */
20
+ declare function fetchCss(sources: Array<Source>): Promise<Array<Source>>;
21
+ //#endregion
22
+ export { Source, fetchCss as default };
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import{parse as e}from"acorn";import t from"rfdc";import n from"postcss";import{parseFragment as r}from"parse5";import i from"unzipper";import a from"url-toolkit";import o from"crypto-js/enc-latin1.js";import s from"crypto-js/sha256.js";import{fetch as c}from"undici";import l from"fetch-enhanced";function u(e,t,n,r){let i=0;return i+=e<<0,i+=t<<8,i+=n<<16,i+=r<<24>>>0,i}function d(e,t,n){let r=``;for(let i=t;i<n;++i)r+=String.fromCharCode(e[i]);return r}function f(e){let t=new Uint8Array(e);if(t[0]===80&&t[1]===75&&t[2]===3&&t[3]===4)throw Error(`Input is not a CRX file, but a ZIP file.`);if(t[0]!==67||t[1]!==114||t[2]!==50||t[3]!==52)throw m(t)?Error(`Input is not a CRX file, but possibly a ZIP file.`):Error(`Invalid header: Does not start with Cr24.`);if(t[4]!==2&&t[4]!==3||t[5]||t[6]||t[7])throw Error(`Unexpected crx format version number.`);let n;if(t[4]===2){let e=u(t[8],t[9],t[10],t[11]),r=u(t[12],t[13],t[14],t[15]);n=16+e+r,btoa(d(t,16,16+e))}else n=12+u(t[8],t[9],t[10],t[11]),p(t,12,n);return e.slice(n)}function p(e,t,n){function r(){let n=e[t]&127;if(e[t++]<128||(n|=(e[t]&127)<<7,e[t++]<128)||(n|=(e[t]&127)<<14,e[t++]<128)||(n|=(e[t]&127)<<21,e[t++]<128))return n;if(n=(n|(e[t]&15)<<28)>>>0,e[t++]&128)throw Error(`proto: not a uint32`);return n}let i=[],a;for(;t<n;){let o=r(),s=r();if(o===80002){let n=r(),i=r();if(n!==10)throw Error(`proto: Unexpected key in signed_header_data: ${n}`);if(i!==16)throw Error(`proto: Unexpected signed_header_data length ${s}`);if(a)throw Error(`proto: Unexpected duplicate signed_header_data`);a=e.subarray(t,t+16),t+=i;continue}if(o!==18){if(o!==26)throw Error(`proto: Unexpected key: ${o}`);t+=s;continue}let c=t+s,l=r(),u=r();if(l===18){if(t+=u,t>=c)continue;l=r(),u=r()}if(l!==10)throw t+=u,Error(`proto: Unexpected key in AsymmetricKeyProof: ${l}`);if(t+u>n)throw Error(`proto: size of public_key field is too large`);i.push(d(e,t,t+u)),t=c}if(!i.length)throw Error(`proto: Did not find any public key`);if(!a)throw Error(`proto: Did not find crx_id`);let c=o.parse(d(a,0,16)).toString();for(let e of i)if(s(o.parse(e)).toString().slice(0,32)===c)return btoa(e);throw Error(`proto: None of the public keys matched with crx_id`)}function m(e){for(let t=e.length-22,n=Math.max(0,t-65535);t>=n;--t)if(e[t]===80&&e[t+1]===75&&e[t+2]===5&&e[t+3]===6)return!0;return!1}const h=l(c,{undici:!0}),g=t();async function _(e,t){try{return await h(e,t)}catch(t){throw t.message=`${t.message} (${e})`,t}}async function v(e){let t=[],n=[],r=await e.text();for(let n of b(r))t.push(a.buildAbsoluteURL(e.url,n));for(let e of x(r))n.push(e);return[t,n]}function y(e,t,n){if(e.status===200)return;let r=`Failed to fetch ${t}: ${e.status} ${e.statusText}`;if(n)throw Error(r);console.warn(`(fetch-css) Warning: ${r}`)}function b(e){return(e.match(/<link.+?>/g)||[]).map(e=>{let t={};for(let n of r(e).childNodes[0].attrs)t[n.name]=n.value;return t.href&&t.rel===`stylesheet`||t.href&&/\.css$/i.test(t.href.replace(/\?.+/,`undefined`))?t.href:null}).filter(Boolean)}function x(e){return Array.from((e||``).matchAll(/<style.*?>([\s\S]*?)<\/style>/g)||[]).map(e=>e[1]).map(e=>e.trim()).filter(Boolean)}function S(e){try{let t=n.parse(e);if(t&&t.type===`root`&&Array.isArray(t.nodes)&&t.nodes.length>=1&&t.nodes.every(e=>e.type===`rule`))return!0}catch{}return!1}function C(e){let t=Buffer.alloc(e.byteLength),n=new Uint8Array(e);for(let e=0;e<t.length;++e)t[e]=n[e];return t}function w(t){let n=``;return e(t,{ecmaVersion:`latest`,onToken:e=>{if(e.type.label===`string`){let t=e.value.trim().replace(/\n/g,``).replace(/^\);\}/,``);t.length>25&&S(t)&&(n+=`${t}\n`)}}}),n.trim()}async function T({crx:e,contentScriptsOnly:t,strict:n}){let r=`https://clients2.google.com/service/update2/crx`;r+=`?response=redirect`,r+=`&os=linux`,r+=`&arch=x86-64`,r+=`&os_arch=x86-64`,r+=`&acceptformat=crx3`,r+=`&prod=chromiumcrx`,r+=`&prodchannel=unknown`,r+=`&prodversion=9999.0.9999.0`,r+=`&x=id%3D${e}`,r+=`%26uc`;let a=await _(r);y(a,r,n);let o=C(await a.arrayBuffer()),s=Buffer.from(f(o)),c={};for(let e of(await i.Open.buffer(s)||{files:[]}).files)c[e.path]=e;if(!c[`manifest.json`])throw Error(`manifest.json not found in extension ${e}`);let l=[],u=[];if(!t)for(let e of Object.keys(c))e.endsWith(`.css`)&&l.push(e),e.endsWith(`.js`)&&u.push(e);let d=JSON.parse(String(await c[`manifest.json`].buffer()));for(let{css:e,js:t}of d.content_scripts||[])Array.isArray(e)&&e.length&&l.push(...e),Array.isArray(t)&&t.length&&u.push(...t);l=Array.from(new Set(l)),u=Array.from(new Set(u)),l=l.map(e=>e.replace(/^\//,``)),u=u.map(e=>e.replace(/^\//,``));let p=``;for(let e of l)p+=`${String(await c[e].buffer())}\n`;for(let e of u){let t=String(await c[e].buffer());p+=w(t)}return p}async function E(e){e=g(e);let t=[];for(let n of e)if(`url`in n&&Array.isArray(n.url))for(let e of n.url)t.push({...n,url:e});else t.push(n);e=t;let n=await Promise.all(e.map(e=>{if(!e.url)return Promise.resolve(null);let{pathname:t}=new URL(e.url);return t.endsWith(`.css`)||t.endsWith(`.js`)?Promise.resolve(null):_(e.url,e.fetchOpts)}));for(let[t,r]of n.entries()){let n=e[t];if(n.urls=[],n.styleTags=[],r){y(r,n.url,n.strict);let[e,t]=await v(r);n.urls.push(...e),n.styleTags.push(...t)}else n.url&&(n.urls=[n.url])}let r=await Promise.all(e.map(e=>e.url?Promise.all(e.urls.map(e=>_(e).then(e=>e.text()))):Promise.resolve(null)));for(let[t,n]of r.entries()){let r=e[t];r.crx?r.css=await T(r):r.url.endsWith(`.js`)?r.css=w(n.join(`
2
+ `)):(r.css=n.join(`
3
+ `),r.styleTags?.length&&(r.css+=`\n${r.styleTags.join(`
4
+ `)}`))}return e}export{E as default};
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "fetch-css",
3
- "version": "4.1.3",
3
+ "version": "4.1.4",
4
4
  "description": "Extract CSS from websites and browser extensions",
5
5
  "author": "silverwind",
6
6
  "repository": "silverwind/fetch-css",
7
7
  "license": "BSD-2-Clause",
8
8
  "type": "module",
9
- "exports": "./index.js",
9
+ "sideEffects": false,
10
+ "main": "./dist/index.js",
11
+ "exports": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
10
13
  "engines": {
11
- "node": ">=14"
14
+ "node": ">=22",
15
+ "bun": "*"
12
16
  },
13
17
  "keywords": [
14
18
  "fetch",
@@ -18,26 +22,32 @@
18
22
  "crx"
19
23
  ],
20
24
  "files": [
21
- "index.js",
22
- "crx-to-zip.js"
25
+ "dist"
23
26
  ],
24
27
  "dependencies": {
25
- "acorn": "8.15.0",
28
+ "acorn": "8.16.0",
26
29
  "crypto-js": "4.2.0",
27
- "fetch-enhanced": "12.3.1",
28
- "parse5": "8.0.0",
29
- "postcss": "8.5.6",
30
+ "fetch-enhanced": "13.0.0",
31
+ "parse5": "8.0.1",
32
+ "postcss": "8.5.14",
30
33
  "rfdc": "1.4.1",
31
- "undici": "7.22.0",
34
+ "undici": "8.2.0",
32
35
  "unzipper": "0.12.3",
33
36
  "url-toolkit": "2.2.5"
34
37
  },
35
38
  "devDependencies": {
36
- "eslint": "9.39.2",
37
- "eslint-config-silverwind": "121.0.5",
38
- "typescript-config-silverwind": "15.0.0",
39
- "updates": "17.5.2",
40
- "updates-config-silverwind": "1.0.3",
41
- "versions": "14.1.2"
39
+ "@types/crypto-js": "4.2.2",
40
+ "@types/node": "25.8.0",
41
+ "@types/unzipper": "0.10.11",
42
+ "@typescript/native-preview": "7.0.0-dev.20260515.1",
43
+ "eslint": "10.4.0",
44
+ "eslint-config-silverwind": "133.0.2",
45
+ "tsdown": "0.22.0",
46
+ "tsdown-config-silverwind": "3.0.1",
47
+ "typescript": "6.0.3",
48
+ "typescript-config-silverwind": "18.0.0",
49
+ "updates": "17.16.12",
50
+ "updates-config-silverwind": "3.0.2",
51
+ "versions": "15.0.4"
42
52
  }
43
53
  }
package/crx-to-zip.js DELETED
@@ -1,176 +0,0 @@
1
- // based on https://github.com/Rob--W/crxviewer/blob/master/src/lib/crx-to-zip.js
2
- // (c) 2013 Rob Wu <rob@robwu.nl>
3
-
4
- import encLatin1 from "crypto-js/enc-latin1.js";
5
- import sha256 from "crypto-js/sha256.js";
6
-
7
- function calcLength(a, b, c, d) {
8
- let length = 0;
9
- length += a << 0; // eslint-disable-line unicorn/prefer-math-trunc
10
- length += b << 8;
11
- length += c << 16;
12
- length += d << 24 >>> 0;
13
- return length;
14
- }
15
- function getBinaryString(bytesView, startOffset, endOffset) {
16
- let binaryString = "";
17
- for (let i = startOffset; i < endOffset; ++i) {
18
- binaryString += String.fromCharCode(bytesView[i]);
19
- }
20
- return binaryString;
21
- }
22
-
23
- // Strips CRX headers from zip
24
- export default function CRXtoZIP(arraybuffer) {
25
- // Definition of crx format: http://developer.chrome.com/extensions/crx.html
26
- const view = new Uint8Array(arraybuffer);
27
-
28
- // 50 4b 03 04
29
- if (view[0] === 80 && view[1] === 75 && view[2] === 3 && view[3] === 4) {
30
- throw new Error("Input is not a CRX file, but a ZIP file.");
31
- }
32
-
33
- // 43 72 32 34
34
- if (view[0] !== 67 || view[1] !== 114 || view[2] !== 50 || view[3] !== 52) {
35
- if (isMaybeZipData(view)) {
36
- throw new Error("Input is not a CRX file, but possibly a ZIP file.");
37
- }
38
- throw new Error("Invalid header: Does not start with Cr24.");
39
- }
40
-
41
- // 02 00 00 00
42
- // 03 00 00 00 CRX3
43
- if (view[4] !== 2 && view[4] !== 3 || view[5] || view[6] || view[7]) {
44
- throw new Error("Unexpected crx format version number.");
45
- }
46
-
47
- let zipStartOffset, _publicKeyBase64;
48
- if (view[4] === 2) {
49
- const publicKeyLength = calcLength(view[8], view[9], view[10], view[11]);
50
- const signatureLength = calcLength(view[12], view[13], view[14], view[15]);
51
- // 16 = Magic number (4), CRX format version (4), lengths (2x4)
52
- zipStartOffset = 16 + publicKeyLength + signatureLength;
53
-
54
- // Public key
55
- _publicKeyBase64 = btoa(getBinaryString(view, 16, 16 + publicKeyLength));
56
- } else { // view[4] === 3
57
- // CRX3 - https://cs.chromium.org/chromium/src/components/crx_file/crx3.proto
58
- const crx3HeaderLength = calcLength(view[8], view[9], view[10], view[11]);
59
- // 12 = Magic number (4), CRX format version (4), header length (4)
60
- zipStartOffset = 12 + crx3HeaderLength;
61
-
62
- // Public key
63
- _publicKeyBase64 = getPublicKeyFromProtoBuf(view, 12, zipStartOffset);
64
- }
65
-
66
- return arraybuffer.slice(zipStartOffset);
67
- }
68
-
69
- function getPublicKeyFromProtoBuf(bytesView, startOffset, endOffset) {
70
- // Protobuf definition: https://cs.chromium.org/chromium/src/components/crx_file/crx3.proto
71
- // Wire format: https://developers.google.com/protocol-buffers/docs/encoding
72
- // The top-level CrxFileHeader message only contains length-delimited fields (type 2).
73
- // To find the public key:
74
- // 1. Look for CrxFileHeader.sha256_with_rsa (field number 2).
75
- // 2. Look for AsymmetricKeyProof.public_key (field number 1).
76
- // 3. Look for CrxFileHeader.signed_header_data (SignedData.crx_id).
77
- // This has 16 bytes (128 bits). Verify that those match with the
78
- // first 128 bits of the sha256 hash of the chosen public key.
79
-
80
- function getvarint() {
81
- // Note: We don't do bound checks (startOffset < endOffset) here,
82
- // because even if we read past the end of bytesView, then we get
83
- // the undefined value, which is converted to 0 when we do a
84
- // bitwise operation in JavaScript.
85
- let val = bytesView[startOffset] & 0x7F;
86
- if (bytesView[startOffset++] < 0x80) return val;
87
- val |= (bytesView[startOffset] & 0x7F) << 7;
88
- if (bytesView[startOffset++] < 0x80) return val;
89
- val |= (bytesView[startOffset] & 0x7F) << 14;
90
- if (bytesView[startOffset++] < 0x80) return val;
91
- val |= (bytesView[startOffset] & 0x7F) << 21;
92
- if (bytesView[startOffset++] < 0x80) return val;
93
- val = (val | (bytesView[startOffset] & 0xF) << 28) >>> 0;
94
- if (bytesView[startOffset++] & 0x80) throw new Error("proto: not a uint32");
95
- return val;
96
- }
97
-
98
- const publicKeys = [];
99
- let crxIdBin;
100
- while (startOffset < endOffset) {
101
- const key = getvarint();
102
- const length = getvarint();
103
- if (key === 80002) { // This is ((10000 << 3) | 2) (signed_header_data).
104
- const sigdatakey = getvarint();
105
- const sigdatalen = getvarint();
106
- if (sigdatakey !== 0xA) {
107
- throw new Error(`proto: Unexpected key in signed_header_data: ${sigdatakey}`);
108
- } else if (sigdatalen !== 16) {
109
- throw new Error(`proto: Unexpected signed_header_data length ${length}`);
110
- } else if (crxIdBin) {
111
- throw new Error("proto: Unexpected duplicate signed_header_data");
112
- } else {
113
- crxIdBin = bytesView.subarray(startOffset, startOffset + 16);
114
- }
115
- startOffset += sigdatalen;
116
- continue;
117
- }
118
- if (key !== 0x12) {
119
- // Likely 0x1a (sha256_with_ecdsa).
120
- if (key !== 0x1A) {
121
- throw new Error(`proto: Unexpected key: ${key}`);
122
- }
123
- startOffset += length;
124
- continue;
125
- }
126
- // Found 0x12 (sha256_with_rsa); Look for 0xA (public_key).
127
- const keyproofend = startOffset + length;
128
- let keyproofkey = getvarint();
129
- let keyprooflength = getvarint();
130
- // AsymmetricKeyProof could contain 0xA (public_key) or 0x12 (signature).
131
- if (keyproofkey === 0x12) {
132
- startOffset += keyprooflength;
133
- if (startOffset >= keyproofend) {
134
- // signature without public_key...? The protocol definition allows it...
135
- continue;
136
- }
137
- keyproofkey = getvarint();
138
- keyprooflength = getvarint();
139
- }
140
- if (keyproofkey !== 0xA) {
141
- startOffset += keyprooflength;
142
- throw new Error(`proto: Unexpected key in AsymmetricKeyProof: ${keyproofkey}`);
143
- }
144
- if (startOffset + keyprooflength > endOffset) {
145
- throw new Error("proto: size of public_key field is too large");
146
- }
147
- // Found 0xA (public_key).
148
- publicKeys.push(getBinaryString(bytesView, startOffset, startOffset + keyprooflength));
149
- startOffset = keyproofend;
150
- }
151
- if (!publicKeys.length) {
152
- throw new Error("proto: Did not find any public key");
153
- }
154
- if (!crxIdBin) {
155
- throw new Error("proto: Did not find crx_id");
156
- }
157
- const crxIdHex = encLatin1.parse(getBinaryString(crxIdBin, 0, 16)).toString();
158
- for (const publicKey of publicKeys) {
159
- const sha256sum = sha256(encLatin1.parse(publicKey)).toString();
160
- if (sha256sum.slice(0, 32) === crxIdHex) {
161
- return btoa(publicKey);
162
- }
163
- }
164
- throw new Error("proto: None of the public keys matched with crx_id");
165
- }
166
- function isMaybeZipData(view) {
167
- // Find EOCD (0xFFFF is the maximum size of an optional trailing comment).
168
- for (let i = view.length - 22, ii = Math.max(0, i - 0xFFFF); i >= ii; --i) {
169
- if (view[i] === 0x50 && view[i + 1] === 0x4B &&
170
- view[i + 2] === 0x05 && view[i + 3] === 0x06) {
171
- return true;
172
- }
173
- }
174
-
175
- return false;
176
- }
package/index.js DELETED
@@ -1,236 +0,0 @@
1
- import {parse} from "acorn";
2
- import cloner from "rfdc";
3
- import postcss from "postcss";
4
- import {parseFragment} from "parse5";
5
- import unzipper from "unzipper";
6
- import urlToolkit from "url-toolkit";
7
- import crxToZip from "./crx-to-zip.js";
8
- import {fetch as undiciFetch} from "undici";
9
- import fetchEnhanced from "fetch-enhanced";
10
-
11
- const fetch = fetchEnhanced(undiciFetch, {undici: true});
12
- const clone = cloner();
13
-
14
- async function doFetch(url, opts) {
15
- try {
16
- return await fetch(url, opts);
17
- } catch (err) {
18
- err.message = `${err.message} (${url})`;
19
- throw err;
20
- }
21
- }
22
-
23
- async function extract(res) {
24
- const styleUrls = [];
25
- const styleTags = [];
26
- const html = await res.text();
27
-
28
- for (const href of extractStyleHrefs(html)) {
29
- styleUrls.push(urlToolkit.buildAbsoluteURL(res.url, href));
30
- }
31
- for (const style of extractStyleTags(html)) {
32
- styleTags.push(style);
33
- }
34
-
35
- return [styleUrls, styleTags];
36
- }
37
-
38
- function validateStatus(res, url, strict) {
39
- if (res.status === 200) return true;
40
- const msg = `Failed to fetch ${url}: ${res.status} ${res.statusText}`;
41
- if (strict) {
42
- throw new Error(msg);
43
- } else {
44
- console.warn(`(fetch-css) Warning: ${msg}`);
45
- }
46
- }
47
-
48
- function extractStyleHrefs(html) {
49
- return (html.match(/<link.+?>/g) || []).map(link => {
50
- const attrs = {};
51
- for (const attr of parseFragment(link).childNodes[0].attrs) {
52
- attrs[attr.name] = attr.value;
53
- }
54
- if (attrs.href && attrs.rel === "stylesheet") return attrs.href;
55
- if (attrs.href && /\.css$/i.test(attrs.href.replace(/\?.+/))) return attrs.href;
56
- return null;
57
- }).filter(Boolean);
58
- }
59
-
60
- function extractStyleTags(html) {
61
- const matches = Array.from((html || "").matchAll(/<style.*?>([\s\S]*?)<\/style>/g) || []);
62
- return matches.map(match => match[1]).map(css => css.trim()).filter(Boolean);
63
- }
64
-
65
- function isValidCSS(string) {
66
- try {
67
- const root = postcss.parse(string);
68
- if (root && root.type === "root" && Array.isArray(root.nodes) && root.nodes.length >= 1 && root.nodes.every(node => node.type === "rule")) {
69
- return true;
70
- }
71
- } catch {}
72
- return false;
73
- }
74
-
75
- function arrayBufferToBufferCycle(ab) {
76
- const buffer = new Buffer(ab.byteLength);
77
- const view = new Uint8Array(ab);
78
- for (let i = 0; i < buffer.length; ++i) {
79
- buffer[i] = view[i];
80
- }
81
- return buffer;
82
- }
83
-
84
- function extractCssFromJs(js) {
85
- let css = "";
86
-
87
- parse(js, {
88
- ecmaVersion: "latest",
89
- onToken: token => {
90
- if (token.type.label === "string") {
91
- const str = token.value.trim()
92
- .replace(/\n/g, "")
93
- .replace(/^\);}/, ""); // this is probably not universal to webpack's css-in-js strings
94
-
95
- if (str.length > 25 && isValidCSS(str)) { // hackish treshold to ignore short strings that may be valid CSS,
96
- css += `${str}\n`;
97
- }
98
- }
99
- }
100
- });
101
-
102
- return css.trim();
103
- }
104
-
105
- async function extensionCss({crx, contentScriptsOnly, strict}) {
106
- let url = `https://clients2.google.com/service/update2/crx`;
107
- url += `?response=redirect`;
108
- url += `&os=linux`;
109
- url += `&arch=x86-64`;
110
- url += `&os_arch=x86-64`;
111
- url += `&acceptformat=crx3`;
112
- url += `&prod=chromiumcrx`;
113
- url += `&prodchannel=unknown`;
114
- url += `&prodversion=9999.0.9999.0`;
115
- url += `&x=id%3D${crx}`;
116
- url += `%26uc`;
117
-
118
- const res = await doFetch(url);
119
- validateStatus(res, url, strict);
120
-
121
- const crxBuffer = arrayBufferToBufferCycle(await res.arrayBuffer());
122
- const zipBuffer = Buffer.from(crxToZip(crxBuffer));
123
-
124
- const files = {};
125
- for (const file of (await unzipper.Open.buffer(zipBuffer) || {}).files) {
126
- files[file.path] = file;
127
- }
128
-
129
- if (!files["manifest.json"]) {
130
- throw new Error(`manifest.json not found in extension ${crx}`);
131
- }
132
-
133
- let cssFiles = [];
134
- let jsFiles = [];
135
-
136
- if (!contentScriptsOnly) {
137
- for (const path of Object.keys(files)) {
138
- if (path.endsWith(".css")) cssFiles.push(path);
139
- if (path.endsWith(".js")) jsFiles.push(path);
140
- }
141
- }
142
-
143
- const manifest = JSON.parse(String(await files["manifest.json"].buffer()));
144
- for (const {css, js} of manifest.content_scripts || []) {
145
- if (Array.isArray(css) && css.length) cssFiles.push(...css);
146
- if (Array.isArray(js) && js.length) jsFiles.push(...js);
147
- }
148
-
149
- // dedupe
150
- cssFiles = Array.from(new Set(cssFiles));
151
- jsFiles = Array.from(new Set(jsFiles));
152
-
153
- // remove leading slash
154
- cssFiles = cssFiles.map(p => p.replace(/^\//, ""));
155
- jsFiles = jsFiles.map(p => p.replace(/^\//, ""));
156
-
157
- let css = "";
158
- for (const file of cssFiles) {
159
- css += `${await files[file].buffer()}\n`;
160
- }
161
-
162
- for (const file of jsFiles) {
163
- const js = String(await files[file].buffer());
164
- css += extractCssFromJs(js);
165
- }
166
-
167
- return css;
168
- }
169
-
170
- export default async function fetchCss(sources) {
171
- sources = clone(sources);
172
-
173
- const expandedSources = [];
174
- for (const source of sources) {
175
- if ("url" in source && Array.isArray(source.url)) {
176
- for (const url of source.url) {
177
- expandedSources.push({...source, url});
178
- }
179
- } else {
180
- expandedSources.push(source);
181
- }
182
- }
183
- sources = expandedSources;
184
-
185
- const sourceResponses = await Promise.all(sources.map(source => {
186
- if (!source.url) return null;
187
- const {pathname} = new URL(source.url);
188
- if (pathname.endsWith(".css") || pathname.endsWith(".js")) return null;
189
-
190
- if (Array.isArray(source.url)) {
191
- return source.url.map(url => doFetch(url, source.fetchOpts));
192
- } else {
193
- return doFetch(source.url, source.fetchOpts);
194
- }
195
- }));
196
-
197
- for (const [index, res] of Object.entries(sourceResponses)) {
198
- const source = sources[index];
199
- source.urls = [];
200
- source.styleTags = [];
201
- if (res) {
202
- for (const r of Array.isArray(res) ? res : [res]) {
203
- validateStatus(r, source.url, source.strict);
204
- const [styleUrls, styleTags] = await extract(r);
205
- source.urls.push(...styleUrls);
206
- source.styleTags.push(...styleTags);
207
- }
208
- } else if (source.url) {
209
- source.urls = [source.url];
210
- }
211
- }
212
-
213
- const fetchResponses = await Promise.all(sources.map(source => {
214
- if (!source.url) return null;
215
- return Promise.all(source.urls.map(url => doFetch(url).then(res => res.text())));
216
- }));
217
-
218
- for (const [index, responses] of Object.entries(fetchResponses)) {
219
- const source = sources[index];
220
-
221
- if (source.crx) {
222
- source.css = await extensionCss(source);
223
- } else {
224
- if (source.url.endsWith(".js")) {
225
- source.css = extractCssFromJs(responses.join("\n"));
226
- } else {
227
- source.css = responses.join("\n");
228
- if (source.styleTags && source.styleTags.length) {
229
- source.css += `\n${source.styleTags.join("\n")}`;
230
- }
231
- }
232
- }
233
- }
234
-
235
- return sources;
236
- }