Skip to content

Commit

Permalink
Allow importing .css files from node_modules directories.
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben Newman committed Apr 7, 2016
1 parent 94d03ee commit b4fe0d5
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 18 deletions.
18 changes: 13 additions & 5 deletions tools/isobuild/compiler-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Fiber from 'fibers';
import {sourceMapLength} from '../utils/utils.js';
import {Console} from '../console/console.js';
import ImportScanner from './import-scanner.js';
import {cssToCommonJS} from "./css-modules.js";

import { isTestFilePath } from './test-files.js';

Expand Down Expand Up @@ -482,15 +483,22 @@ class ResourceSlot {
// unconditionally as a CSS resource, so that it can be imported
// when needed.
resource.type = "js";
resource.data = new Buffer(
'module.exports = require("meteor/modules").addStyles(' +
JSON.stringify(data) + ');\n',
'utf8'
);
resource.data =
new Buffer(cssToCommonJS(data, resource.hash), "utf8");

self.jsOutputResources.push(resource);

} else {
// Eager CSS is added unconditionally to a combined <style> tag at
// the beginning of the <head>. If the corresponding module ever
// gets imported, its module.exports object should be an empty stub,
// rather than a <style> node added dynamically to the <head>.
self.addJavaScript({
...options,
data: "// These styles have already been applied to the document.\n",
lazy: true
});

resource.type = "css";
resource.data = new Buffer(data, 'utf8'),

Expand Down
65 changes: 65 additions & 0 deletions tools/isobuild/css-modules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import LRU from "lru-cache";
import { load as isoLoad } from "../tool-env/isopackets.js";

const CACHE = new LRU({
max: 1024 * 1024,
length(value) {
// The 40 here is the length of the hash key, and the value is the
// CommonJS string that cssToCommonJS returns.
return 40 + value.length;
}
});

export function cssToCommonJS(css, hash) {
if (hash && CACHE.has(hash)) {
return CACHE.get(hash);
}

const { parseCss, stringifyCss } =
isoLoad("css-tools")["minifier-css"].CssTools;

const ast = parseCss(css);
const rules = ast.stylesheet.rules;
const lines = [];
const earlyRules = [];

rules.some((rule, i) => {
if (rule.type === "rule") {
rules.splice(0, i, ...earlyRules);
// Once the actual CSS rules start, there can be no more @import
// directives, so we can stop collecting earlyRules.
return true;
}

if (rule.type === "import") {
// Require the imported .css file, but omit the @import directive
// from earlyRules, so that it won't be loaded that way.
lines.push("require(" + rule.import + ");");

// TODO Handle url(...).
// TODO Convert to CommonJS relative identifier syntax.
// TODO Handle media queries with matchMedia.
} else {
// Early rules (i.e. rules that come before the first normal CSS
// rule) are typically either @import directives or comments.
earlyRules.push(rule);
}
});

lines.push(
'module.exports = require("meteor/modules").addStyles(',
" " + JSON.stringify(stringifyCss(ast, {
// TODO Only compress if building a production bundle.
compress: true })),
");",
""
);

const commonJS = lines.join("\n");

if (hash) {
CACHE.set(hash, commonJS);
}

return commonJS;
}
14 changes: 12 additions & 2 deletions tools/isobuild/import-scanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import {sha1, readAndWatchFileWithHash} from "../fs/watch.js";
import {matches as archMatches} from "../utils/archinfo.js";
import {findImportedModuleIdentifiers} from "./js-analyze.js";
import {cssToCommonJS} from "./css-modules.js";
import buildmessage from "../utils/buildmessage.js";
import LRU from "lru-cache";
import {Profile} from "../tool-env/profile.js";
Expand Down Expand Up @@ -55,6 +56,10 @@ const defaultExtensionHandlers = {
return "module.exports = " +
JSON.stringify(JSON.parse(dataString), null, 2) +
";\n";
},

".css"(dataString, hash) {
return cssToCommonJS(dataString, hash);
}
};

Expand Down Expand Up @@ -127,7 +132,9 @@ export default class ImportScanner {

const dotExt = "." + file.type;
const dataString = file.data.toString("utf8");
file.dataString = defaultExtensionHandlers[dotExt](dataString);
file.dataString = defaultExtensionHandlers[dotExt](
dataString, file.hash);

if (! (file.data instanceof Buffer) ||
file.dataString !== dataString) {
file.data = new Buffer(file.dataString, "utf8");
Expand Down Expand Up @@ -424,7 +431,10 @@ export default class ImportScanner {
}

const ext = pathExtname(absPath).toLowerCase();
info.dataString = defaultExtensionHandlers[ext](info.dataString);
info.dataString = defaultExtensionHandlers[ext](
info.dataString,
info.hash,
);

if (info.dataString !== dataString) {
info.data = new Buffer(info.dataString, "utf8");
Expand Down
12 changes: 5 additions & 7 deletions tools/tests/apps/modules/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,11 @@ describe("css modules", () => {
"none"
);

let error;
try {
require("./eager.css");
} catch (expected) {
error = expected;
}
assert.ok(error instanceof Error);
// Eager CSS is added unconditionally to a combined <style> tag at the
// beginning of the <head>. If the corresponding module ever gets
// imported, its module.exports object should be an empty stub, rather
// than a <style> node added dynamically to the <head>.
assert.deepEqual(require("./eager.css"), {});
});

it("should be importable by an app", () => {
Expand Down
9 changes: 5 additions & 4 deletions tools/tool-env/isopackets.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@ var Profile = require('./profile.js').Profile;

// All of the defined isopackets. Whenever they are being built, they will be
// built in the order listed here.
var ISOPACKETS = {
const ISOPACKETS = {
'ddp': ['ddp-client'],
'mongo': ['npm-mongo'],
'ejson': ['ejson'],
'constraint-solver': ['constraint-solver'],
'cordova-support': ['boilerplate-generator', 'logging', 'webapp-hashing',
'xmlbuilder'],
'logging': ['logging']
'logging': ['logging'],
'css-tools': ['minifier-css'],
};

// Caches isopackets in memory (each isopacket only needs to be loaded
Expand All @@ -76,7 +77,7 @@ var loadedIsopackets = {};
// The main entry point: loads and returns an isopacket from cache or from
// disk. Does not do a build step: ensureIsopacketsLoadable must be called
// first!
var load = function (isopacketName) {
export function load(isopacketName) {
return fiberHelpers.noYieldsAllowed(function () {
if (_.has(loadedIsopackets, isopacketName)) {
if (loadedIsopackets[isopacketName]) {
Expand All @@ -97,7 +98,7 @@ var load = function (isopacketName) {

throw Error("Unknown isopacket: " + isopacketName);
});
};
}

var isopacketPath = function (isopacketName) {
return files.pathJoin(config.getIsopacketRoot(), isopacketName);
Expand Down

0 comments on commit b4fe0d5

Please sign in to comment.