-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathindex.ts
371 lines (340 loc) · 13.6 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
import EyeglassCompiler = require('broccoli-eyeglass');
import Eyeglass = require('eyeglass');
import findHost from "./findHost";
import funnel = require('broccoli-funnel');
import MergeTrees = require('broccoli-merge-trees');
import * as path from 'path';
import * as url from 'url';
import cloneDeep = require('lodash.clonedeep');
import defaultsDeep = require('lodash.defaultsdeep');
import {BroccoliSymbolicLinker} from "./broccoli-ln-s";
import debugGenerator = require("debug");
import BroccoliDebug = require("broccoli-debug");
import { URL } from '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");
interface EyeglassProjectInfo {
apps: Array<any>;
}
interface EyeglassAddonInfo {
name: string;
parentPath: string;
isApp: boolean;
app: any;
assets?: BroccoliSymbolicLinker;
}
interface EyeglassAppInfo {
assets?: BroccoliSymbolicLinker;
sessionCache: Map<string, string | number>;
}
interface GlobalEyeglassData {
infoPerAddon: WeakMap<object, EyeglassAddonInfo>;
infoPerApp: WeakMap<object, EyeglassAppInfo>;
projectInfo: EyeglassProjectInfo;
}
const g: typeof global & {EYEGLASS?: GlobalEyeglassData} = 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: any): boolean {
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: any): boolean {
return config.embroiderEnabled ?? false;
}
//eslint-disable-next-line @typescript-eslint/no-explicit-any
function getDefaultAssetHttpPrefix(parent: any, config: any): string {
// 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): Array<{path: string}> {
let paths = new Array<{path: string}>();
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(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 BroccoliSymbolicLinker({}, {annotation: app.name, persistentOutput: true})
});
}
let addonInfo = {
isApp,
name,
parentPath,
app,
isEngine: isEngine(this.parent),
assets: new 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: Array<ReturnType<typeof funnel> | EyeglassCompiler | BroccoliSymbolicLinker> = [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: string, httpRoot: string, destUri: string, config: any): string {
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: ConstructorParameters<typeof EyeglassCompiler>[1], 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: any): boolean {
let keywords: Array<string> = appOrAddon._packageInfo.pkg.keywords || new Array<string>();
return keywords.includes("ember-engine");
}
function convertURLToPath(fragment: string): string {
return (new URL(`file://${fragment}`)).pathname;
}
export = EMBER_CLI_EYEGLASS;