-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.js
203 lines (185 loc) · 6.95 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
'use strict';
var _ = require('lodash');
var path = require('path');
var pathParse = require('path-parse');
var roux = require('@retailmenot/roux');
var Pantry = require('@retailmenot/roux/lib/pantry');
var util = require('util');
function importOnce(importCache, value) {
if (importCache[value.file]) {
return {
contents: ''
};
}
importCache[value.file] = true;
return value;
};
function getImportPath(importCache, pantry, ingredientName) {
var ingredient = pantry.ingredients[ingredientName];
if (!ingredient) {
return new Error(
util.format('No such ingredient "%s/%s"', pantry.name, ingredientName));
}
if (!ingredient.entryPoints.sass) {
return new Error(
util.format(
'"%s/%s" has no Sass entry point', pantry.name, ingredientName
)
);
}
return importOnce(importCache, {
file: path.resolve(ingredient.path, ingredient.entryPoints.sass.filename)
});
}
/**
* Get a node-sass custom importer[1] with the provided configuration
*
* The importer returns `NODE_SASS_NULL` unless it matches one of the following
* patterns:
*
* - @<namespace>/<pantry>/<ingredient>
* - <pantry>/<ingredient>
*
* If the value of `url` matches one of the above, the importer attempts to
* look up the Sass entry point for the named pantry and ingredient. If
* successful, an absolute path to the entry point is returned (possibly
* asynchronously).
*
* If the pantry is not found, an `Error` is returned. If the pantry is found
* but does not contain the named ingredient, an `Error` is returned. If both
* the pantry and ingredient are found but the ingredient does not have a Sass
* entry point, an `Error` is returned.
*
* The importer caches pantries it looks up. The cache can be primed by means of
* the optional `config.pantries` parameter. If the named pantry is cached,
* the above process completes synchronously. If not cached, the pantry is
* looked up in the locations named by `config.pantrySearchPaths`. The first
* matching pantry found is cached and the above process performed.
*
* @param {*} NODE_SASS_NULL The object to return when node-sass should do its
* thing. You should pass require('node-sass').NULL from your webpack config
* in the repo that's consuming this module.
* @param {Object} [config] - the importer configuration
* @param {Object} [config.pantries] - the cache of pantries to use, defaults to
* `{}`. Should be {name:pantry} mappings. If pantry is a string, it will be
* passed to `@retailmenot/roux.initialize` and cached across calls
* to the importer.
* @param {string[]} [config.pantrySearchPaths=['$CWD/node_modules'] - the paths
* to search for pantries in if not found in the cache
*
* [1]: https://github.com/sass/node-sass#importer--v200---experimental
*/
module.exports = function (NODE_SASS_NULL, config) {
if (arguments.length < 1) {
throw new Error('Argument NODE_SASS_NULL is required.');
}
config = roux.normalizeConfig(config);
config.pantries = _.mapValues(config.pantries, function (pantry, name) {
if (_.isString(pantry)) {
return roux.initialize({
name: name,
path: pantry
}, config);
}
return pantry;
});
/**
* node-sass custom importer[1] for ingredients in the Roux ecosystem.
*
* The importer will use `this` to store a cache of ingredient paths
* that it has already resolved, and return an empty string as the file's
* contents in cases where the file has already returned the file. This will
* effectively deduplicate the sass output.
*
* `node-sass` will:
*
* @param {Object} config - the importer configuration
* @param {Object} config.pantries - the cache of pantries to use
* @param {string[]} config.pantrySearchPaths - the paths to search for
* pantries in if not found in the cache
* @param {string} url - the original import path (provided by node-sass: see
* [1])
* @param {string} prev - the absolute path to the file importing `url`
* (provided by node-sass: see [1])
* @param {function} done - a callback function to invoke on async completion
* (provided by node-sass: see [1])
*
* [1]: https://github.com/sass/node-sass#importer--v200---experimental
*/
return function Importer(url, prev, done) {
var pantry;
this._rouxImportOnceCache = this._rouxImportOnceCache || {};
// If we are being asked to resolve a relative url, we want to let
// the sass importer do its thing by returning NODE_SASS_NULL, however
// if that relative URL refers to a file that is a child of a pantry,
// then we want to add it to this._rouxImportOnceCache, and prevent it from
// showing up in the output if we encounter an @import for that
// same file again. To do this, we resolve the relative `url` with
// respect to `prev` and if the resolved fully qualified URL is a child
// of one of the pantries in config.pantries, we apply our caching logic.
if (url.charAt(0) === '.') {
var baseDir = pathParse(prev).dir;
// this will lack the .scss extension so it does not necessarily
// map to a file on the filesystem
var absoluteImportUrl = path.resolve(baseDir, url);
// Check our pantries to see if the @import is refering to one of its
// children. If so, apply caching logic and return as appropriate.
var pantries = Object.keys(config.pantries);
for (var i = 0; i < pantries.length; ++i) {
pantry = config.pantries[pantries[i]];
if (!Pantry.isPantry(pantry)) {
continue;
}
// Figure out where it lives
var absolutePantryUrl = path.resolve(pantry.path);
// If the import is a child of the pantry, apply our caching logic
if (absoluteImportUrl.indexOf(absolutePantryUrl) === 0) {
return this._rouxImportOnceCache[absoluteImportUrl] === true ?
{
contents: ''
} :
NODE_SASS_NULL;
}
}
}
var parsedPath = roux.parseIngredientPath(url);
if (parsedPath == null) {
// url was not a valid ingredient name, so pass the @import path
// along unmodified
return NODE_SASS_NULL;
}
pantry = config.pantries[parsedPath.pantry];
// There are three posibilities:
// 1. The pantry already exists and is initialized
// 2. The pantry is a promise that will resolve to the initialized pantry
// 3. The pantry is completely undefined
// This if block handles cases (2, 3)
if (!pantry || (pantry && _.isFunction(pantry.then))) {
// The promise that will ultimately resolve to a pantry
// is either the pantry (case 2), which is already a promise,
// or the promise returned by calling roux.resolve (case 3).
(pantry || roux.resolve(parsedPath.pantry, config))
.then(function (pantry) {
config.pantries[parsedPath.pantry] = pantry;
done(
getImportPath(
this._rouxImportOnceCache,
pantry,
parsedPath.ingredient
)
);
}.bind(this))
.catch(function (errs) {
var err = new Error(util.format('Failed to resolve %s', url));
err.errors = errs;
done(err);
});
return undefined;
}
return getImportPath(
this._rouxImportOnceCache,
pantry,
parsedPath.ingredient
);
};
};