UNPKG

16.6 kBJavaScriptView Raw
1var gulp = require('gulp');
2var fs = require('fs');
3var main = require('../index');
4var langConfig = require('./langConfig');
5var dependencies = require('./dependecies');
6var paths = require('./paths');
7var maven = require('./maven');
8var logger = require('./logger');
9var args = require('./args');
10var notifier = require('./notifier');
11var browserify = require('browserify');
12var source = require('vinyl-source-stream');
13var transformTools = require('browserify-transform-tools');
14var _string = require('underscore.string');
15var templates = require('./templates');
16var ModuleSpec = require('@jenkins-cd/js-modules/js/ModuleSpec');
17var entryModuleTemplate = templates.getTemplate('entry-module.hbs');
18
19var hasJenkinsJsModulesDependency = dependencies.hasJenkinsJsModulesDep();
20var preBundleListeners = [];
21var postBundleListeners = [];
22var globalImportMappings = [];
23var globalExportMappings = [];
24
25/**
26 * Add a listener to be called just before Browserify starts bundling.
27 * <p>
28 * The listener is called with the {@code bundle} as {@code this} and
29 * the {@code bundler} as as the only arg.
30 *
31 * @param listener The listener to add.
32 */
33exports.onPreBundle = function(listener) {
34 preBundleListeners.push(listener);
35};
36
37/**
38 * Add a listener to be called just after Browserify finishes bundling.
39 * <p>
40 * The listener is called with the {@code bundle} as {@code this} and
41 * the location of the generated bundle as as the only arg.
42 *
43 * @param listener The listener to add.
44 */
45exports.onPostBundle = function(listener) {
46 postBundleListeners.push(listener);
47};
48
49exports.addGlobalImportMapping = function(mapping) {
50 globalImportMappings.push(mapping);
51};
52
53exports.addGlobalExportMapping = function(mapping) {
54 globalExportMappings.push(mapping);
55};
56
57exports.doJSBundle = function(bundle, applyImports) {
58 if (!bundle.bundleInDir) {
59 var adjunctBase = setAdjunctInDir(bundle);
60 logger.logInfo('Javascript bundle "' + bundle.as + '" will be available in Jenkins as adjunct "' + adjunctBase + '.' + bundle.as + '".')
61 }
62
63 // Add all global mappings.
64 if (!bundle.globalModuleMappingsApplied) {
65 if (bundle.useGlobalImportMappings === true) {
66 for (var i = 0; i < globalImportMappings.length; i++) {
67 bundle._import(globalImportMappings[i]);
68 }
69 }
70 if (bundle.useGlobalExportMappings === true) {
71 if (main.bundleCount() > 1 && globalExportMappings.length > 0) {
72 logger.logError('Unable to apply bundle dependency "export" configurations from package.json because there are multiple bundles being generated.');
73 logger.logError(' (exporting the same package from multiple bundles is not permitted)');
74 logger.logError(' TIP: From inside gulpfile.js, call bundle.export([package-name]) directly on the bundle performing the export.');
75 } else {
76 for (var i = 0; i < globalExportMappings.length; i++) {
77 bundle.export(globalExportMappings[i]);
78 }
79 }
80 }
81 bundle.globalModuleMappingsApplied = true;
82 }
83
84 var bundleTo = bundle.bundleInDir;
85
86 if (!applyImports) {
87 bundleTo += '/no_imports';
88 }
89
90 // Only process LESS when generating the bundle containing imports. If using the "no_imports" bundle, you
91 // need to take care of adding the CSS yourself.
92 if (applyImports && bundle.lessSrcPath) {
93 var lessBundleTo = bundleTo;
94
95 if (bundle.lessTargetDir) {
96 lessBundleTo = bundle.lessTargetDir;
97 }
98
99 less(bundle.lessSrcPath, lessBundleTo);
100 }
101
102 var fileToBundle = bundle.bundleModule;
103 if (bundle.bundleDependencyModule) {
104 // Lets generate a temp file containing the module require.
105 if (!fs.existsSync('target')) {
106 fs.mkdirSync('target');
107 }
108 fileToBundle = 'target/' + bundle.bundleOutputFile;
109 fs.writeFileSync(fileToBundle, "module.exports = require('" + bundle.module + "');");
110 }
111
112 var browserifyConfig = {
113 entries: [fileToBundle],
114 extensions: ['.js', '.es6', '.jsx', '.hbs'],
115 cache: {},
116 packageCache: {},
117 fullPaths: true
118 };
119
120 if (bundle.minifyBundle === true) {
121 browserifyConfig.debug = true;
122 }
123 var bundler = browserify(browserifyConfig);
124
125 var hasJSX = paths.hasSourceFiles('jsx');
126 var hasES6 = paths.hasSourceFiles('es6');
127 var hasBabelRc = fs.existsSync('.babelrc');
128
129 if (langConfig.ecmaVersion === 6 || hasJSX || hasES6 || hasBabelRc) {
130 var babelify = require('babelify');
131 var presets = [];
132 var plugins = [];
133
134 if (hasBabelRc) {
135 logger.logInfo("Will use babel config from .babelrc");
136 }
137 else if (hasJSX) {
138 presets.push('react');
139 dependencies.warnOnMissingDependency('babel-preset-react', 'You have JSX sources in this project. Transpiling these will require the "babel-preset-react" package.');
140 presets.push('es2015');
141 dependencies.warnOnMissingDependency('babel-preset-es2015', 'You have JSX/ES6 sources in this project. Transpiling these will require the "babel-preset-es2015" package.');
142 } else {
143 presets.push('es2015');
144 dependencies.warnOnMissingDependency('babel-preset-es2015', 'You have ES6 sources in this project. Transpiling these will require the "babel-preset-es2015" package.');
145 }
146
147 var babelConfig = {};
148
149 // if no .babelrc was found, configure babel with the default presets and plugins from above
150 if (!hasBabelRc) {
151 babelConfig.presets = presets;
152 babelConfig.plugins = plugins;
153 }
154
155 // if .babelrc was found, an empty config object must be passed in order for .babelrc config to be read automatically
156 bundler.transform(babelify, babelConfig);
157 }
158
159 if (bundle.bundleTransforms) {
160 for (var i = 0; i < bundle.bundleTransforms.length; i++) {
161 bundler.transform(bundle.bundleTransforms[i]);
162 }
163 }
164
165 if (applyImports) {
166 addModuleMappingTransforms(bundle, bundler);
167 }
168
169 if (bundle.minifyBundle === true) {
170 var sourceMap = bundle.as + '.map.json';
171 bundler.plugin('minifyify', {
172 map: sourceMap,
173 output: bundleTo + '/' + sourceMap
174 });
175 }
176
177 for (var i = 0; i < preBundleListeners.length; i++) {
178 preBundleListeners[i].call(bundle, bundler);
179 }
180
181 // Allow reading of stuff from the filesystem.
182 bundler.transform(require('brfs'));
183
184 var bundleOutput = bundler.bundle()
185 .on('error', function (err) {
186 logger.logError('Browserify bundle processing error');
187 if (err) {
188 logger.logError('\terror: ' + err.stack);
189 }
190 if (main.isRebundle() || main.isRetest()) {
191 notifier.notify('bundle:watch failure', 'See console for details.');
192 // ignore failures if we are running rebundle/retesting.
193 this.emit('end');
194 } else {
195 throw new Error('Browserify bundle processing error. See above for details.');
196 }
197 });
198
199 if (applyImports) {
200 var bufferedTextTransform = require('./pipeline-transforms/buffered-text-accumulator-transform');
201 var requireStubTransform = require('./pipeline-transforms/require-stub-transform');
202 var pack = require('browser-pack');
203
204 bundleOutput = bundleOutput.pipe(bufferedTextTransform())// gathers together all the bundle JS, preparing for the next pipeline stage
205 .pipe(requireStubTransform.pipelinePlugin(bundle.moduleMappings)) // transform the require stubs
206 .pipe(pack()); // repack the bundle after the previous transform
207 }
208
209 var through = require('through2');
210 var bundleOutFile = bundleTo + '/' + bundle.bundleOutputFile;
211 return bundleOutput.pipe(source(bundle.bundleOutputFile))
212 .pipe(gulp.dest(bundleTo))
213 .pipe(through.obj(function (bundle, encoding, callback) {
214 for (var i = 0; i < postBundleListeners.length; i++) {
215 postBundleListeners[i].call(bundle, bundleOutFile);
216 }
217 callback();
218 }));
219};
220
221exports.doCSSBundle = function(bundle, resource) {
222 var ncp = require('ncp').ncp;
223 var folder = paths.parentDir(resource);
224
225 if (!bundle.bundleInDir) {
226 var adjunctBase = setAdjunctInDir(bundle);
227 logger.logInfo('CSS resource "' + resource + '" will be available in Jenkins as adjunct "' + adjunctBase + '.' + bundle.as + '".')
228 }
229
230 paths.mkdirp(bundle.bundleInDir);
231 ncp(folder, bundle.bundleInDir, function (err) {
232 if (err) {
233 return logger.logError(err);
234 }
235 if (bundle.format === 'less') {
236 less(resource, bundle.bundleInDir);
237 }
238 // Add a .adjunct marker file in each of the subdirs
239 paths.walkDirs(bundle.bundleInDir, function(dir) {
240 var dotAdjunct = dir + '/.adjunct';
241 if (!fs.existsSync(dotAdjunct)) {
242 fs.writeFileSync(dotAdjunct, '');
243 }
244 });
245 });
246};
247
248function less(src, targetDir) {
249 var less = require('gulp-less');
250 gulp.src(src)
251 .pipe(less().on('error', function (err) {
252 logger.logError('LESS processing error:');
253 if (err) {
254 logger.logError('\tmessage: ' + err.message);
255 logger.logError('\tline #: ' + err.line);
256 if (err.extract) {
257 logger.logError('\textract: ' + JSON.stringify(err.extract));
258 }
259 }
260 if (main.isRebundle() || main.isRetest()) {
261 notifier.notify('LESS processing error', 'See console for details.');
262 // ignore failures if we are running rebundle/retesting.
263 this.emit('end');
264 } else {
265 throw new Error('LESS processing error. See above for details.');
266 }
267 }))
268 .pipe(gulp.dest(targetDir));
269 logger.logInfo("LESS CSS pre-processing completed to '" + targetDir + "'.");
270}
271
272function addModuleMappingTransforms(bundle, bundler) {
273 var moduleMappings = bundle.moduleMappings;
274 var requiredModuleMappings = [];
275
276 if (moduleMappings.length > 0) {
277 var requireSearch = transformTools.makeStringTransform("requireSearch", {},
278 function(content, opts, cb) {
279 for (var i = 0; i < moduleMappings.length; i++) {
280 var mapping = moduleMappings[i];
281 // Do a rough search for the module name. If we find it, then we
282 // add that module name to the list. This may result in some false
283 // positives, but that's okay. The most important thing is that we
284 // do add the import for the module if it is required. Adding additional
285 // imports for modules not required is not optimal, but is also not the
286 // end of the world.
287 if (content.indexOf(mapping.fromSpec.moduleName) !== -1) {
288 var toSpec = new ModuleSpec(mapping.to);
289 var importAs = toSpec.importAs();
290 if (requiredModuleMappings.indexOf(importAs) === -1) {
291 requiredModuleMappings.push(importAs);
292 }
293 }
294 }
295 return cb(null, content);
296 });
297 bundler.transform({ global: true }, requireSearch);
298 }
299 var importExportApplied = false;
300 var importExportTransform = transformTools.makeStringTransform("importExportTransform", {},
301 function (content, opts, done) {
302 if (!importExportApplied) {
303 try {
304 if(!hasJenkinsJsModulesDependency) {
305 throw new Error("This module must have a dependency on the '@jenkins-cd/js-modules' package. Please run 'npm install --save @jenkins-cd/js-modules'.");
306 }
307
308 var exportNamespace = 'undefined'; // global namespace
309 var exportModule = undefined;
310
311 if (bundle.exportEmptyModule) {
312 exportModule = '{}'; // exporting nothing (an "empty" module object)
313 }
314
315 if (bundle.bundleExportNamespace) {
316 // It's a hpi plugin, so use it's name as the export namespace.
317 exportNamespace = "'" + bundle.bundleExportNamespace + "'";
318 }
319 if (bundle.bundleExport) {
320 // export function was called, so export the module.
321 exportModule = 'module'; // export the module
322 }
323
324 var templateParams = {
325 bundle: bundle,
326 content: content,
327 css: []
328 };
329
330 if(exportModule) {
331 // Always call export, even if the export function was not called on the builder instance.
332 // If the export function was not called, we export nothing (see above). In this case, it just
333 // generates an event for any modules that need to sync on the load event for the module.
334 templateParams.entryExport = {
335 namespace: exportNamespace,
336 module: exportModule
337 };
338 }
339
340 templateParams.dependencyExports = expandDependencyExports(bundle.moduleExports);
341
342 // perform addModuleCSSToPage actions for mappings that requested it.
343 // We don't need the imports to complete before adding these. We can just add
344 // them immediately.
345 for (var i = 0; i < moduleMappings.length; i++) {
346 var mapping = moduleMappings[i];
347 var addDefaultCSS = mapping.config.addDefaultCSS;
348 if (addDefaultCSS && addDefaultCSS === true) {
349 var parsedModuleQName = new ModuleSpec(mapping.to);
350 templateParams.css.push(parsedModuleQName);
351 }
352 }
353
354 var wrappedContent = entryModuleTemplate(templateParams);
355
356 return done(null, wrappedContent);
357 } finally {
358 importExportApplied = true;
359 }
360 } else {
361 return done(null, content);
362 }
363 });
364
365 bundler.transform(importExportTransform);
366
367 var through = require('through2');
368 bundler.pipeline.get('deps').push(through.obj(function (row, enc, next) {
369 if (row.entry) {
370 row.source = "var ___$$$___requiredModuleMappings = " + JSON.stringify(requiredModuleMappings) + ";\n\n" + row.source;
371 }
372 this.push(row);
373 next();
374 }));
375}
376
377function expandDependencyExports(bundleExports) {
378 if (!bundleExports || bundleExports.length === 0) {
379 return undefined;
380 }
381
382 var dependencyExports = [];
383 for (var i in bundleExports) {
384 var packageName = bundleExports[i];
385 var versionMetadata = dependencies.externalizedVersionMetadata(packageName);
386 if (versionMetadata) {
387 dependencyExports.push(versionMetadata);
388 } else {
389 logger.logWarn("Ignoring export decl for package '" + packageName + "'. This package is not installed, or is not a declared dependency.");
390 }
391 }
392
393 return dependencyExports;
394}
395
396function setAdjunctInDir(bundle) {
397 var adjunctBase = 'org/jenkins/ui/jsmodules';
398 if (bundle.bundleExportNamespace) {
399 if (maven.isMavenProject && bundle.bundleExportNamespace === maven.getArtifactId()) {
400 adjunctBase += '/' + maven.getArtifactId();
401 } else {
402 adjunctBase += '/' + normalizeForFilenameUse(bundle.bundleExportNamespace);
403 }
404 } else if (maven.isMavenProject) {
405 adjunctBase += '/' + maven.getArtifactId();
406 }
407 bundle.bundleInDir = 'target/classes/' + adjunctBase;
408 return _string.replaceAll(adjunctBase, '/', '\.');
409}
410
411function normalizeForFilenameUse(string) {
412 // Replace all non alphanumerics with an underscore.
413 return string.replace(/\W/g, '-');
414}
\No newline at end of file