@ui5/builder 3.0.0-alpha.0 → 3.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +58 -1
  2. package/index.js +4 -16
  3. package/jsdoc.json +0 -1
  4. package/lib/builder/BuildContext.js +17 -0
  5. package/lib/builder/ProjectBuildContext.js +9 -7
  6. package/lib/builder/builder.js +1 -2
  7. package/lib/lbt/analyzer/JSModuleAnalyzer.js +18 -3
  8. package/lib/lbt/analyzer/XMLTemplateAnalyzer.js +2 -1
  9. package/lib/lbt/bundle/AutoSplitter.js +10 -24
  10. package/lib/lbt/bundle/Builder.js +363 -134
  11. package/lib/lbt/bundle/BundleWriter.js +17 -0
  12. package/lib/lbt/resources/LocatorResource.js +6 -8
  13. package/lib/lbt/resources/LocatorResourcePool.js +8 -4
  14. package/lib/lbt/resources/Resource.js +7 -0
  15. package/lib/lbt/resources/ResourceCollector.js +33 -15
  16. package/lib/lbt/utils/parseUtils.js +1 -1
  17. package/lib/processors/bundlers/moduleBundler.js +40 -14
  18. package/lib/processors/minifier.js +90 -0
  19. package/lib/processors/resourceListCreator.js +2 -16
  20. package/lib/tasks/TaskUtil.js +9 -9
  21. package/lib/tasks/bundlers/generateBundle.js +81 -13
  22. package/lib/tasks/bundlers/generateComponentPreload.js +20 -6
  23. package/lib/tasks/bundlers/generateFlexChangesBundle.js +3 -2
  24. package/lib/tasks/bundlers/generateLibraryPreload.js +65 -10
  25. package/lib/tasks/bundlers/generateStandaloneAppBundle.js +46 -12
  26. package/lib/tasks/bundlers/utils/createModuleNameMapping.js +30 -0
  27. package/lib/tasks/generateResourcesJson.js +14 -8
  28. package/lib/tasks/minify.js +41 -0
  29. package/lib/tasks/taskRepository.js +6 -2
  30. package/lib/types/application/ApplicationBuilder.js +22 -31
  31. package/lib/types/library/LibraryBuilder.js +23 -29
  32. package/lib/types/themeLibrary/ThemeLibraryBuilder.js +1 -0
  33. package/package.json +16 -14
  34. package/lib/processors/debugFileCreator.js +0 -52
  35. package/lib/processors/resourceCopier.js +0 -24
  36. package/lib/processors/uglifier.js +0 -45
  37. package/lib/tasks/createDebugFiles.js +0 -30
  38. package/lib/tasks/uglify.js +0 -33
@@ -3,10 +3,9 @@
3
3
  "use strict";
4
4
 
5
5
  const path = require("path");
6
- const terser = require("terser");
7
6
  const {pd} = require("pretty-data");
8
7
  const {parseJS, Syntax} = require("../utils/parseUtils");
9
- // const MOZ_SourceMap = require("source-map");
8
+ const {encode: encodeMappings, decode: decodeMappings} = require("sourcemap-codec");
10
9
 
11
10
  const {isMethodCall} = require("../utils/ASTUtils");
12
11
  const ModuleName = require("../utils/ModuleName");
@@ -19,7 +18,8 @@ const {SectionType} = require("./BundleDefinition");
19
18
  const BundleWriter = require("./BundleWriter");
20
19
  const log = require("@ui5/logger").getLogger("lbt:bundle:Builder");
21
20
 
22
- const copyrightCommentsPattern = /copyright|\(c\)(?:[0-9]+|\s+[0-9A-za-z])|released under|license|\u00a9|^@ui5-bundle-raw-include |^@ui5-bundle /i;
21
+ const sourceMappingUrlPattern = /\/\/# sourceMappingURL=(.+)\s*$/;
22
+ const httpPattern = /^https?:\/\//i;
23
23
  const xmlHtmlPrePattern = /<(?:\w+:)?pre\b/;
24
24
 
25
25
  const strReplacements = {
@@ -41,30 +41,30 @@ function isEmptyBundle(resolvedBundle) {
41
41
  }
42
42
 
43
43
  const UI5BundleFormat = {
44
- beforePreloads(outW, section) {
45
- outW.write(`jQuery.sap.registerPreloadedModules(`);
46
- outW.writeln(`{`);
44
+ beforePreloads(section) {
45
+ let str = `jQuery.sap.registerPreloadedModules({\n`;
47
46
  if ( section.name ) {
48
- outW.writeln(`"name":"${section.name}",`);
47
+ str += `"name":"${section.name}",\n`;
49
48
  }
50
- outW.writeln(`"version":"2.0",`);
51
- outW.writeln(`"modules":{`);
49
+ str += `"version":"2.0",\n`;
50
+ str += `"modules":{\n`;
51
+ return str;
52
52
  },
53
53
 
54
- afterPreloads(outW, section) {
55
- outW.writeln(`}});`);
54
+ afterPreloads(section) {
55
+ return `}});\n`;
56
56
  },
57
57
 
58
- beforeBundleInfo(outW) {
59
- outW.writeln("\"unsupported\"; /* 'bundleInfo' section mode not supported (requires ui5loader)");
58
+ beforeBundleInfo() {
59
+ return `"unsupported"; /* 'bundleInfo' section mode not supported (requires ui5loader)\n`;
60
60
  },
61
61
 
62
- afterBundleInfo(outW) {
63
- outW.writeln("*/");
62
+ afterBundleInfo() {
63
+ return "*/\n";
64
64
  },
65
65
 
66
- requireSync(outW, moduleName) {
67
- outW.writeln(`sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");`);
66
+ requireSync(moduleName) {
67
+ return `sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");\n`;
68
68
  },
69
69
 
70
70
  shouldDecorate(resolvedModule) {
@@ -73,28 +73,29 @@ const UI5BundleFormat = {
73
73
  };
74
74
 
75
75
  const EVOBundleFormat = {
76
- beforePreloads(outW, section) {
77
- outW.writeln(`sap.ui.require.preload({`);
76
+ beforePreloads(section) {
77
+ return `sap.ui.require.preload({\n`;
78
78
  },
79
79
 
80
- afterPreloads(outW, section) {
81
- outW.write(`}`);
80
+ afterPreloads(section) {
81
+ let str = `}`;
82
82
  if ( section.name ) {
83
- outW.write(`,"${section.name}"`);
83
+ str += `,"${section.name}"`;
84
84
  }
85
- outW.writeln(`);`);
85
+ str += `);\n`;
86
+ return str;
86
87
  },
87
88
 
88
- beforeBundleInfo(outW) {
89
- outW.writeln("sap.ui.loader.config({bundlesUI5:{");
89
+ beforeBundleInfo() {
90
+ return "sap.ui.loader.config({bundlesUI5:{\n";
90
91
  },
91
92
 
92
- afterBundleInfo(outW) {
93
- outW.writeln("}});");
93
+ afterBundleInfo() {
94
+ return "}});\n";
94
95
  },
95
96
 
96
- requireSync(outW, moduleName) {
97
- outW.writeln(`sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");`);
97
+ requireSync(moduleName) {
98
+ return `sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");\n`;
98
99
  },
99
100
 
100
101
  shouldDecorate(resolvedModule) {
@@ -148,6 +149,9 @@ class BundleBuilder {
148
149
 
149
150
  this.options = options || {};
150
151
  this.optimize = !!this.options.optimize;
152
+ if (this.options.sourceMap === undefined) {
153
+ this.options.sourceMap = true;
154
+ }
151
155
 
152
156
  // when decorateBootstrapModule is set to false, we don't write the optimized flag
153
157
  // and don't write the try catch wrapper
@@ -157,6 +161,15 @@ class BundleBuilder {
157
161
  // TODO is the following condition ok or should the availability of jquery.sap.global.js be configurable?
158
162
  this.jqglobalAvailable = !resolvedModule.containsGlobal;
159
163
  this.openModule(resolvedModule.name);
164
+
165
+ this._sourceMap = {
166
+ version: 3,
167
+ file: path.posix.basename(resolvedModule.name),
168
+ sections: [],
169
+ };
170
+ this._bundleName = resolvedModule.name;
171
+ this._inlineContentCounter = 0;
172
+
160
173
  let bundleInfos = [];
161
174
  // create all sections in sequence
162
175
  for ( const section of resolvedModule.sections ) {
@@ -185,6 +198,7 @@ class BundleBuilder {
185
198
  return {
186
199
  name: module.name,
187
200
  content: this.outW.toString(),
201
+ sourceMap: this.options.sourceMap ? JSON.stringify(this._sourceMap) : null,
188
202
  bundleInfo: bundleInfo
189
203
  };
190
204
  }
@@ -202,23 +216,36 @@ class BundleBuilder {
202
216
  }
203
217
  }
204
218
 
219
+ writeWithSourceMap(content) {
220
+ const transientSourceName =
221
+ `${path.posix.basename(this._bundleName)}?bundle-code-${this._inlineContentCounter++}`;
222
+ const sourceMap = createTransientSourceMap({
223
+ moduleName: transientSourceName,
224
+ moduleContent: content,
225
+ includeContent: true
226
+ });
227
+ this.addSourceMap(this._bundleName, sourceMap);
228
+ this.outW.write(content);
229
+ this.outW.ensureNewLine();
230
+ }
231
+
205
232
  closeModule(resolvedModule) {
206
233
  if ( resolvedModule.containsCore ) {
207
234
  this.outW.ensureNewLine(); // for clarity and to avoid issues with single line comments
208
- this.outW.writeln(`// as this module contains the Core, we ensure that the Core has been booted`);
209
- this.outW.writeln(`sap.ui.getCore().boot && sap.ui.getCore().boot();`);
235
+ this.writeWithSourceMap(
236
+ `// as this module contains the Core, we ensure that the Core has been booted\n` +
237
+ `sap.ui.getCore().boot && sap.ui.getCore().boot();`);
210
238
  }
211
239
  if ( this.shouldDecorate && this.options.addTryCatchRestartWrapper ) {
212
240
  this.outW.ensureNewLine(); // for clarity and to avoid issues with single line comments
213
- this.outW.writeln(`} catch(oError) {`);
214
- this.outW.writeln(`if (oError.name != "Restart") { throw oError; }`);
215
- this.outW.writeln(`}`);
241
+ this.writeWithSourceMap(
242
+ `} catch(oError) {\n` +
243
+ `if (oError.name != "Restart") { throw oError; }\n` +
244
+ `}`);
245
+ }
246
+ if (this.options.sourceMap) {
247
+ this.outW.writeln(`//# sourceMappingURL=${path.posix.basename(resolvedModule.name)}.map`);
216
248
  }
217
- /* NODE-TODO
218
- if ( writeSourceMap && writeSourceMapAnnotation ) {
219
- outW.ensureNewLine();
220
- outW.write("//# sourceMappingURL=" + moduleName.getBaseName().replaceFirst("\\.js$", ".js.map"));
221
- }*/
222
249
  }
223
250
 
224
251
  addSection(section) {
@@ -260,34 +287,43 @@ class BundleBuilder {
260
287
  // TODO check that there are only JS modules contained
261
288
  async writeRaw(section) {
262
289
  // write all modules in sequence
263
- for ( const module of section.modules ) {
264
- const resource = await this.pool.findResourceWithInfo(module);
290
+ for ( const moduleName of section.modules ) {
291
+ const resource = await this.pool.findResourceWithInfo(moduleName);
265
292
  if ( resource != null ) {
266
- this.outW.startSegment(module);
293
+ this.outW.startSegment(moduleName);
267
294
  this.outW.ensureNewLine();
268
- this.outW.writeln("//@ui5-bundle-raw-include " + module);
269
- await this.writeRawModule(module, resource);
295
+ this.outW.writeln("//@ui5-bundle-raw-include " + moduleName);
296
+ await this.writeRawModule(moduleName, resource);
270
297
  const compressedSize = this.outW.endSegment();
271
- log.verbose(" %s (%d,%d)", module, resource.info != null ? resource.info.size : -1, compressedSize);
298
+ log.verbose(" %s (%d,%d)", moduleName,
299
+ resource.info != null ? resource.info.size : -1, compressedSize);
272
300
  if ( section.declareRawModules ) {
273
- this.missingRawDeclarations.push(module);
301
+ this.missingRawDeclarations.push(moduleName);
274
302
  }
275
- if ( module === UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL ) {
303
+ if ( moduleName === UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL ) {
276
304
  this.jqglobalAvailable = true;
277
305
  }
278
306
  } else {
279
- log.error(" couldn't find %s", module);
307
+ log.error(" couldn't find %s", moduleName);
280
308
  }
281
309
  }
282
310
  }
283
311
 
284
- async writeRawModule(module, resource) {
285
- let fileContent = await resource.buffer();
286
- if ( /\.js$/.test(module) ) {
287
- fileContent = await this.compressJS( fileContent, resource );
312
+ async writeRawModule(moduleName, resource) {
313
+ this.outW.ensureNewLine();
314
+ let moduleContent = (await resource.buffer()).toString();
315
+ if (this.options.sourceMap) {
316
+ let moduleSourceMap;
317
+ ({moduleContent, moduleSourceMap} =
318
+ await this.getSourceMapForModule({
319
+ moduleName,
320
+ moduleContent,
321
+ resourcePath: resource.getPath()
322
+ }));
323
+ this.addSourceMap(moduleName, moduleSourceMap);
288
324
  }
325
+ this.outW.write(moduleContent);
289
326
  this.outW.ensureNewLine();
290
- this.outW.write( fileContent );
291
327
  }
292
328
 
293
329
  async writePreloadFunction(section) {
@@ -301,7 +337,7 @@ class BundleBuilder {
301
337
 
302
338
  await this.rewriteAMDModules(sequence);
303
339
  if ( sequence.length > 0 ) {
304
- this.targetBundleFormat.beforePreloads(outW, section);
340
+ this.writeWithSourceMap(this.targetBundleFormat.beforePreloads(section));
305
341
  let i = 0;
306
342
  for ( const module of sequence ) {
307
343
  const resource = await this.pool.findResourceWithInfo(module);
@@ -309,7 +345,7 @@ class BundleBuilder {
309
345
  if ( i>0 ) {
310
346
  outW.writeln(",");
311
347
  }
312
- this.beforeWritePreloadModule(module, resource.info, resource);
348
+ // this.beforeWritePreloadModule(module, resource.info, resource);
313
349
  outW.write(`\t"${module.toString()}":`);
314
350
  outW.startSegment(module);
315
351
  await this.writePreloadModule(module, resource.info, resource);
@@ -325,66 +361,83 @@ class BundleBuilder {
325
361
  if ( i > 0 ) {
326
362
  outW.writeln();
327
363
  }
328
- this.targetBundleFormat.afterPreloads(outW, section);
364
+ outW.write(this.targetBundleFormat.afterPreloads(section));
329
365
  }
330
366
 
331
367
  // this.afterWriteFunctionPreloadSection();
332
368
  }
333
369
 
334
- async compressJS(fileContent, resource) {
335
- if ( this.optimize ) {
336
- const result = await terser.minify({
337
- [resource.name]: String(fileContent)
338
- }, {
339
- compress: false, // TODO configure?
340
- output: {
341
- comments: copyrightCommentsPattern,
342
- wrap_func_args: false
343
- }
344
- // , outFileName: resource.name
345
- // , outSourceMap: true
346
- });
347
- // console.log(result.map);
348
- // const map = new MOZ_SourceMap.SourceMapConsumer(result.map);
349
- // map.eachMapping(function (m) { console.log(m); }); // console.log(map);
350
- fileContent = result.code;
351
- // throw new Error();
352
- }
353
- return fileContent;
354
- }
355
-
356
370
  beforeWriteFunctionPreloadSection(sequence) {
357
371
  // simple version: just sort alphabetically
358
372
  sequence.sort();
359
373
  }
360
374
 
375
+ addSourceMap(moduleName, map) {
376
+ if (!map) {
377
+ throw new Error("No source map provided");
378
+ }
379
+
380
+ if (map.mappings.startsWith(";")) {
381
+ // If first line is not already mapped (typical for comments or parentheses), add a mapping to
382
+ // make sure that dev-tools (especially Chrome's) don't choose the end of the preceding module
383
+ // when the user tries to set a breakpoint from the bundle file
384
+ map.mappings = "AAAA" + map.mappings;
385
+ }
386
+
387
+ map.sourceRoot = path.posix.relative(
388
+ path.posix.dirname(this._bundleName), path.posix.dirname(moduleName));
389
+
390
+ this._sourceMap.sections.push({
391
+ offset: {
392
+ line: this.outW.lineOffset,
393
+ column: this.outW.columnOffset
394
+ },
395
+ map
396
+ });
397
+ }
398
+
361
399
  async rewriteAMDModules(sequence) {
362
400
  if ( this.options.usePredefineCalls ) {
363
401
  const outW = this.outW;
364
402
 
365
403
  const remaining = [];
366
- for ( const module of sequence ) {
367
- if ( /\.js$/.test(module) ) {
368
- // console.log("Processing " + module);
369
- const resource = await this.pool.findResourceWithInfo(module);
370
- let code = await resource.buffer();
371
- code = rewriteDefine(this.targetBundleFormat, code, module);
372
- if ( code ) {
373
- outW.startSegment(module);
404
+ for ( const moduleName of sequence ) {
405
+ if ( /\.js$/.test(moduleName) ) {
406
+ // console.log("Processing " + moduleName);
407
+ const resource = await this.pool.findResourceWithInfo(moduleName);
408
+ let moduleContent = (await resource.buffer()).toString();
409
+ let moduleSourceMap;
410
+ if (this.options.sourceMap) {
411
+ ({moduleContent, moduleSourceMap} =
412
+ await this.getSourceMapForModule({
413
+ moduleName,
414
+ moduleContent,
415
+ resourcePath: resource.getPath()
416
+ }));
417
+ }
418
+
419
+ const rewriteRes = await rewriteDefine({
420
+ moduleName, moduleContent, moduleSourceMap
421
+ });
422
+ if (rewriteRes) {
423
+ const {moduleContent, moduleSourceMap} = rewriteRes;
424
+ outW.startSegment(moduleName);
374
425
  outW.ensureNewLine();
375
- const fileContent = await this.compressJS(code, resource);
376
- outW.write( fileContent );
426
+ if (moduleSourceMap) {
427
+ this.addSourceMap(moduleName, moduleSourceMap);
428
+ }
429
+ outW.write(moduleContent);
377
430
  outW.ensureNewLine();
378
431
  const compressedSize = outW.endSegment();
379
- log.verbose(" %s (%d,%d)", module,
432
+ log.verbose(" %s (%d,%d)", moduleName,
380
433
  resource.info != null ? resource.info.size : -1, compressedSize);
381
434
  } else {
382
435
  // keep unprocessed modules
383
- remaining.push(module);
436
+ remaining.push(moduleName);
384
437
  }
385
438
  } else {
386
439
  // keep unprocessed modules
387
- remaining.push(module);
440
+ remaining.push(moduleName);
388
441
  }
389
442
  }
390
443
 
@@ -400,59 +453,71 @@ class BundleBuilder {
400
453
 
401
454
  /**
402
455
  *
403
- * @param {string} module module name
456
+ * @param {string} moduleName module name
404
457
  * @param {ModuleInfo} info
405
458
  * @param {module:@ui5/fs.Resource} resource
406
459
  * @returns {Promise<boolean>}
407
460
  */
408
- async writePreloadModule(module, info, resource) {
461
+ async writePreloadModule(moduleName, info, resource) {
409
462
  const outW = this.outW;
410
463
 
411
- if ( /\.js$/.test(module) && (info == null || !info.requiresTopLevelScope) ) {
412
- const compressedContent = await this.compressJS( await resource.buffer(), resource );
413
- outW.write(`function(){`);
414
- outW.write( compressedContent );
464
+ if ( /\.js$/.test(moduleName) && (info == null || !info.requiresTopLevelScope) ) {
465
+ outW.writeln(`function(){`);
466
+ // The module should be written to a new line in order for dev-tools to map breakpoints to it
467
+ outW.ensureNewLine();
468
+ let moduleContent = (await resource.buffer()).toString();
469
+ if (this.options.sourceMap) {
470
+ let moduleSourceMap;
471
+ ({moduleContent, moduleSourceMap} =
472
+ await this.getSourceMapForModule({
473
+ moduleName,
474
+ moduleContent,
475
+ resourcePath: resource.getPath()
476
+ }));
477
+
478
+ this.addSourceMap(moduleName, moduleSourceMap);
479
+ }
480
+ outW.write(moduleContent);
415
481
  this.exportGlobalNames(info);
416
482
  outW.ensureNewLine();
417
483
  outW.write(`}`);
418
- } else if ( /\.js$/.test(module) /* implicitly: && info != null && info.requiresTopLevelScope */ ) {
484
+ } else if ( /\.js$/.test(moduleName) /* implicitly: && info != null && info.requiresTopLevelScope */ ) {
419
485
  log.warn("**** warning: module %s requires top level scope" +
420
- " and can only be embedded as a string (requires 'eval')", module);
421
- const compressedContent = await this.compressJS( await resource.buffer(), resource );
422
- outW.write( makeStringLiteral( compressedContent ) );
423
- } else if ( /\.html$/.test(module) ) {
424
- const fileContent = await resource.buffer();
486
+ " and can only be embedded as a string (requires 'eval')", moduleName);
487
+ outW.write( makeStringLiteral( (await resource.buffer()).toString() ) );
488
+ } else if ( /\.html$/.test(moduleName) ) {
489
+ const fileContent = (await resource.buffer()).toString();
425
490
  outW.write( makeStringLiteral( fileContent ) );
426
- } else if ( /\.json$/.test(module) ) {
427
- let fileContent = await resource.buffer();
491
+ } else if ( /\.json$/.test(moduleName) ) {
492
+ let fileContent = (await resource.buffer()).toString();
428
493
  if ( this.optimize ) {
429
494
  try {
430
495
  fileContent = JSON.stringify( JSON.parse( fileContent) );
431
496
  } catch (e) {
432
- log.verbose("Failed to parse JSON file %s. Ignoring error, skipping compression.", module);
497
+ log.verbose("Failed to parse JSON file %s. Ignoring error, skipping compression.", moduleName);
433
498
  log.verbose(e);
434
499
  }
435
500
  }
436
501
  outW.write(makeStringLiteral(fileContent));
437
- } else if ( /\.xml$/.test(module) ) {
438
- let fileContent = await resource.buffer();
502
+ } else if ( /\.xml$/.test(moduleName) ) {
503
+ let fileContent = (await resource.buffer()).toString();
439
504
  if ( this.optimize ) {
440
505
  // For XML we use the pretty data
441
506
  // Do not minify if XML(View) contains an <*:pre> tag,
442
507
  // because whitespace of HTML <pre> should be preserved (should only happen rarely)
443
- if (!xmlHtmlPrePattern.test(fileContent.toString())) {
444
- fileContent = pd.xmlmin(fileContent.toString(), false);
508
+ if (!xmlHtmlPrePattern.test(fileContent)) {
509
+ fileContent = pd.xmlmin(fileContent, false);
445
510
  }
446
511
  }
447
512
  outW.write( makeStringLiteral( fileContent ) );
448
- } else if ( /\.properties$/.test(module) ) {
513
+ } else if ( /\.properties$/.test(moduleName) ) {
449
514
  // Since the Builder is also used when building non-project resources (e.g. dependencies)
450
515
  // *.properties files should be escaped if encoding option is specified
451
516
  const fileContent = await escapePropertiesFile(resource);
452
517
 
453
518
  outW.write( makeStringLiteral( fileContent ) );
454
519
  } else {
455
- log.error("don't know how to embed module " + module); // TODO throw?
520
+ log.error("don't know how to embed module " + moduleName); // TODO throw?
456
521
  }
457
522
 
458
523
  return true;
@@ -470,18 +535,19 @@ class BundleBuilder {
470
535
  this.outW.ensureNewLine();
471
536
  info.exposedGlobals.forEach( (globalName) => {
472
537
  // Note: globalName can be assumed to be a valid identifier as it is used as variable name anyhow
473
- this.outW.writeln(`this.${globalName}=${globalName};`);
538
+ this.writeWithSourceMap(`this.${globalName}=${globalName};\n`);
474
539
  });
475
540
  }
476
541
 
477
542
  writeBundleInfos(sections) {
478
543
  this.outW.ensureNewLine();
479
544
 
545
+ let bundleInfoStr = "";
480
546
  if ( sections.length > 0 ) {
481
- this.targetBundleFormat.beforeBundleInfo(this.outW);
547
+ bundleInfoStr = this.targetBundleFormat.beforeBundleInfo();
482
548
  sections.forEach((section, idx) => {
483
549
  if ( idx > 0 ) {
484
- this.outW.writeln(",");
550
+ bundleInfoStr += ",\n";
485
551
  }
486
552
 
487
553
  if (!section.name) {
@@ -492,31 +558,116 @@ class BundleBuilder {
492
558
  `The info might not work as expected. ` +
493
559
  `The name must match the bundle filename (incl. extension such as '.js')`);
494
560
  }
495
- this.outW.write(`"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`);
561
+ bundleInfoStr += `"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`;
496
562
  });
497
- this.outW.writeln();
498
- this.targetBundleFormat.afterBundleInfo(this.outW);
563
+ bundleInfoStr += "\n";
564
+ bundleInfoStr += this.targetBundleFormat.afterBundleInfo();
565
+
566
+ this.writeWithSourceMap(bundleInfoStr);
499
567
  }
500
568
  }
501
569
 
502
570
  writeRequires(section) {
503
571
  this.outW.ensureNewLine();
504
572
  section.modules.forEach( (module) => {
505
- this.targetBundleFormat.requireSync(this.outW, module);
573
+ this.writeWithSourceMap(this.targetBundleFormat.requireSync(module));
506
574
  });
507
575
  }
576
+
577
+ async getSourceMapForModule({moduleName, moduleContent, resourcePath}) {
578
+ let moduleSourceMap = null;
579
+ let newModuleContent = moduleContent;
580
+
581
+ const sourceMapUrlMatch = moduleContent.match(sourceMappingUrlPattern);
582
+ if (sourceMapUrlMatch) {
583
+ const sourceMapUrl = sourceMapUrlMatch[1];
584
+ log.verbose(`Found source map reference in content of module ${moduleName}: ${sourceMapUrl}`);
585
+
586
+ // Strip sourceMappingURL from module code to be bundled
587
+ // It has no effect and might be cause for confusion
588
+ newModuleContent = moduleContent.replace(sourceMappingUrlPattern, "");
589
+
590
+ if (sourceMapUrl) {
591
+ if (sourceMapUrl.startsWith("data:")) {
592
+ // Data-URI indicates an inline source map
593
+ const expectedTypeAndEncoding = "data:application/json;charset=utf-8;base64,";
594
+ if (sourceMapUrl.startsWith(expectedTypeAndEncoding)) {
595
+ const base64Content = sourceMapUrl.slice(expectedTypeAndEncoding.length);
596
+ moduleSourceMap = Buffer.from(base64Content, "base64").toString();
597
+ } else {
598
+ log.warn(
599
+ `Source map reference in module ${moduleName} is a data URI but has an unexpected` +
600
+ `encoding: ${sourceMapUrl}. Expected it to start with ` +
601
+ `"data:application/json;charset=utf-8;base64,"`);
602
+ }
603
+ } else if (httpPattern.test(sourceMapUrl)) {
604
+ log.warn(`Source map reference in module ${moduleName} is an absolute URL. ` +
605
+ `Currently, only relative URLs are supported.`);
606
+ } else if (path.posix.isAbsolute(sourceMapUrl)) {
607
+ log.warn(`Source map reference in module ${moduleName} is an absolute path. ` +
608
+ `Currently, only relative paths are supported.`);
609
+ } else {
610
+ const sourceMapPath = path.posix.join(path.posix.dirname(moduleName), sourceMapUrl);
611
+
612
+ try {
613
+ const sourceMapResource = await this.pool.findResource(sourceMapPath);
614
+ moduleSourceMap = (await sourceMapResource.buffer()).toString();
615
+ } catch (e) {
616
+ // No input source map
617
+ log.warn(`Unable to read source map for module ${moduleName}: ${e.message}`);
618
+ }
619
+ }
620
+ }
621
+ } else {
622
+ const sourceMapFileCandidate = resourcePath.slice("/resources/".length) + ".map";
623
+ log.verbose(`Could not find a sourceMappingURL reference in content of module ${moduleName}. ` +
624
+ `Attempting to find a source map resource based on the module's path: ${sourceMapFileCandidate}`);
625
+ try {
626
+ const sourceMapResource = await this.pool.findResource(sourceMapFileCandidate);
627
+ if (sourceMapResource) {
628
+ moduleSourceMap = (await sourceMapResource.buffer()).toString();
629
+ }
630
+ } catch (e) {
631
+ // No input source map
632
+ log.verbose(`Could not find a source map for module ${moduleName}: ${e.message}`);
633
+ }
634
+ }
635
+
636
+
637
+ if (moduleSourceMap) {
638
+ moduleSourceMap = JSON.parse(moduleSourceMap);
639
+ } else {
640
+ log.verbose(`No source map available for module ${moduleName}. Creating transient source map...`);
641
+ moduleSourceMap = createTransientSourceMap({
642
+ moduleName: path.posix.basename(resourcePath),
643
+ moduleContent
644
+ });
645
+ }
646
+
647
+ return {
648
+ moduleSourceMap,
649
+ moduleContent: newModuleContent
650
+ };
651
+ }
508
652
  }
509
653
 
510
654
  const CALL_SAP_UI_DEFINE = ["sap", "ui", "define"];
511
655
 
512
- function rewriteDefine(targetBundleFormat, code, moduleName) {
656
+ /*
657
+ * @param {object} parameters
658
+ * @param {string} parameters.moduleName
659
+ * @param {string} parameters.moduleContent
660
+ * @param {object} [parameters.moduleSourceMap]
661
+ * @returns {Promise<object|null>} Object containing <code>moduleContent</code> and
662
+ * <code>moduleSourceMap</code> (if one was provided) or <code>null</code> if no rewrite was applicable
663
+ */
664
+ async function rewriteDefine({moduleName, moduleContent, moduleSourceMap}) {
513
665
  let ast;
514
- const codeStr = code.toString();
515
666
  try {
516
- ast = parseJS(codeStr, {range: true});
667
+ ast = parseJS(moduleContent, {range: true});
517
668
  } catch (e) {
518
669
  log.error("error while parsing %s: %s", moduleName, e.message);
519
- return;
670
+ return {};
520
671
  }
521
672
 
522
673
  if ( ast.type === Syntax.Program &&
@@ -525,7 +676,6 @@ function rewriteDefine(targetBundleFormat, code, moduleName) {
525
676
  const changes = [];
526
677
  const defineCall = ast.body[0].expression;
527
678
 
528
-
529
679
  // Inject module name if missing
530
680
  if ( defineCall.arguments.length == 0 ||
531
681
  defineCall.arguments[0].type !== Syntax.Literal ) {
@@ -543,7 +693,6 @@ function rewriteDefine(targetBundleFormat, code, moduleName) {
543
693
 
544
694
  changes.push({
545
695
  index,
546
- count: 0,
547
696
  value
548
697
  });
549
698
  }
@@ -555,29 +704,109 @@ function rewriteDefine(targetBundleFormat, code, moduleName) {
555
704
  changes.push({
556
705
  // asterisk marks the index: sap.ui.*define()
557
706
  index: defineCall.callee.property.range[0],
558
- count: 0,
559
707
  value: "pre"
560
708
  });
561
709
  }
562
710
 
563
- return applyChanges(codeStr, changes);
711
+ return transform(changes, moduleContent, moduleSourceMap);
564
712
  }
565
713
 
566
- return false;
714
+ return null;
567
715
  }
568
716
 
569
- function applyChanges(string, changes) {
570
- // No sorting needed as changes are added in correct order
571
-
572
- const array = Array.from(string);
717
+ /*
718
+ * @param {object[]} changes Changes that should be applied to the code
719
+ * @param {string} moduleContent Code to transform
720
+ * @param {object} [moduleSourceMap] Optional source map that should be aligned with the content change
721
+ * @returns {Promise<object>} Object containing <code>moduleContent</code> and
722
+ * <code>moduleSourceMap</code> (if one was provided)
723
+ */
724
+ async function transform(changes, moduleContent, moduleSourceMap) {
725
+ const mappingChanges = [];
726
+
727
+ const array = Array.from(moduleContent);
728
+ // No sorting needed as changes are added in correct (reverse) order
573
729
  changes.forEach((change) => {
730
+ if (moduleSourceMap) {
731
+ // Compute line and column for given index to re-align source map with inserted characters
732
+ const precedingCode = array.slice(0, change.index);
733
+
734
+ const line = precedingCode.reduce((lineCount, char) => {
735
+ if (char === "\n") {
736
+ lineCount++;
737
+ }
738
+ return lineCount;
739
+ }, 0);
740
+ const lineStartIndex = precedingCode.lastIndexOf("\n") + 1;
741
+ const column = change.index - lineStartIndex;
742
+
743
+ // Source map re-alignment needs to be done from front to back
744
+ mappingChanges.unshift({
745
+ line,
746
+ column,
747
+ columnDiff: change.value.length
748
+ });
749
+ }
750
+
751
+ // Apply modification
574
752
  array.splice(
575
753
  change.index,
576
- change.count,
754
+ 0,
577
755
  change.value
578
756
  );
579
757
  });
580
- return array.join("");
758
+ const transformedCode = array.join("");
759
+
760
+ if (moduleSourceMap) {
761
+ const mappings = decodeMappings(moduleSourceMap.mappings);
762
+ mappingChanges.forEach((mappingChange) => {
763
+ const lineMapping = mappings[mappingChange.line];
764
+ if (!lineMapping) {
765
+ // No mapping available that could be transformed
766
+ return;
767
+ }
768
+ // Mapping structure:
769
+ // [generatedCodeColumn, sourceIndex, sourceCodeLine, sourceCodeColumn, nameIndex]
770
+ lineMapping.forEach((mapping) => {
771
+ if (mapping[0] > mappingChange.column) {
772
+ // All column mappings for the generated code after any change
773
+ // need to be moved by the amount of inserted characters
774
+ mapping[0] = mapping[0] + mappingChange.columnDiff;
775
+ }
776
+ });
777
+ });
778
+
779
+ moduleSourceMap.mappings = encodeMappings(mappings);
780
+
781
+ // No need for file information in source map since the bundled code does not exist in any file anyways
782
+ delete moduleSourceMap.file;
783
+ }
784
+
785
+ return {
786
+ moduleContent: transformedCode,
787
+ moduleSourceMap
788
+ };
789
+ }
790
+
791
+ function createTransientSourceMap({moduleName, moduleContent, includeContent = false}) {
792
+ const sourceMap = {
793
+ version: 3,
794
+ sources: [moduleName],
795
+ // TODO: check whether moduleContent.match() with \n is better w.r.t performance/memory usage
796
+ mappings: encodeMappings(moduleContent.split("\n").map((line, i) => {
797
+ return [[0, 0, i, 0]];
798
+ }))
799
+ };
800
+ if (includeContent) {
801
+ sourceMap.sourcesContent = [moduleContent];
802
+ }
803
+ return sourceMap;
581
804
  }
582
805
 
583
806
  module.exports = BundleBuilder;
807
+
808
+ // Export local functions for testing only
809
+ /* istanbul ignore else */
810
+ if (process.env.NODE_ENV === "test") {
811
+ module.exports.__localFunctions__ = {rewriteDefine, createTransientSourceMap};
812
+ }