-
Notifications
You must be signed in to change notification settings - Fork 86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Refactor loader dependencies #119
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
{ | ||
"presets": [ | ||
["env", {"targets": {"node": "6"}}] | ||
["@babel/preset-env", {"targets": {"node": "6"}}] | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,112 +1,32 @@ | ||
// Copied from https://github.com/css-modules/css-modules-loader-core | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can move the loader and parser files to the |
||
|
||
import postcss from "postcss"; | ||
import fs from "fs"; | ||
import path from "path"; | ||
|
||
import Parser from "./parser"; | ||
|
||
class Core { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I love to see the Core is gone |
||
constructor(plugins) { | ||
this.plugins = plugins || Core.defaultPlugins; | ||
export function resolveRelativeImport(importee, importer, projectRoot) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems to be an internal function. Let's not export it then There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, of course. good catch :) |
||
const importerDir = path.dirname(importer); | ||
let pathFromProjectRoot = path.resolve(projectRoot, importerDir, importee); | ||
|
||
// if the path is not relative or absolute, try to resolve it in node_modules | ||
if (importee[0] !== "." && importee[0] !== "/") { | ||
try { | ||
pathFromProjectRoot = require.resolve(importee); | ||
} catch (e) { | ||
// noop | ||
} | ||
} | ||
|
||
load(sourceString, sourcePath, trace, pathFetcher) { | ||
let parser = new Parser(pathFetcher, trace); | ||
|
||
return postcss(this.plugins.concat([parser.plugin()])) | ||
.process(sourceString, { from: "/" + sourcePath }) | ||
.then((result) => { | ||
return { | ||
injectableSource: result.css, | ||
exportTokens: parser.exportTokens, | ||
}; | ||
}); | ||
} | ||
return pathFromProjectRoot; | ||
} | ||
|
||
// Sorts dependencies in the following way: | ||
// AAA comes before AA and A | ||
// AB comes after AA and before A | ||
// All Bs come after all As | ||
// This ensures that the files are always returned in the following order: | ||
// - In the order they were required, except | ||
// - After all their dependencies | ||
const traceKeySorter = (a, b) => { | ||
if (a.length < b.length) { | ||
return a < b.substring(0, a.length) ? -1 : 1; | ||
} else if (a.length > b.length) { | ||
return a.substring(0, b.length) <= b ? -1 : 1; | ||
} else { | ||
return a < b ? -1 : 1; | ||
} | ||
}; | ||
|
||
export default class FileSystemLoader { | ||
constructor(root, plugins) { | ||
this.root = root; | ||
this.sources = {}; | ||
this.traces = {}; | ||
this.importNr = 0; | ||
this.core = new Core(plugins); | ||
this.tokensByFile = {}; | ||
} | ||
|
||
fetch(_newPath, relativeTo, _trace) { | ||
let newPath = _newPath.replace(/^["']|["']$/g, ""), | ||
trace = _trace || String.fromCharCode(this.importNr++); | ||
export default { | ||
resolveId: (importee, importer, projectRoot) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice idea to use |
||
return resolveRelativeImport(importee, importer, projectRoot) | ||
}, | ||
resolve: (importee) => { | ||
return new Promise((resolve, reject) => { | ||
let relativeDir = path.dirname(relativeTo), | ||
rootRelativePath = path.resolve(relativeDir, newPath), | ||
fileRelativePath = path.resolve( | ||
path.join(this.root, relativeDir), | ||
newPath | ||
); | ||
Comment on lines
-59
to
-64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @madyankin There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe I had the insight a couple of years ago or so, but I'm not quite sure now. AFAIR, this is useful when you have CSS modules placed in some other location and want to resolve them properly, e.g., in Rails or Django frameworks. These files were in the |
||
|
||
// if the path is not relative or absolute, try to resolve it in node_modules | ||
if (newPath[0] !== "." && newPath[0] !== "/") { | ||
try { | ||
fileRelativePath = require.resolve(newPath); | ||
} catch (e) { | ||
// noop | ||
} | ||
} | ||
|
||
const tokens = this.tokensByFile[fileRelativePath]; | ||
if (tokens) { | ||
return resolve(tokens); | ||
} | ||
|
||
fs.readFile(fileRelativePath, "utf-8", (err, source) => { | ||
fs.readFile(importee, "utf-8", (err, source) => { | ||
if (err) reject(err); | ||
this.core | ||
.load(source, rootRelativePath, trace, this.fetch.bind(this)) | ||
.then(({ injectableSource, exportTokens }) => { | ||
this.sources[fileRelativePath] = injectableSource; | ||
this.traces[trace] = fileRelativePath; | ||
this.tokensByFile[fileRelativePath] = exportTokens; | ||
resolve(exportTokens); | ||
}, reject); | ||
else resolve(source); | ||
}); | ||
}); | ||
} | ||
|
||
get finalSource() { | ||
const traces = this.traces; | ||
const sources = this.sources; | ||
let written = new Set(); | ||
|
||
return Object.keys(traces) | ||
.sort(traceKeySorter) | ||
.map((key) => { | ||
const filename = traces[key]; | ||
if (written.has(filename)) { | ||
return null; | ||
} | ||
written.add(filename); | ||
|
||
return sources[filename]; | ||
}) | ||
.join(""); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,22 @@ | ||
// Copied from https://github.com/css-modules/css-modules-loader-core | ||
import replaceSymbols from "icss-replace-symbols"; | ||
import postcss from 'postcss'; | ||
|
||
const importRegexp = /^:import\((.+)\)$/; | ||
import replaceSymbols from "icss-replace-symbols"; | ||
|
||
export default class Parser { | ||
constructor(pathFetcher, trace) { | ||
this.pathFetcher = pathFetcher; | ||
constructor(root, plugins, loader, trace, ctx = {}) { | ||
this.root = root; | ||
this.plugins = plugins; | ||
this.loader = loader; | ||
this.plugin = this.plugin.bind(this); | ||
this.exportTokens = {}; | ||
this.translations = {}; | ||
this.trace = trace; | ||
this.trace = trace || ''; | ||
|
||
this.sources = ctx.sources || {}; | ||
this.traces = ctx.traces || {}; | ||
this.tokensByFile = ctx.tokensByFile || {}; | ||
} | ||
|
||
plugin() { | ||
|
@@ -26,10 +33,11 @@ export default class Parser { | |
|
||
fetchAllImports(css) { | ||
let imports = []; | ||
let trace = 0; | ||
css.each((node) => { | ||
if (node.type == "rule" && node.selector.match(importRegexp)) { | ||
imports.push( | ||
this.fetchImport(node, css.source.input.from, imports.length) | ||
this.fetchImport(node, css.source.input.from, trace++) | ||
); | ||
} | ||
}); | ||
|
@@ -62,19 +70,74 @@ export default class Parser { | |
exportNode.remove(); | ||
} | ||
|
||
fetchImport(importNode, relativeTo, depNr) { | ||
let file = importNode.selector.match(importRegexp)[1], | ||
depTrace = this.trace + String.fromCharCode(depNr); | ||
return this.pathFetcher(file, relativeTo, depTrace).then( | ||
(exports) => { | ||
importNode.each((decl) => { | ||
if (decl.type == "decl") { | ||
this.translations[decl.prop] = exports[decl.value]; | ||
} | ||
}); | ||
importNode.remove(); | ||
}, | ||
(err) => console.log(err) | ||
); | ||
async fetchImport(importNode, relativeTo, depNr) { | ||
const file = importNode.selector.match(importRegexp)[1].replace(/^["']|["']$/g, ""), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's split the declarations into separate statements everywhere for the readability sake |
||
depTrace = this.trace + String.fromCharCode(65 + depNr), // 65 = A, making the trace more readable | ||
id = this.loader.resolveId(file, relativeTo, this.root), | ||
subParser = new Parser(this.root, this.plugins, this.loader, depTrace, { | ||
sources: this.sources, | ||
traces: this.traces, | ||
tokensByFile: this.tokensByFile | ||
}); | ||
let tokens = this.tokensByFile[id]; | ||
|
||
try { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be cleaner now 👍 |
||
if (!tokens) { | ||
const content = await this.loader.resolve(id); | ||
const { css } = await postcss(this.plugins.concat([subParser.plugin()])) | ||
.process(content, { from: id }); | ||
tokens = subParser.exportTokens; | ||
|
||
this.sources[id] = css; | ||
this.traces[depTrace] = id; | ||
this.tokensByFile[id] = tokens; | ||
} | ||
|
||
importNode.each((decl) => { | ||
if (decl.type == "decl") { | ||
this.translations[decl.prop] = tokens[decl.value]; | ||
} | ||
}); | ||
importNode.remove(); | ||
} catch (e) { | ||
console.error(e); | ||
} | ||
} | ||
|
||
|
||
get finalSource() { | ||
const traces = this.traces; | ||
const sources = this.sources; | ||
let written = new Set(); | ||
|
||
return Object.keys(traces) | ||
.sort(traceKeySorter) | ||
.map((key) => { | ||
const filename = traces[key]; | ||
if (written.has(filename)) { | ||
return null; | ||
} | ||
written.add(filename); | ||
|
||
return sources[filename]; | ||
}) | ||
.join(""); | ||
} | ||
} | ||
|
||
// Sorts dependencies in the following way: | ||
// AAA comes before AA and A | ||
// AB comes after AA and before A | ||
// All Bs come after all As | ||
// This ensures that the files are always returned in the following order: | ||
// - In the order they were required, except | ||
// - After all their dependencies | ||
const traceKeySorter = (a, b) => { | ||
if (a.length < b.length) { | ||
return a < b.substring(0, a.length) ? -1 : 1; | ||
} else if (a.length > b.length) { | ||
return a.substring(0, b.length) <= b ? -1 : 1; | ||
} else { | ||
return a < b ? -1 : 1; | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍