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

localnerve / sass-asset-functions / 25017454519

27 Apr 2026 08:20PM UTC coverage: 98.033%. First build
25017454519

Pull #114

github

web-flow
Merge 890fb0994 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%)

111.54 hits per line

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

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

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

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

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

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

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

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

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

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

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

270✔
78
      done(url.format(new_url));
270✔
79
    });
270✔
80
  }
288✔
81

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

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

3✔
94
  resolve_url (from, to) {
3✔
95
    let result;
135✔
96

135✔
97
    try {
135✔
98
      const resolvedUrl = new URL(to, new URL(from, 'resolve://bogus.local'));
135✔
99
      if (resolvedUrl.protocol === 'resolve:') {
135!
NEW
100
        // `from` is a relative URL.
×
NEW
101
        const { pathname, search, hash } = resolvedUrl;
×
NEW
102
        result = `${pathname}${search}${hash}`;
×
103
      } else {
135✔
104
        result = resolvedUrl.toString();
135✔
105
      }
135✔
106
    } catch {
135!
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

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

3✔
114
  parse_url (input) {
3✔
115
    let result;
567✔
116

567✔
117
    try {
567✔
118
      const parsedUrl = new URL(input, 'parse://bogus.local'); // base used if relative was supplied
567✔
119
      result = parsedUrl;
567✔
120
    } catch {
567!
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

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

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

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

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

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

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

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

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

117✔
176
    let data;
117✔
177

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

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

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

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

522✔
209
    if (this.options.asset_cache_buster) {
522✔
210
      this.asset_cache_buster(sanitized_http_path, real_path, next);
288✔
211
    } else {
522✔
212
      next(sanitized_http_path);
234✔
213
    }
234✔
214
  }
522✔
215
  
3✔
216
  image_url (filepath, done) {
3✔
217
    this.asset_url(filepath, 'images', done);
180✔
218
  }
180✔
219
  
3✔
220
  font_url (filepath, done) {
3✔
221
    this.asset_url(filepath, 'fonts', done);
342✔
222
  }
342✔
223

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

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

603✔
260
    done(data !== this.options.data ? data : null);
603✔
261
  }
603✔
262
}
3✔
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