1 | import EyeglassCompiler = require('broccoli-eyeglass');
|
2 | import Eyeglass = require('eyeglass');
|
3 | import findHost from "./findHost";
|
4 | import Funnel = require('broccoli-funnel');
|
5 | import MergeTrees = require('broccoli-merge-trees');
|
6 | import * as path from 'path';
|
7 | import * as url from 'url';
|
8 | import cloneDeep = require('lodash.clonedeep');
|
9 | import defaultsDeep = require('lodash.defaultsdeep');
|
10 | import {BroccoliSymbolicLinker} from "./broccoli-ln-s";
|
11 | import debugGenerator = require("debug");
|
12 | import { URL } from 'url';
|
13 |
|
14 | const debug = debugGenerator("ember-cli-eyeglass");
|
15 | const debugSetup = debug.extend("setup");
|
16 | const debugBuild = debug.extend("build");
|
17 | const debugCache = debug.extend("cache");
|
18 | const debugAssets = debug.extend("assets");
|
19 |
|
20 | interface EyeglassProjectInfo {
|
21 | apps: Array<any>;
|
22 | }
|
23 | interface EyeglassAddonInfo {
|
24 | name: string;
|
25 | parentPath: string;
|
26 | isApp: boolean;
|
27 | app: any;
|
28 | }
|
29 |
|
30 | interface EyeglassAppInfo {
|
31 | assets: BroccoliSymbolicLinker;
|
32 | sessionCache: Map<string, string | number>;
|
33 | }
|
34 |
|
35 | interface GlobalEyeglassData {
|
36 | infoPerAddon: WeakMap<object, EyeglassAddonInfo>;
|
37 | infoPerApp: WeakMap<object, EyeglassAppInfo>;
|
38 | projectInfo: EyeglassProjectInfo;
|
39 | }
|
40 |
|
41 | const g: typeof global & {EYEGLASS?: GlobalEyeglassData} = global;
|
42 |
|
43 | if (!g.EYEGLASS) {
|
44 | g.EYEGLASS = {
|
45 | infoPerAddon: new WeakMap(),
|
46 | infoPerApp: new WeakMap(),
|
47 | projectInfo: {
|
48 | apps: []
|
49 | }
|
50 | }
|
51 | }
|
52 |
|
53 | const EYEGLASS_INFO_PER_ADDON = g.EYEGLASS.infoPerAddon;
|
54 | const EYEGLASS_INFO_PER_APP = g.EYEGLASS.infoPerApp;
|
55 | const APPS = g.EYEGLASS.projectInfo.apps;
|
56 |
|
57 |
|
58 | function isLazyEngine(addon: any): boolean {
|
59 | if (addon.lazyLoading === true) {
|
60 |
|
61 | return true;
|
62 | }
|
63 | if (addon.lazyLoading && addon.lazyLoading.enabled === true) {
|
64 | return true;
|
65 | }
|
66 | return false;
|
67 | }
|
68 |
|
69 |
|
70 | function getDefaultAssetHttpPrefix(parent: any): string {
|
71 |
|
72 |
|
73 |
|
74 | let current = parent;
|
75 |
|
76 | while (current.parent) {
|
77 | if (isLazyEngine(current)) {
|
78 |
|
79 | return `engines-dist/${current.name}/assets`;
|
80 | }
|
81 | current = current.parent;
|
82 | }
|
83 |
|
84 |
|
85 | return 'assets';
|
86 | }
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 | function localEyeglassAddons(addon): Array<{path: string}> {
|
99 | let paths = new Array<{path: string}>();
|
100 |
|
101 | if (typeof addon.addons !== 'object' ||
|
102 | typeof addon.addonPackages !== 'object') {
|
103 | return paths;
|
104 | }
|
105 |
|
106 | let packages = Object.keys(addon.addonPackages);
|
107 |
|
108 | for (let i = 0; i < packages.length; i++) {
|
109 | let p = addon.addonPackages[packages[i]];
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 | if (p.pkg.keywords.some(kw => kw == 'eyeglass-module')) {
|
116 | paths.push({ path: p.path })
|
117 | }
|
118 | }
|
119 |
|
120 |
|
121 | for (let i = 0; i < addon.addons.length; i++) {
|
122 | paths = paths.concat(localEyeglassAddons(addon.addons[i]));
|
123 | }
|
124 | return paths;
|
125 | }
|
126 |
|
127 | const EMBER_CLI_EYEGLASS = {
|
128 | name: require("../package.json").name,
|
129 | included(parent) {
|
130 | this._super.included.apply(this, arguments);
|
131 | this.initSelf();
|
132 | },
|
133 | initSelf() {
|
134 | if (EYEGLASS_INFO_PER_ADDON.has(this)) return;
|
135 | let app = findHost(this);
|
136 | if (!app) return;
|
137 | let isApp = (this.app === app);
|
138 | let name = app.name;
|
139 | if (!isApp) {
|
140 | let thisName = typeof this.parent.name === "function" ? this.parent.name() : this.parent.name;
|
141 | name = `${name}/${thisName}`
|
142 | }
|
143 | let parentPath = this.parent.root;
|
144 | debugSetup("Initializing %s with eyeglass support for %s at %s", isApp ? "app" : "addon", name, parentPath);
|
145 | if (isApp) {
|
146 | APPS.push(app);
|
147 |
|
148 |
|
149 |
|
150 |
|
151 | EYEGLASS_INFO_PER_APP.set(app, {
|
152 | sessionCache: new Map(),
|
153 | assets: new BroccoliSymbolicLinker({}, {annotation: app.name, persistentOutput: true})
|
154 | });
|
155 | }
|
156 | let addonInfo = {isApp, name, parentPath, app};
|
157 | EYEGLASS_INFO_PER_ADDON.set(this, addonInfo);
|
158 | },
|
159 | postBuild(_result) {
|
160 | debugBuild("Build Succeeded.");
|
161 | this._resetCaches();
|
162 | },
|
163 | _resetCaches() {
|
164 | debugCache("clearing eyeglass global cache");
|
165 | Eyeglass.resetGlobalCaches();
|
166 | for (let app of APPS) {
|
167 | let appInfo = EYEGLASS_INFO_PER_APP.get(app);
|
168 | appInfo.assets.reset();
|
169 | debugCache("clearing %d cached items from the eyeglass build cache for %s", appInfo.sessionCache.size, app.name);
|
170 | appInfo.sessionCache.clear();
|
171 | }
|
172 | },
|
173 | buildError(_error) {
|
174 | debugBuild("Build Failed.");
|
175 | this._resetCaches();
|
176 | },
|
177 | postprocessTree(type, tree) {
|
178 | let addonInfo = EYEGLASS_INFO_PER_ADDON.get(this);
|
179 | if (type === "all" && addonInfo.isApp) {
|
180 | debugBuild("Merging eyeglass asset tree with the '%s' tree", type);
|
181 | let appInfo = EYEGLASS_INFO_PER_APP.get(addonInfo.app);
|
182 | return new MergeTrees([tree, appInfo.assets], {overwrite: true});
|
183 | } else {
|
184 | return tree;
|
185 | }
|
186 | },
|
187 | setupPreprocessorRegistry(type, registry) {
|
188 | let addon = this;
|
189 |
|
190 | registry.add('css', {
|
191 | name: 'eyeglass',
|
192 | ext: 'scss',
|
193 | toTree: (tree, inputPath, outputPath) => {
|
194 |
|
195 | let cssDir = outputPath.slice(1) || './';
|
196 | let sassDir = inputPath.slice(1) || './';
|
197 | let {app, name} = EYEGLASS_INFO_PER_ADDON.get(this);
|
198 | let extracted = this.extractConfig(app, addon);
|
199 | extracted.cssDir = cssDir;
|
200 | extracted.sassDir = sassDir;
|
201 | const config = this.setupConfig(extracted);
|
202 | debugSetup("Broccoli Configuration for %s: %O", name, config)
|
203 | let httpRoot = config.eyeglass && config.eyeglass.httpRoot || "/";
|
204 | let compiler = new EyeglassCompiler(tree, config);
|
205 | compiler.events.on("cached-asset", (absolutePathToSource, httpPathToOutput) => {
|
206 | debugBuild("will symlink %s to %s", absolutePathToSource, httpPathToOutput);
|
207 | try {
|
208 | this.linkAsset(absolutePathToSource, httpRoot, httpPathToOutput);
|
209 | } catch (e) {
|
210 |
|
211 | }
|
212 | });
|
213 | return compiler;
|
214 | }
|
215 | });
|
216 | },
|
217 |
|
218 | extractConfig(host, addon) {
|
219 | const isNestedAddon = typeof addon.parent.parent === 'object';
|
220 |
|
221 | const hostConfig = cloneDeep(host.options.eyeglass || {});
|
222 | const addonConfig = isNestedAddon ? cloneDeep(addon.parent.options.eyeglass || {}) : {};
|
223 | return defaultsDeep(addonConfig, hostConfig);
|
224 | },
|
225 |
|
226 | linkAsset(srcFile: string, httpRoot: string, destUri: string): string {
|
227 | let rootPath = httpRoot.startsWith("/") ? httpRoot.substring(1) : httpRoot;
|
228 | let destPath = destUri.startsWith("/") ? destUri.substring(1) : destUri;
|
229 |
|
230 | if (process.platform === "win32") {
|
231 | destPath = convertURLToPath(destPath);
|
232 | rootPath = convertURLToPath(rootPath);
|
233 | }
|
234 |
|
235 | if (destPath.startsWith(rootPath)) {
|
236 | destPath = path.relative(rootPath, destPath);
|
237 | }
|
238 | let {app} = EYEGLASS_INFO_PER_ADDON.get(this);
|
239 | let {assets} = EYEGLASS_INFO_PER_APP.get(app);
|
240 | debugAssets("Will link asset %s to %s to expose it at %s relative to %s",
|
241 | srcFile, destPath, destUri, httpRoot);
|
242 | return assets.ln_s(srcFile, destPath);
|
243 | },
|
244 |
|
245 | setupConfig(config: ConstructorParameters<typeof EyeglassCompiler>[1], options) {
|
246 | let {isApp, app, parentPath} = EYEGLASS_INFO_PER_ADDON.get(this);
|
247 | let {sessionCache} = EYEGLASS_INFO_PER_APP.get(app);
|
248 | config.sessionCache = sessionCache;
|
249 | config.annotation = `EyeglassCompiler(${parentPath})`;
|
250 | if (!config.sourceFiles && !config.discover) {
|
251 | config.sourceFiles = [isApp ? 'app.scss' : 'addon.scss'];
|
252 | }
|
253 | config.assets = ['public', 'app'].concat(config.assets || []);
|
254 | config.eyeglass = config.eyeglass || {}
|
255 |
|
256 |
|
257 | config.eyeglass.httpRoot = config.eyeglass.httpRoot || config["httpRoot"];
|
258 | if (config.persistentCache) {
|
259 | let cacheDir = parentPath.replace(/\//g, "$");
|
260 | config.persistentCache += `/${cacheDir}`;
|
261 | }
|
262 |
|
263 | config.assetsHttpPrefix = config.assetsHttpPrefix || getDefaultAssetHttpPrefix(this.parent);
|
264 |
|
265 | if (config.eyeglass.modules) {
|
266 | config.eyeglass.modules =
|
267 | config.eyeglass.modules.concat(localEyeglassAddons(this.parent));
|
268 | } else {
|
269 | config.eyeglass.modules = localEyeglassAddons(this.parent);
|
270 | }
|
271 | let originalConfigureEyeglass = config.configureEyeglass;
|
272 | config.configureEyeglass = (eyeglass, sass, details) => {
|
273 | eyeglass.assets.installer((file, uri, fallbackInstaller, cb) => {
|
274 | try {
|
275 | cb(null, this.linkAsset(file, eyeglass.options.eyeglass.httpRoot || "/", uri))
|
276 | } catch (e) {
|
277 | cb(e);
|
278 | }
|
279 | });
|
280 | if (originalConfigureEyeglass) {
|
281 | originalConfigureEyeglass(eyeglass, sass, details);
|
282 | }
|
283 | };
|
284 |
|
285 |
|
286 |
|
287 | let originalGenerator = config.optionsGenerator;
|
288 | config.optionsGenerator = (sassFile, cssFile, sassOptions, compilationCallback) => {
|
289 | if (isApp) {
|
290 | cssFile = cssFile.replace(/app\.css$/, `${this.app.name}.css`);
|
291 | } else {
|
292 | cssFile = cssFile.replace(/addon\.css$/, `${this.parent.name}.css`);
|
293 | }
|
294 |
|
295 | if (originalGenerator) {
|
296 | originalGenerator(sassFile, cssFile, sassOptions, compilationCallback);
|
297 | } else {
|
298 | compilationCallback(cssFile, sassOptions);
|
299 | }
|
300 | };
|
301 |
|
302 | return config;
|
303 | }
|
304 | };
|
305 |
|
306 | function convertURLToPath(fragment: string): string {
|
307 | return (new URL(`file://${fragment}`)).pathname;
|
308 | }
|
309 | export = EMBER_CLI_EYEGLASS; |
\ | No newline at end of file |