ember-cli-eyeglass
Version:
Use eyeglass and node-sass to compile Sass files.
344 lines • 15.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
const EyeglassCompiler = require("broccoli-eyeglass");
const Eyeglass = require("eyeglass");
const findHost_1 = __importDefault(require("./findHost"));
const funnel = require("broccoli-funnel");
const MergeTrees = require("broccoli-merge-trees");
const path = __importStar(require("path"));
const cloneDeep = require("lodash.clonedeep");
const defaultsDeep = require("lodash.defaultsdeep");
const broccoli_ln_s_1 = require("./broccoli-ln-s");
const debugGenerator = require("debug");
const BroccoliDebug = require("broccoli-debug");
const url_1 = require("url");
const debug = debugGenerator("ember-cli-eyeglass");
const debugSetup = debug.extend("setup");
const debugBuild = debug.extend("build");
const debugCache = debug.extend("cache");
const debugAssets = debug.extend("assets");
const g = global;
if (!g.EYEGLASS) {
g.EYEGLASS = {
infoPerAddon: new WeakMap(),
infoPerApp: new WeakMap(),
projectInfo: {
apps: [],
}
};
}
const EYEGLASS_INFO_PER_ADDON = g.EYEGLASS.infoPerAddon;
const EYEGLASS_INFO_PER_APP = g.EYEGLASS.infoPerApp;
const APPS = g.EYEGLASS.projectInfo.apps;
//eslint-disable-next-line @typescript-eslint/no-explicit-any
function isLazyEngine(addon) {
if (addon.lazyLoading === true) {
// pre-ember-engines 0.5.6 lazyLoading flag
return true;
}
if (addon.lazyLoading && addon.lazyLoading.enabled === true) {
return true;
}
return false;
}
//eslint-disable-next-line @typescript-eslint/no-explicit-any
function embroiderEnabled(config) {
var _a;
return (_a = config.embroiderEnabled) !== null && _a !== void 0 ? _a : false;
}
//eslint-disable-next-line @typescript-eslint/no-explicit-any
function getDefaultAssetHttpPrefix(parent, config) {
// the default http prefix differs between Ember app and lazy Ember engine
// iterate over the parent's chain and look for a lazy engine or there are
// no more parents, which means we've reached the Ember app project
let current = parent;
while (current.parent) {
if (isLazyEngine(current) && !embroiderEnabled(config)) {
// only lazy engines will inline their assets in the engines-dist folder
return `engines-dist/${current.name}/assets`;
}
else if (isEngine(current)) {
return `${current.name}/assets`;
}
current = current.parent;
}
// at this point, the highlevel container is Ember app and we should use the default 'assets' prefix
return 'assets';
}
/* addon.addons forms a tree(graph?) of addon objects that allow us to traverse the
* ember addon dependencies. However there's no path information in the addon object,
* but each addon object has some disconnected metadata in addon.addonPackages
* with the path info. Unfortunately there's no shared information that would
* allow us to connect which addon packages are actually which addon objects.
* It would be great if ember-cli didn't throw that information on the ground
* while building these objects. It would also be marvelous if we knew which
* addons came from a local addon declaration and which ones came from node
* modules.
**/
function localEyeglassAddons(addon) {
let paths = new Array();
if (typeof addon.addons !== 'object' ||
typeof addon.addonPackages !== 'object') {
return paths;
}
let packages = Object.keys(addon.addonPackages);
for (let i = 0; i < packages.length; i++) {
let p = addon.addonPackages[packages[i]];
// Note: this will end up creating manual addons for things in node modules
// that are actually auto discovered, these manual modules will get deduped
// out. but we need to add all of them because the some eyeglass modules
// for addons & engines won't get autodiscovered otherwise unless the
// addons/engines are themselves eyeglass modules (which we don't want to require).
if (p.pkg.keywords.some(kw => kw == 'eyeglass-module')) {
paths.push({ path: p.path });
}
}
// TODO: if there's a cycle in the addon graph don't recurse.
for (let i = 0; i < addon.addons.length; i++) {
paths = paths.concat(localEyeglassAddons(addon.addons[i]));
}
return paths;
}
const EMBER_CLI_EYEGLASS = {
name: require("../package.json").name,
included(parent) {
this._super.included.apply(this, arguments);
this.initSelf();
},
initSelf() {
if (EYEGLASS_INFO_PER_ADDON.has(this))
return;
let app = findHost_1.default(this);
if (!app)
return;
let isApp = (this.app === app);
let name = app.name;
if (!isApp) {
let thisName = typeof this.parent.name === "function" ? this.parent.name() : this.parent.name;
name = `${name}/${thisName}`;
}
let parentPath = this.parent.root;
debugSetup("Initializing %s with eyeglass support for %s at %s", isApp ? "app" : "addon", name, parentPath);
if (isApp) {
APPS.push(app);
// we create the symlinker in persistent mode because there's not a good
// way yet to recreate the symlinks when sass files are cached. I would
// worry about it more but it seems like the dist directory is cumulative
// across builds anyway.
EYEGLASS_INFO_PER_APP.set(app, {
sessionCache: new Map(),
assets: new broccoli_ln_s_1.BroccoliSymbolicLinker({}, { annotation: app.name, persistentOutput: true })
});
}
let addonInfo = {
isApp,
name,
parentPath,
app,
isEngine: isEngine(this.parent),
assets: new broccoli_ln_s_1.BroccoliSymbolicLinker({}, {
annotation: `Eyeglass Assets for ${app.name}/${name}`,
persistentOutput: true,
}),
};
EYEGLASS_INFO_PER_ADDON.set(this, addonInfo);
},
postBuild(_result) {
debugBuild("Build Succeeded.");
this._resetCaches();
},
_resetCaches() {
debugCache("clearing eyeglass global cache");
Eyeglass.resetGlobalCaches();
for (let app of APPS) {
let appInfo = EYEGLASS_INFO_PER_APP.get(app);
let addonInfo = EYEGLASS_INFO_PER_ADDON.get(this);
let extracted = this.extractConfig(addonInfo.app, this);
if (!embroiderEnabled(extracted)) {
appInfo.assets.reset();
}
debugCache("clearing %d cached items from the eyeglass build cache for %s", appInfo.sessionCache.size, app.name);
appInfo.sessionCache.clear();
}
},
buildError(_error) {
debugBuild("Build Failed.");
this._resetCaches();
},
postprocessTree(type, tree) {
let addon = this;
let addonInfo = EYEGLASS_INFO_PER_ADDON.get(this);
let extracted = this.extractConfig(addonInfo.app, addon);
// This code is intended only for classic ember builds.
// (There's no "all" postprocess tree in embroider.)
// Skip this entirely for embroider builds (we'll include
// assets on a per-addon basis in the CSS tree).
if (embroiderEnabled(extracted)) {
return tree;
}
if (type === "all" && addonInfo.isApp) {
debugBuild("Merging eyeglass asset tree with the '%s' tree", type);
let appInfo = EYEGLASS_INFO_PER_APP.get(addonInfo.app);
return new MergeTrees([tree, appInfo.assets], { overwrite: true });
}
else {
return tree;
}
},
setupPreprocessorRegistry(type, registry) {
let addon = this;
registry.add('css', {
name: 'eyeglass',
ext: 'scss',
toTree: (tree, inputPath, outputPath) => {
// These start with a slash and that messes things up.
let cssDir = outputPath.slice(1) || './';
let sassDir = inputPath.slice(1) || './';
let { app, name, isApp } = EYEGLASS_INFO_PER_ADDON.get(this);
tree = new BroccoliDebug(tree, `ember-cli-eyeglass:${name}:input`);
let extracted = this.extractConfig(app, addon);
extracted.cssDir = cssDir;
extracted.sassDir = sassDir;
const config = this.setupConfig(extracted);
debugSetup("Broccoli Configuration for %s: %O", name, config);
let httpRoot = config.eyeglass && config.eyeglass.httpRoot || "/";
let addonInfo = EYEGLASS_INFO_PER_ADDON.get(this);
let compiler = new EyeglassCompiler(tree, config);
compiler.events.on("cached-asset", (absolutePathToSource, httpPathToOutput) => {
debugBuild("will symlink %s to %s", absolutePathToSource, httpPathToOutput);
try {
this.linkAsset(absolutePathToSource, httpRoot, httpPathToOutput);
}
catch (e) {
// pass this only happens with a cache after downgrading ember-cli.
}
});
if (embroiderEnabled(config)) {
compiler.events.on("build", () => {
addonInfo.assets.reset();
});
}
let withoutSassFiles = funnel(tree, {
srcDir: (isApp && !embroiderEnabled(config)) ? 'app/styles' : undefined,
destDir: isApp ? 'assets' : undefined,
exclude: ['**/*.s{a,c}ss'],
allowEmpty: true,
});
let trees = [withoutSassFiles, compiler];
if (embroiderEnabled(config)) {
// Push the addon assets tree on to this build only if we're using embroider.
// (For classic builds, the postprocess tree will handle this.)
trees.push(addonInfo.assets);
}
let result = new MergeTrees(trees, {
overwrite: true
});
return new BroccoliDebug(result, `ember-cli-eyeglass:${name}:output`);
}
});
},
extractConfig(host, addon) {
const isNestedAddon = typeof addon.parent.parent === 'object';
// setup eyeglass for this project's configuration
const hostConfig = cloneDeep(host.options.eyeglass || {});
const addonConfig = isNestedAddon ? cloneDeep(addon.parent.options.eyeglass || {}) : {};
return defaultsDeep(addonConfig, hostConfig);
},
linkAsset(srcFile, httpRoot, destUri, config) {
let rootPath = httpRoot.startsWith("/") ? httpRoot.substring(1) : httpRoot;
let destPath = destUri.startsWith("/") ? destUri.substring(1) : destUri;
if (process.platform === "win32") {
destPath = convertURLToPath(destPath);
rootPath = convertURLToPath(rootPath);
}
if (destPath.startsWith(rootPath)) {
destPath = path.relative(rootPath, destPath);
}
let assets;
if (embroiderEnabled(config)) {
assets = EYEGLASS_INFO_PER_ADDON.get(this).assets;
}
else {
let { app } = EYEGLASS_INFO_PER_ADDON.get(this);
assets = EYEGLASS_INFO_PER_APP.get(app).assets;
}
debugAssets("Will link asset %s to %s to expose it at %s relative to %s", srcFile, destPath, destUri, httpRoot);
return assets.ln_s(srcFile, destPath);
},
setupConfig(config, options) {
let { isApp, app, parentPath } = EYEGLASS_INFO_PER_ADDON.get(this);
let { sessionCache } = EYEGLASS_INFO_PER_APP.get(app);
config.sessionCache = sessionCache;
config.annotation = `EyeglassCompiler(${parentPath})`;
if (!config.sourceFiles && !config.discover) {
config.sourceFiles = [isApp ? 'app.scss' : 'addon.scss'];
}
config.assets = ['public', 'app'].concat(config.assets || []);
config.eyeglass = config.eyeglass || {};
// XXX We don't set the root anywhere but I'm not sure what might break if we do.
// config.eyeglass.root = parentPath;
config.eyeglass.httpRoot = config.eyeglass.httpRoot || config["httpRoot"];
if (config.persistentCache) {
let cacheDir = parentPath.replace(/\//g, "$");
config.persistentCache += `/${cacheDir}`;
}
config.assetsHttpPrefix = config.assetsHttpPrefix || getDefaultAssetHttpPrefix(this.parent, config);
if (config.eyeglass.modules) {
config.eyeglass.modules =
config.eyeglass.modules.concat(localEyeglassAddons(this.parent));
}
else {
config.eyeglass.modules = localEyeglassAddons(this.parent);
}
let originalConfigureEyeglass = config.configureEyeglass;
config.configureEyeglass = (eyeglass, sass, details) => {
eyeglass.assets.installer((file, uri, fallbackInstaller, cb) => {
try {
cb(null, this.linkAsset(file, eyeglass.options.eyeglass.httpRoot || "/", uri, config));
}
catch (e) {
cb(e);
}
});
if (originalConfigureEyeglass) {
originalConfigureEyeglass(eyeglass, sass, details);
}
};
// If building an app, rename app.css to <project>.css per Ember conventions.
// Otherwise, we're building an addon, so rename addon.css to <name-of-addon>.css.
let originalGenerator = config.optionsGenerator;
config.optionsGenerator = (sassFile, cssFile, sassOptions, compilationCallback) => {
if (isApp) {
cssFile = cssFile.replace(/app\.css$/, `${this.app.name}.css`);
}
else {
cssFile = cssFile.replace(/addon\.css$/, `${this.parent.name}.css`);
}
if (originalGenerator) {
originalGenerator(sassFile, cssFile, sassOptions, compilationCallback);
}
else {
compilationCallback(cssFile, sassOptions);
}
};
return config;
}
};
function isEngine(appOrAddon) {
let keywords = appOrAddon._packageInfo.pkg.keywords || new Array();
return keywords.includes("ember-engine");
}
function convertURLToPath(fragment) {
return (new url_1.URL(`file://${fragment}`)).pathname;
}
module.exports = EMBER_CLI_EYEGLASS;
//# sourceMappingURL=index.js.map