UNPKG

8.45 kBJavaScriptView Raw
1"use strict";
2/* eslint-disable no-underscore-dangle */
3/**
4 * Shameless adapted from [Eyeglass](https://github.com/sass-eyeglass/eyeglass)
5 * because I wanted a general-use import-once importer for Node
6 * */
7const fs = require("fs");
8const path = require("path");
9/**
10 * All imports use the forward slash as a directory
11 * delimeter. This function converts to the filesystem's
12 * delimeter if it uses an alternate.
13 * */
14const makeFsPath = function makeFsPath(importPath) {
15 let fsPath = importPath;
16 if (path.sep !== "/") {
17 fsPath = fsPath.replace(/\//, path.sep);
18 }
19 return fsPath;
20};
21/**
22 * Determines if a file should be imported or not
23 * */
24const importOnce = function importOnce(data, done) {
25 if (this._importOnceCache[data.file]) {
26 done({
27 contents: "",
28 filename: `already-imported:${data.file}`,
29 });
30 }
31 else {
32 this._importOnceCache[data.file] = true;
33 done(data);
34 }
35};
36/**
37 * Sass imports are usually in an abstract form in that
38 * they leave off the partial prefix and the suffix.
39 * This code creates the possible extensions, whether it is a partial
40 * and whether it is a directory index file having those
41 * same possible variations. If the import contains an extension,
42 * then it is left alone.
43 *
44 * */
45const getFileNames = function getFileNames(abstractName) {
46 const names = [];
47 let directory;
48 let basename;
49 if ([".scss", ".sass"].indexOf(path.extname(abstractName)) !== -1) {
50 directory = path.dirname(abstractName);
51 basename = path.basename(abstractName);
52 ["", "_"].forEach((prefix) => {
53 names.push(path.join(directory, prefix + basename));
54 });
55 }
56 else if (path.extname(abstractName)) {
57 names.push(abstractName);
58 }
59 else {
60 directory = path.dirname(abstractName);
61 basename = path.basename(abstractName);
62 // Standard File Names
63 ["", "_"].forEach((prefix) => {
64 [".scss", ".sass"].forEach((ext) => {
65 names.push(path.join(directory, prefix + basename + ext));
66 });
67 });
68 // Index Files
69 if (this.options.importOnce.index) {
70 ["", "_"].forEach((prefix) => {
71 [".scss", ".sass"].forEach((ext) => {
72 names.push(path.join(abstractName, `${prefix}index${ext}`));
73 });
74 });
75 }
76 // CSS Files
77 if (this.options.importOnce.css) {
78 names.push(`${abstractName}.css`);
79 }
80 }
81 return names;
82};
83/**
84 * getIn
85 * */
86const getIncludePaths = function getIncludePaths(uri) {
87 // From https://github.com/sass/node-sass/issues/762#issuecomment-80580955
88 const arr = this.options.includePaths.split(path.delimiter);
89 const gfn = getFileNames.bind(this);
90 let paths = [];
91 arr.forEach((includePath) => {
92 paths = paths.concat(gfn(path.resolve(process.cwd(), includePath, uri)));
93 });
94 return paths;
95};
96/**
97 * Parse JSON into Sass
98 * */
99const parseJSON = function parseJSON(data, filename) {
100 let fileReturn = `$${path.basename(filename).replace(path.extname(filename), "")}:`;
101 let colors;
102 let stringData = data.toString();
103 stringData = stringData.replace(/{/g, "(");
104 stringData = stringData.replace(/\[/g, "(");
105 stringData = stringData.replace(/}/g, ")");
106 stringData = stringData.replace(/]/g, ")");
107 fileReturn += stringData;
108 if (fileReturn.substr(fileReturn.length - 1) === "\n") {
109 fileReturn = fileReturn.slice(0, -1);
110 }
111 fileReturn += ";";
112 // ////////////////////////////
113 // Hex colors
114 // ////////////////////////////
115 colors = fileReturn.match(/"(#([0-9a-f]{3}){1,2})"/g);
116 if (colors) {
117 colors.forEach((color) => {
118 fileReturn = fileReturn.replace(color, color.slice(1, -1));
119 });
120 }
121 // ////////////////////////////
122 // RGB/A Colors
123 // ////////////////////////////
124 colors = fileReturn.match(/"(rgb|rgba)\((\d{1,3}), (\d{1,3}), (\d{1,3})\)"/g);
125 if (colors) {
126 colors.forEach((color) => {
127 fileReturn = fileReturn.replace(color, color.slice(1, -1));
128 });
129 }
130 // ////////////////////////////
131 // HSL/A Colors
132 // ////////////////////////////
133 // ////////////////////////////
134 // RGB/A Colors
135 // ////////////////////////////
136 colors = fileReturn.match(/"(hsl|hsla)\((\d{1,3}), (\d{1,3}), (\d{1,3})\)"/g);
137 if (colors) {
138 colors.forEach((color) => {
139 fileReturn = fileReturn.replace(color, color.slice(1, -1));
140 });
141 }
142 return Buffer.from(fileReturn);
143};
144/**
145 * Asynchronously walks the file list until a match is found. If
146 * no matches are found, calls the callback with an error
147 * */
148const readFirstFile = function readFirstFile(uri, fileNames, css, cb, examinedFiles) {
149 /* eslint-disable no-param-reassign */
150 const filename = fileNames.shift();
151 examinedFiles = examinedFiles || [];
152 examinedFiles.push(filename);
153 fs.readFile(filename, (err, data) => {
154 if (err) {
155 if (fileNames.length) {
156 readFirstFile(uri, fileNames, css, cb, examinedFiles);
157 }
158 else {
159 cb(new Error(`Could not import \`${uri}\` from any of the following locations:
160 \n ${examinedFiles.join("\n ")}`));
161 }
162 }
163 else {
164 if ([".js", ".json"].indexOf(path.extname(filename)) !== -1) {
165 data = parseJSON(data, filename);
166 }
167 cb(null, {
168 contents: data.toString(),
169 file: filename,
170 });
171 }
172 });
173};
174// This is a bootstrap function for calling readFirstFile.
175const readAbstractFile = function readAbstractFile(uri, abstractName, cb) {
176 const gfn = getFileNames.bind(this);
177 const gip = getIncludePaths.bind(this);
178 const css = Object.assign({}, this.options.importOnce);
179 let files = gfn(abstractName);
180 if (this.options.includePaths) {
181 files = files.concat(gip(uri));
182 }
183 readFirstFile(uri, files, css, cb);
184};
185/**
186 * Import the goodies!
187 * */
188function createImporter(nodeModules) {
189 return function importer(uri, prev, done) {
190 if (uri[0] === "~" && nodeModules) {
191 uri = nodeModules + uri.slice(1);
192 }
193 const isRealFile = fs.existsSync(prev);
194 const io = importOnce.bind(this);
195 const raf = readAbstractFile.bind(this);
196 let file;
197 // Ensure options are available
198 if (!this.options.importOnce) {
199 this.options.importOnce = {};
200 }
201 // Set default index import
202 if (!this.options.importOnce.index) {
203 this.options.importOnce.index = false;
204 }
205 // Set default bower import
206 if (!this.options.importOnce.bower) {
207 this.options.importOnce.bower = false;
208 }
209 // Set default css import
210 if (!this.options.importOnce.css) {
211 this.options.importOnce.css = false;
212 }
213 // Create an import cache if it doesn't exist
214 if (!this._importOnceCache) {
215 this._importOnceCache = {};
216 }
217 if (isRealFile) {
218 file = path.resolve(path.dirname(prev), makeFsPath(uri));
219 raf(uri, file, (err, data) => {
220 if (err) {
221 // eslint-disable-next-line no-console
222 console.log(err.toString());
223 done({});
224 }
225 else {
226 io(data, done);
227 }
228 });
229 }
230 else {
231 raf(uri, process.cwd(), (err, data) => {
232 if (err) {
233 // TODO here we need to throw? or call something from webpack to show errors
234 // eslint-disable-next-line no-console
235 console.log(err.toString());
236 done({});
237 }
238 else {
239 io(data, done);
240 }
241 });
242 }
243 };
244}
245/**
246 * Exports the importer
247 * */
248module.exports = (customNodeModulesPath = "") => {
249 const pathHasEndWithSlash = customNodeModulesPath[customNodeModulesPath.length - 1] === "/";
250 const newCustomNodeModulesPath = !pathHasEndWithSlash ? `${customNodeModulesPath}/` : customNodeModulesPath;
251 return createImporter(newCustomNodeModulesPath);
252};