Skip to content

Commit 2a32c47

Browse files
committed
init
0 parents  commit 2a32c47

18 files changed

+643
-0
lines changed

.editorconfig

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# This file is for unifying the coding style for different editors and IDEs.
2+
# More information at http://EditorConfig.org
3+
4+
# No .editorconfig files above the root directory
5+
root = true
6+
7+
[*]
8+
indent_style = space
9+
indent_size = 2
10+
charset = utf-8
11+
trim_trailing_whitespace = true
12+
insert_final_newline = true
13+
14+
[package.json]
15+
indent_size = 2

.gitignore

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
lib-cov
2+
*.seed
3+
*.log
4+
*.csv
5+
*.dat
6+
*.out
7+
*.pid
8+
*.gz
9+
10+
pids
11+
logs
12+
results
13+
14+
npm-debug.log
15+
/node_modules
16+
coverage
17+
.idea
18+
/tests/runtime/*

.npmignore

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
lib-cov
2+
*.seed
3+
*.log
4+
*.csv
5+
*.dat
6+
*.out
7+
*.pid
8+
*.gz
9+
10+
pids
11+
logs
12+
results
13+
14+
npm-debug.log
15+
node_modules
16+
coverage
17+
examples
18+
test
19+
.idea

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# fast-sass-loader
2+
3+
fast sass loader for webpack. 5~10 times faster than **sass-loader**, and support url resolve.
4+
5+
## Notice
6+
7+
fast-sass-loader do not support these features:
8+
9+
- **sourceMap**, fast-ass-loader merge sass files by itself, so the source map will be incorrect.
10+
- **resolve-url-loader**, since fast-sass-loader already support url resolve, resolve-url-loader can be removed from you loader list.
11+

cache.js

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use strict'
2+
3+
const utils = require('./utils')
4+
5+
// 编译缓存
6+
// cache = {
7+
// <entry>: {
8+
// mtime: <Number>, // 修改时间
9+
// writeTimes: <Number>, // 编译次数
10+
// readTimes: <Number>, // 读取次数 (自最后一次编译)
11+
// lastCompile: <Number>, // 最后一次编译
12+
// result: <String>, // 编译结果
13+
// dependencies: { // 依赖文件状态
14+
// <file>: <Number>, // 依赖文件以及修改时间
15+
// ...
16+
// }
17+
// },
18+
// ...
19+
// }
20+
const CACHE_STORE = {}
21+
22+
23+
/**
24+
* Cache
25+
*
26+
* Usage:
27+
*
28+
* let cache = new Cache(entry)
29+
*
30+
* if (cache.isValid()) {
31+
* return cache.read()
32+
* } else {
33+
* // compile sass ....
34+
* cache.write(dependencies, result.css.toString())
35+
* }
36+
*/
37+
class Cache {
38+
39+
constructor(entry) {
40+
this.entry = entry
41+
}
42+
43+
isValid() {
44+
if (!(this.entry in CACHE_STORE)) {
45+
return false
46+
}
47+
48+
let cache = CACHE_STORE[this.entry]
49+
let estat = utils.fstat(this.entry)
50+
51+
// 文件不存在, 或时间不正确
52+
if (!estat || estat.mtime.getTime() !== cache.mtime) {
53+
return false
54+
}
55+
56+
for (let depFile in cache.dependencies) {
57+
if (!cache.dependencies.hasOwnProperty(depFile)) {
58+
continue
59+
}
60+
61+
let mtime = cache.dependencies[depFile]
62+
let dstat = utils.fstat(depFile)
63+
64+
if (!dstat || dstat.mtime.getTime() !== mtime) {
65+
return false
66+
}
67+
}
68+
69+
return true
70+
}
71+
72+
read() {
73+
if (this.entry in CACHE_STORE) {
74+
let cache = CACHE_STORE[this.entry]
75+
cache.readTimes++
76+
77+
return cache.result
78+
} else {
79+
return false
80+
}
81+
}
82+
83+
write(dependencies, result) {
84+
let cache = CACHE_STORE[this.entry]
85+
86+
if (!cache) {
87+
CACHE_STORE[this.entry] = cache = {
88+
mtime: 0,
89+
writeTimes: 0,
90+
readTimes: 0,
91+
lastCompile: Date.now(),
92+
result: null,
93+
dependencies: {}
94+
}
95+
}
96+
97+
cache.mtime = utils.fstat(this.entry).mtime.getTime()
98+
cache.writeTimes++
99+
cache.readTimes = 0
100+
cache.result = result
101+
cache.dependencies = {}
102+
103+
for (let i = 0; i < dependencies.length; i++) {
104+
let depFile = dependencies[i]
105+
let dstat = utils.fstat(depFile)
106+
107+
cache.dependencies[depFile] = dstat ? dstat.mtime.getTime() : 0
108+
}
109+
}
110+
}
111+
112+
module.exports = Cache

index.js

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
'use strict'
2+
3+
const path = require('path')
4+
const fs = require('fs-promise')
5+
const preview = require('cli-source-preview')
6+
const replaceAsync = require('./replace')
7+
const co = require('co')
8+
const async = require('async')
9+
const sass = require('node-sass')
10+
const Cache = require('./cache')
11+
const loaderUtils = require('loader-utils')
12+
13+
const EXT_PRECEDENCE = ['.scss', '.sass', '.css'];
14+
const MATCH_URL_ALL = /url\(\s*(['"]?)([^ '"\(\)]+)(\1)\s*\)/g;
15+
const MATCH_IMPORTS = /@import\s+(['"])([^,;'"]+)(\1)(\s*,\s*(['"])([^,;'"]+)(\1))*\s*;/g;
16+
const MATCH_FILES = /(['"])([^,;'"]+)(\1)/g;
17+
const BAD_URL_FILE = /(^#)|(^(\w+:)?\/\/)|(^data:)/
18+
19+
function getImportsToResolve(original) {
20+
let extname = path.extname(original)
21+
let basename = path.basename(original, extname)
22+
let dirname = path.dirname(original)
23+
24+
let imports = []
25+
let names = [basename]
26+
let exts = [extname]
27+
28+
if (!extname) {
29+
exts = EXT_PRECEDENCE
30+
}
31+
if (basename[0] !== '_') {
32+
names.push('_' + basename)
33+
}
34+
35+
for (let i = 0; i < names.length; i++) {
36+
for (let j = 0; j < exts.length; j++) {
37+
imports.push(path.join(dirname, names[i] + exts[j]))
38+
}
39+
}
40+
41+
return imports
42+
}
43+
44+
function getLoaderConfig(loaderContext) {
45+
let query = loaderUtils.parseQuery(loaderContext.query);
46+
let configKey = query.config || 'sassLoader';
47+
let config = loaderContext.options[configKey] || {};
48+
49+
delete query.config;
50+
51+
return Object.assign({}, config, query);
52+
}
53+
54+
function* mergeSources(opts, entry, resolve, dependencies, level) {
55+
level = level || 0
56+
dependencies = dependencies || []
57+
58+
let content = false
59+
60+
if (typeof entry === 'object') {
61+
content = entry.content
62+
entry = entry.file
63+
} else {
64+
content = yield fs.readFile(entry, 'utf8')
65+
}
66+
67+
let entryDir = path.dirname(entry)
68+
69+
// replace url(...)
70+
content = content.replace(MATCH_URL_ALL, (total, left, file, right) => {
71+
if (!file.match(BAD_URL_FILE)) {
72+
let absoluteFile = path.resolve(entryDir, file)
73+
let relativeFile = path.relative(opts.baseDir, absoluteFile)
74+
75+
return `url(${relativeFile})`
76+
} else {
77+
return total
78+
}
79+
})
80+
81+
// replace @import "..."
82+
function* importReplacer(total) {
83+
let contents = []
84+
let matched
85+
86+
// must reset lastIndex
87+
MATCH_FILES.lastIndex = 0
88+
89+
while (matched = MATCH_FILES.exec(total)) {
90+
let originalImport = matched[2].trim()
91+
if (!originalImport) {
92+
throw new Error(`import file cannot be empty: "${total}" @${entry}`)
93+
}
94+
95+
let imports = getImportsToResolve(originalImport)
96+
let resolvedImport
97+
98+
// console.log('resolve ->', imports)
99+
100+
for (let i = 0; i < imports.length; i++) {
101+
try {
102+
let reqFile = loaderUtils.urlToRequest(imports[i], opts.root)
103+
resolvedImport = yield resolve(entryDir, reqFile)
104+
break;
105+
} catch (err) {
106+
// console.log('resolve err: ', err.message)
107+
// skip
108+
}
109+
}
110+
111+
if (!resolvedImport) {
112+
throw new Error(`import file cannot be resolved: "${total}" @${entry}`)
113+
}
114+
115+
resolvedImport = path.normalize(resolvedImport)
116+
117+
if (dependencies.indexOf(resolvedImport) < 0) {
118+
dependencies.push(resolvedImport)
119+
120+
contents.push(yield mergeSources(opts, resolvedImport, resolve, dependencies, level + 1))
121+
}
122+
}
123+
124+
return contents.join('\n')
125+
}
126+
127+
return yield replaceAsync(content, MATCH_IMPORTS, co.wrap(importReplacer))
128+
}
129+
130+
module.exports = function(content) {
131+
let entry = this.resourcePath
132+
let callback = this.async()
133+
let cache = new Cache(entry)
134+
let options = getLoaderConfig(this)
135+
let ctx = this
136+
137+
options.baseDir = path.dirname(entry)
138+
139+
this.cacheable()
140+
141+
function resolver(ctx) {
142+
return function(dir, importFile) {
143+
return new Promise((resolve, reject) => {
144+
ctx.resolve(dir, importFile, (err, resolvedFile) => {
145+
if (err) {
146+
reject(err)
147+
} else {
148+
resolve(resolvedFile)
149+
}
150+
})
151+
})
152+
}
153+
}
154+
155+
return co(function*() {
156+
let dependencies = []
157+
158+
if (cache.isValid()) {
159+
return cache.read()
160+
} else {
161+
console.time('merge')
162+
let merged = yield mergeSources(options, {
163+
file: entry,
164+
content: content
165+
}, resolver(ctx), dependencies)
166+
167+
console.timeEnd('merge')
168+
169+
try {
170+
console.time('compile')
171+
172+
let result = yield new Promise((resolve, reject) => {
173+
sass.render({
174+
file: entry,
175+
data: merged
176+
}, (err, result) => {
177+
if (err) {
178+
reject(err)
179+
} else {
180+
resolve(result)
181+
}
182+
})
183+
})
184+
185+
dependencies.forEach(file => {
186+
ctx.dependency(file)
187+
})
188+
189+
let css = result.css.toString()
190+
191+
cache.write(dependencies, css)
192+
193+
console.timeEnd('compile')
194+
195+
return css
196+
} catch (err) {
197+
console.log(preview(merged, err, {
198+
offset: 10
199+
}))
200+
console.error(err.stack || err)
201+
202+
err.file && ctx.dependency(err.file)
203+
throw err
204+
}
205+
}
206+
}).then(css => {
207+
callback(null, css)
208+
}, err => {
209+
callback(err)
210+
})
211+
}

0 commit comments

Comments
 (0)