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