• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localnerve / sass-asset-functions / 25017172089

27 Apr 2026 08:13PM UTC coverage: 98.033%. First build
25017172089

Pull #114

github

web-flow
Merge 11535abae into fcfe22d24
Pull Request #114: @7.11.0 - devdeps, new URL processing w/fallback

247 of 254 branches covered (97.24%)

Branch coverage included in aggregate %.

116 of 125 new or added lines in 2 files covered. (92.8%)

949 of 966 relevant lines covered (98.24%)

40.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.19
/lib/processor.js
1
/**
1✔
2
 * Internal processor for the asset function suite.
1✔
3
 * 
1✔
4
 * Copyright (c) 2023-2025 Alex Grant (@localnerve), LocalNerve LLC
1✔
5
 * Licensed under the MIT license.
1✔
6
 */
1✔
7
import * as fs from 'node:fs';
1✔
8
import * as path from 'node:path';
1✔
9
import * as url from 'node:url';
1✔
10
import mime from 'mime-types';
1✔
11
import { imageSize } from 'image-size';
1✔
12

1✔
13
const defaultPaths = {
1✔
14
  images_path: 'public/images',
1✔
15
  fonts_path: 'public/fonts',
1✔
16
  http_images_path: '/images',
1✔
17
  http_fonts_path: '/fonts'  
1✔
18
};
1✔
19

1✔
20
const FONT_TYPES = {
1✔
21
  woff: 'woff',
1✔
22
  woff2: 'woff2',
1✔
23
  otf: 'opentype',
1✔
24
  opentype: 'opentype',
1✔
25
  ttf: 'truetype',
1✔
26
  truetype: 'truetype',
1✔
27
  svg: 'svg',
1✔
28
  eot: 'embedded-opentype'
1✔
29
};
1✔
30

1✔
31
export default class Processor {
1✔
32
  constructor (options) {
1✔
33
    this.options = { ...{ data: {} }, ...options };
156✔
34
    const {
156✔
35
      images_path = defaultPaths.images_path,
156✔
36
      fonts_path = defaultPaths.fonts_path,
156✔
37
      http_images_path = defaultPaths.http_images_path,
156✔
38
      http_fonts_path = defaultPaths.http_fonts_path
156✔
39
    } = options;
156✔
40
    this.paths = {
156✔
41
      images_path, fonts_path, http_images_path, http_fonts_path
156✔
42
    }
156✔
43
  }
156✔
44

1✔
45
  asset_cache_buster (http_path, real_path, done) {
1✔
46
    const { asset_cache_buster: buster } = this.options;
96✔
47

96✔
48
    if (typeof buster !== 'function') {
96✔
49
      throw new Error('asset_cache_buster should be a function');
6✔
50
    }
6✔
51

90✔
52
    function preserveEmptyQS (originalUrl, query) {
90✔
53
      let result = query;
45✔
54
      if (!query && originalUrl.includes('?')) {
45✔
55
        result = '?';
10✔
56
      }
10✔
57
      return result;
45✔
58
    }
45✔
59

90✔
60
    const http_path_url = this.parse_url(http_path);
90✔
61

90✔
62
    buster(http_path, real_path, value => {
90✔
63
      let new_url;
90✔
64

90✔
65
      if (typeof value == 'object') {
90✔
66
        const parsed_path = this.parse_url(value.path);
45✔
67
        new_url = {
45✔
68
          pathname: parsed_path.pathname,
45✔
69
          search: value.query || preserveEmptyQS(http_path, http_path_url.search)
45✔
70
        };
45✔
71
      } else {
45✔
72
        new_url = {
45✔
73
          pathname: http_path_url.pathname,
45✔
74
          search: value
45✔
75
        };
45✔
76
      }
45✔
77

90✔
78
      done(url.format(new_url));
90✔
79
    });
90✔
80
  }
96✔
81

1✔
82
  asset_host (filepath, done) {
1✔
83
    const { asset_host: ahost } = this.options;
51✔
84
    
51✔
85
    if (typeof ahost !== 'function') {
51✔
86
      throw new Error('asset_host should be a function');
6✔
87
    }
6✔
88

45✔
89
    ahost(filepath, host => {
45✔
90
      done(this.resolve_url(host, filepath));
45✔
91
    });
45✔
92
  }
51✔
93

1✔
94
  resolve_url (from, to) {
1✔
95
    let result;
45✔
96

45✔
97
    try {
45✔
98
      const resolvedUrl = new URL(to, new URL(from, 'resolve://bogus.local'));
45✔
99
      if (resolvedUrl.protocol === 'resolve:') {
45!
NEW
100
        // `from` is a relative URL.
×
NEW
101
        const { pathname, search, hash } = resolvedUrl;
×
NEW
102
        result = `${pathname}${search}${hash}`;
×
103
      } else {
45✔
104
        result = resolvedUrl.toString();
45✔
105
      }
45✔
106
    } catch {
45!
NEW
107
      result = url.resolve(from, to); // eslint-disable-line n/no-deprecated-api
×
NEW
108
      console.warn(`DEPRECATION WARNING [sass-asset-functions]: Resolving '${from}' to '${to}' will not work in an upcoming version`);
×
NEW
109
    }
×
110

45✔
111
    return result;
45✔
112
  }
45✔
113

1✔
114
  parse_url (input) {
1✔
115
    let result;
189✔
116

189✔
117
    try {
189✔
118
      const parsedUrl = new URL(input, 'parse://bogus.local'); // base used if relative was supplied
189✔
119
      result = parsedUrl;
189✔
120
    } catch {
189!
NEW
121
      result = url.parse(input); // eslint-disable-line n/no-deprecated-api
×
NEW
122
      console.warn(`DEPRECATION WARNING [sass-asset-functions]: Parsing url '${input}' will not work in an upcoming version`);
×
NEW
123
    }
×
124

189✔
125
    return result;
189✔
126
  }
189✔
127

1✔
128
  real_path (filepath, segment) {
1✔
129
    const sanitized_filepath = filepath.replace(/(#|\?).+$/, '');
261✔
130
    return path.resolve(this.paths[`${segment}_path`], sanitized_filepath);
261✔
131
  }
261✔
132

1✔
133
  http_path (filepath, segment) {
1✔
134
    return path.join(this.paths[`http_${segment}_path`], filepath).replace(/\\/g, '/');
174✔
135
  }
174✔
136
  
1✔
137
  image_width (filepath, done) {
1✔
138
    const src = this.real_path(filepath, 'images');
24✔
139
    let buffer;
24✔
140

24✔
141
    try {
24✔
142
      buffer = fs.readFileSync(src);
24✔
143
    } catch (err) {
24✔
144
      throw new Error(`image_width failed to read '${src}': ${err}`, {
3✔
145
        cause: err
3✔
146
      });
3✔
147
    }
3✔
148
    
21✔
149
    done(imageSize(buffer).width);
21✔
150
  }
24✔
151

1✔
152
  image_height (filepath, done) {
1✔
153
    const src = this.real_path(filepath, 'images');
21✔
154
    let buffer;
21✔
155

21✔
156
    try {
21✔
157
      buffer = fs.readFileSync(src);
21✔
158
    } catch (err) {
21✔
159
      throw new Error(`image_height failed to read '${src}': ${err}`, {
3✔
160
        cause: err
3✔
161
      });
3✔
162
    }
3✔
163

18✔
164
    done(imageSize(buffer).height);
18✔
165
  }
21✔
166
  
1✔
167
  inline_image (filepath, mime_type, done) {
1✔
168
    const src = this.real_path(filepath, 'images');
42✔
169
  
42✔
170
    mime_type = mime_type || mime.lookup(src);
42✔
171

42✔
172
    if (!mime_type) {
42✔
173
      throw new Error(`Could not find mime type for filepath '${filepath}'`);
3✔
174
    }
3✔
175

39✔
176
    let data;
39✔
177

39✔
178
    try {
39✔
179
      data = fs.readFileSync(src);
39✔
180
    } catch (err) {
42✔
181
      throw new Error(`inline_image failed to read '${src}': ${err}`, {
3✔
182
        cause: err
3✔
183
      });
3✔
184
    }
3✔
185

36✔
186
    done(`data:${mime_type};base64,${data.toString('base64')}`);
36✔
187
  }
42✔
188
  
1✔
189
  asset_url (filepath, segment, done) {
1✔
190
    let fragment = '';
174✔
191
    let sanitized_http_path = this.http_path(filepath, segment);
174✔
192
    const real_path = this.real_path(filepath, segment);
174✔
193
    const fragmentIndex = sanitized_http_path.indexOf('#');
174✔
194

174✔
195
    const restoreFragment = url => done(url + fragment);
174✔
196
    const next = http_path => {
174✔
197
      if (this.options.asset_host) {
168✔
198
        this.asset_host(http_path, restoreFragment);
51✔
199
      } else {
168✔
200
        restoreFragment(http_path);
117✔
201
      }
117✔
202
    }
168✔
203

174✔
204
    if (~fragmentIndex) {
174✔
205
      fragment = sanitized_http_path.substring(fragmentIndex);
57✔
206
      sanitized_http_path = sanitized_http_path.substring(0, fragmentIndex);
57✔
207
    }
57✔
208

174✔
209
    if (this.options.asset_cache_buster) {
174✔
210
      this.asset_cache_buster(sanitized_http_path, real_path, next);
96✔
211
    } else {
174✔
212
      next(sanitized_http_path);
78✔
213
    }
78✔
214
  }
174✔
215
  
1✔
216
  image_url (filepath, done) {
1✔
217
    this.asset_url(filepath, 'images', done);
60✔
218
  }
60✔
219
  
1✔
220
  font_url (filepath, done) {
1✔
221
    this.asset_url(filepath, 'fonts', done);
114✔
222
  }
114✔
223

1✔
224
  font_files (files, done) {
1✔
225
    const processed_files = [];
21✔
226
    let count = 0;
21✔
227
  
21✔
228
    const complete = (index, type) => {
21✔
229
      return url => {
75✔
230
        processed_files[index] = {url: url, type: type};
72✔
231
        if (++count == files.length) {
72✔
232
          done(processed_files);
18✔
233
        }
18✔
234
      };
75✔
235
    };
21✔
236
  
21✔
237
    let i = 0, parts, ext, file, next, type;
21✔
238
    for (; i < files.length; ++i) {
21✔
239
      file = files[i];
75✔
240
      next = files[i + 1];
75✔
241
  
75✔
242
      if (FONT_TYPES[next]) {
75✔
243
        type = files.splice(i + 1, 1);
21✔
244
      } else {
75✔
245
        parts = this.parse_url(file);
54✔
246
        ext = path.extname(parts.pathname);
54✔
247
        type = ext.substring(1);
54✔
248
      }
54✔
249
      type = FONT_TYPES[type];
75✔
250
      this.font_url(file, complete(i, type));
75✔
251
    }
75✔
252
  }
21✔
253

1✔
254
  lookup (keys, done) {
1✔
255
    let data = this.options.data;
201✔
256
    for (let key of keys) {
201✔
257
      data = data[key];
327✔
258
    }
327✔
259

201✔
260
    done(data !== this.options.data ? data : null);
201✔
261
  }
201✔
262
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc