-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathbase.ts
290 lines (249 loc) · 8.11 KB
/
base.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
import * as path from 'path';
import * as fs from 'fs';
import * as Funnel from 'broccoli-funnel';
import findPlugins from 'find-plugins';
import { Tree } from 'broccoli';
import * as MergeTree from 'broccoli-merge-trees';
import * as writeFile from 'broccoli-file-creator';
import { sync as readPkg } from 'read-pkg';
import AddonBuilder from './addon';
import globify from '../utils/globify';
import UnitTests from '../trees/unit-tests';
import * as createDebug from 'debug';
import { ExtracterTree as DocumentationExtracter } from '@denali-js/documenter';
// import { debug } from 'broccoli-stew';
const debug = createDebug('@denali-js/cli:builder');
export interface BuilderOptions {
environment: string;
parent?: BaseBuilder;
docs?: boolean;
}
export default class BaseBuilder {
/**
* Creates the appropriate builder instance for the given directory.
*
* @param dir
* @param environment
* @param parent
*/
static createFor(dir: string, options: BuilderOptions): BaseBuilder {
debug(`creating builder for ${ dir }`);
let localBuilderPath = path.join(dir, 'denali-build');
let Builder: typeof BaseBuilder;
// Use the local denali-build.js file if present
if (fs.existsSync(localBuilderPath + '.js')) {
let LocalBuilderModule = require(localBuilderPath);
Builder = LocalBuilderModule.default || LocalBuilderModule;
debug('using local builder');
// Dummy apps
} else if (path.basename(dir) === 'dummy') {
Builder = require('./dummy').default;
debug('using default dummy builder');
} else {
let pkg = readPkg(dir);
// Addons
if (pkg.keywords && pkg.keywords.includes('denali-addon')) {
Builder = require('./addon').default;
debug('using default addon builder');
// Apps
} else {
Builder = require('./app').default;
debug('using default app builder');
}
}
return new Builder(dir, options);
}
dir: string;
environment: string;
addons: AddonBuilder[];
pkg: any;
ejections: Map<string, Tree[]> = new Map();
processSelf: (tree: Tree, dir: string) => Tree;
unitTestDir = path.join('test', 'unit');
packageFiles: string[] = [];
private _cachedTree: Tree;
protected debug: (msg: string) => void;
protected options: BuilderOptions;
get logicalDependencyPath() {
let builder: BaseBuilder = this;
let depPath = [ builder.pkg.name ];
while (builder = builder.options.parent) {
depPath.unshift(builder.pkg.name);
}
return depPath;
}
constructor(dir: string, options: BuilderOptions) {
this.dir = dir;
this.environment = options.environment;
this.options = options;
this.pkg = readPkg(dir);
this.debug = createDebug(`@denali-js/cli:builder:${ this.logicalDependencyPath.join('>') }`);
this.debug(`created builder for ${ this.pkg.name }@${ this.pkg.version }`);
}
/**
* Look for addons in the node_modules folder. Only search packages explicitly
* mentioned in package.json, and look for the `denali-addon` keyword in their
* package.json's. Then create a Builder for each one.
*/
protected discoverAddons(options: { include: string[] } = { include: [] }): AddonBuilder[] {
this.debug(`searching for child addons in ${ this.dir }`);
return findPlugins({
dir: this.dir,
keyword: 'denali-addon',
sort: true,
includeDev: true,
configName: 'denali',
include: options.include
}).map((addon) => {
this.debug(`discovered child addon: ${ addon.pkg.name }`);
return <AddonBuilder>BaseBuilder.createFor(addon.dir, {
environment: this.environment,
parent: this
});
});
}
/**
* Which directories should be considered "source" directories to be fed into
* the main build pipeline?
*/
protected sources(): (string | Tree)[] {
let dirs = [ 'app', 'config', 'lib', 'blueprints', 'commands', 'config', 'guides', 'test' ];
return dirs;
}
/**
* Which directories should be bundled into runtime bundles/fragments?
*/
protected bundledSources(): string[] {
let dirs = [ 'app', 'config', 'lib' ];
return dirs;
}
/**
* Assemble the main build pipeline
*/
assembleTree() {
this.addons = this.discoverAddons();
let baseTree = this.toBaseTree();
let finalTrees: Tree[] = [];
if (this.shouldBuildDocs()) {
let docsTree = this.docs(baseTree);
finalTrees.push(docsTree);
}
let compiledTree = this.compile(baseTree);
finalTrees.push(compiledTree);
let bundleTree = new Funnel(compiledTree, {
include: globify(this.bundledSources()),
annotation: 'combined tree (bundled files)'
});
bundleTree = this.bundle(bundleTree);
finalTrees.push(bundleTree);
if (this.environment === 'test') {
this.debug('including unit tests in output');
let unitTestsTree = this.compileUnitTests(compiledTree);
finalTrees.push(unitTestsTree);
}
finalTrees.push(this.packageFilesTree());
let tree = new MergeTree(finalTrees, { overwrite: true });
return tree;
}
/**
* Create trees that copy top level files over. Broccoli can't pick up
* top level files one-off, because Broccoli can't do one-off files.
* Which means Broccoli would have to watch the root directory, which
* includes the tmp directory where intermediate build steps are stored,
* resulting in an infinite loop (watch triggers build, touches tmp,
* triggers watch).
*/
packageFilesTree() {
let files = this.packageFiles;
return new MergeTree(files.map((filepath) => {
let sourcepath = path.join(this.dir, filepath);
return writeFile(filepath, fs.readFileSync(sourcepath, 'utf-8'));
}));
}
shouldBuildDocs(): boolean {
return this.options.docs;
}
docs(baseTree: Tree): Tree {
return new DocumentationExtracter(baseTree, {
projectName: this.pkg.name,
projectVersion: this.pkg.version,
pagesDir: 'docs',
sourceDirs: [ 'app', 'lib' ]
});
}
/**
* Compile the unit tests - see UnitTestsTree for more details
*/
compileUnitTests(compiledTree: Tree) {
let unitTestsTree = new Funnel(compiledTree, {
include: globify([this.unitTestDir]),
annotation: 'unit tests'
});
return new UnitTests(unitTestsTree, {
bundleName: this.unitTestBundleName(),
baseDir: this.dir,
sourceRoot: this.unitTestDir
});
}
unitTestBundleName() {
return 'app';
}
/**
* Wrapper method over assembleTree, used to cache the results
*/
toTree() {
if (!this._cachedTree) {
this._cachedTree = this.assembleTree();
}
return this._cachedTree;
}
/**
* Create a single base tree from the source directories. Multiple
* consumers can use this base tree to ensure deduplication of the
* starting point.
*/
protected toBaseTree(): Tree {
let sources = this.sources();
this.debug(`creating base tree from: ${ sources.map((s) => `${s}/`).join(',') }`);
sources = sources.map((dir) => {
if (typeof dir === 'string') {
let localpath = path.join(this.dir, dir);
if (fs.existsSync(localpath)) {
return new Funnel(localpath, { destDir: dir });
}
return false;
}
return dir;
}).filter(Boolean);
return new MergeTree(sources, { overwrite: true, annotation: 'baseTree' });
}
/**
* Compile the project. Defaults to running the process* hooks, but
* can be extended to do more.
*/
protected compile(tree: Tree): Tree {
this.debug('compiling');
tree = this.processHooks(tree);
return tree;
}
/**
* Run processSelf and processParent hooks
*/
protected processHooks(tree: Tree): Tree {
this.debug('running hooks');
if (this.processSelf) {
this.debug('running processSelf');
tree = this.processSelf(tree, this.dir);
}
this.addons.forEach((addonBuilder) => {
if (addonBuilder.processParent) {
this.debug(`running processParent hook from ${ addonBuilder.pkg.name }`);
tree = addonBuilder.processParent(tree, this.dir);
}
});
return tree;
}
bundle(tree: Tree): Tree {
throw new Error('Bundle method not implemented!');
}
}