@ui5/builder 3.0.0-alpha.1 → 3.0.0-alpha.4

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.
@@ -5,7 +5,7 @@
5
5
  const path = require("path");
6
6
  const {pd} = require("pretty-data");
7
7
  const {parseJS, Syntax} = require("../utils/parseUtils");
8
- // const MOZ_SourceMap = require("source-map");
8
+ const {encode: encodeMappings, decode: decodeMappings} = require("sourcemap-codec");
9
9
 
10
10
  const {isMethodCall} = require("../utils/ASTUtils");
11
11
  const ModuleName = require("../utils/ModuleName");
@@ -18,6 +18,8 @@ const {SectionType} = require("./BundleDefinition");
18
18
  const BundleWriter = require("./BundleWriter");
19
19
  const log = require("@ui5/logger").getLogger("lbt:bundle:Builder");
20
20
 
21
+ const sourceMappingUrlPattern = /\/\/# sourceMappingURL=(.+)\s*$/;
22
+ const httpPattern = /^https?:\/\//i;
21
23
  const xmlHtmlPrePattern = /<(?:\w+:)?pre\b/;
22
24
 
23
25
  const strReplacements = {
@@ -39,30 +41,30 @@ function isEmptyBundle(resolvedBundle) {
39
41
  }
40
42
 
41
43
  const UI5BundleFormat = {
42
- beforePreloads(outW, section) {
43
- outW.write(`jQuery.sap.registerPreloadedModules(`);
44
- outW.writeln(`{`);
44
+ beforePreloads(section) {
45
+ let str = `jQuery.sap.registerPreloadedModules({\n`;
45
46
  if ( section.name ) {
46
- outW.writeln(`"name":"${section.name}",`);
47
+ str += `"name":"${section.name}",\n`;
47
48
  }
48
- outW.writeln(`"version":"2.0",`);
49
- outW.writeln(`"modules":{`);
49
+ str += `"version":"2.0",\n`;
50
+ str += `"modules":{\n`;
51
+ return str;
50
52
  },
51
53
 
52
- afterPreloads(outW, section) {
53
- outW.writeln(`}});`);
54
+ afterPreloads(section) {
55
+ return `}});\n`;
54
56
  },
55
57
 
56
- beforeBundleInfo(outW) {
57
- outW.writeln("\"unsupported\"; /* 'bundleInfo' section mode not supported (requires ui5loader)");
58
+ beforeBundleInfo() {
59
+ return `"unsupported"; /* 'bundleInfo' section mode not supported (requires ui5loader)\n`;
58
60
  },
59
61
 
60
- afterBundleInfo(outW) {
61
- outW.writeln("*/");
62
+ afterBundleInfo() {
63
+ return "*/\n";
62
64
  },
63
65
 
64
- requireSync(outW, moduleName) {
65
- outW.writeln(`sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");`);
66
+ requireSync(moduleName) {
67
+ return `sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");\n`;
66
68
  },
67
69
 
68
70
  shouldDecorate(resolvedModule) {
@@ -71,28 +73,29 @@ const UI5BundleFormat = {
71
73
  };
72
74
 
73
75
  const EVOBundleFormat = {
74
- beforePreloads(outW, section) {
75
- outW.writeln(`sap.ui.require.preload({`);
76
+ beforePreloads(section) {
77
+ return `sap.ui.require.preload({\n`;
76
78
  },
77
79
 
78
- afterPreloads(outW, section) {
79
- outW.write(`}`);
80
+ afterPreloads(section) {
81
+ let str = `}`;
80
82
  if ( section.name ) {
81
- outW.write(`,"${section.name}"`);
83
+ str += `,"${section.name}"`;
82
84
  }
83
- outW.writeln(`);`);
85
+ str += `);\n`;
86
+ return str;
84
87
  },
85
88
 
86
- beforeBundleInfo(outW) {
87
- outW.writeln("sap.ui.loader.config({bundlesUI5:{");
89
+ beforeBundleInfo() {
90
+ return "sap.ui.loader.config({bundlesUI5:{\n";
88
91
  },
89
92
 
90
- afterBundleInfo(outW) {
91
- outW.writeln("}});");
93
+ afterBundleInfo() {
94
+ return "}});\n";
92
95
  },
93
96
 
94
- requireSync(outW, moduleName) {
95
- outW.writeln(`sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");`);
97
+ requireSync(moduleName) {
98
+ return `sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");\n`;
96
99
  },
97
100
 
98
101
  shouldDecorate(resolvedModule) {
@@ -146,6 +149,9 @@ class BundleBuilder {
146
149
 
147
150
  this.options = options || {};
148
151
  this.optimize = !!this.options.optimize;
152
+ if (this.options.sourceMap === undefined) {
153
+ this.options.sourceMap = true;
154
+ }
149
155
 
150
156
  // when decorateBootstrapModule is set to false, we don't write the optimized flag
151
157
  // and don't write the try catch wrapper
@@ -155,6 +161,15 @@ class BundleBuilder {
155
161
  // TODO is the following condition ok or should the availability of jquery.sap.global.js be configurable?
156
162
  this.jqglobalAvailable = !resolvedModule.containsGlobal;
157
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
+
158
173
  let bundleInfos = [];
159
174
  // create all sections in sequence
160
175
  for ( const section of resolvedModule.sections ) {
@@ -183,6 +198,7 @@ class BundleBuilder {
183
198
  return {
184
199
  name: module.name,
185
200
  content: this.outW.toString(),
201
+ sourceMap: this.options.sourceMap ? JSON.stringify(this._sourceMap) : null,
186
202
  bundleInfo: bundleInfo
187
203
  };
188
204
  }
@@ -200,23 +216,36 @@ class BundleBuilder {
200
216
  }
201
217
  }
202
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
+
203
232
  closeModule(resolvedModule) {
204
233
  if ( resolvedModule.containsCore ) {
205
234
  this.outW.ensureNewLine(); // for clarity and to avoid issues with single line comments
206
- this.outW.writeln(`// as this module contains the Core, we ensure that the Core has been booted`);
207
- 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();`);
208
238
  }
209
239
  if ( this.shouldDecorate && this.options.addTryCatchRestartWrapper ) {
210
240
  this.outW.ensureNewLine(); // for clarity and to avoid issues with single line comments
211
- this.outW.writeln(`} catch(oError) {`);
212
- this.outW.writeln(`if (oError.name != "Restart") { throw oError; }`);
213
- 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`);
214
248
  }
215
- /* NODE-TODO
216
- if ( writeSourceMap && writeSourceMapAnnotation ) {
217
- outW.ensureNewLine();
218
- outW.write("//# sourceMappingURL=" + moduleName.getBaseName().replaceFirst("\\.js$", ".js.map"));
219
- }*/
220
249
  }
221
250
 
222
251
  addSection(section) {
@@ -258,31 +287,43 @@ class BundleBuilder {
258
287
  // TODO check that there are only JS modules contained
259
288
  async writeRaw(section) {
260
289
  // write all modules in sequence
261
- for ( const module of section.modules ) {
262
- const resource = await this.pool.findResourceWithInfo(module);
290
+ for ( const moduleName of section.modules ) {
291
+ const resource = await this.pool.findResourceWithInfo(moduleName);
263
292
  if ( resource != null ) {
264
- this.outW.startSegment(module);
293
+ this.outW.startSegment(moduleName);
265
294
  this.outW.ensureNewLine();
266
- this.outW.writeln("//@ui5-bundle-raw-include " + module);
267
- await this.writeRawModule(module, resource);
295
+ this.outW.writeln("//@ui5-bundle-raw-include " + moduleName);
296
+ await this.writeRawModule(moduleName, resource);
268
297
  const compressedSize = this.outW.endSegment();
269
- 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);
270
300
  if ( section.declareRawModules ) {
271
- this.missingRawDeclarations.push(module);
301
+ this.missingRawDeclarations.push(moduleName);
272
302
  }
273
- if ( module === UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL ) {
303
+ if ( moduleName === UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL ) {
274
304
  this.jqglobalAvailable = true;
275
305
  }
276
306
  } else {
277
- log.error(" couldn't find %s", module);
307
+ log.error(" couldn't find %s", moduleName);
278
308
  }
279
309
  }
280
310
  }
281
311
 
282
- async writeRawModule(module, resource) {
283
- const fileContent = await resource.string();
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);
324
+ }
325
+ this.outW.write(moduleContent);
284
326
  this.outW.ensureNewLine();
285
- this.outW.write(fileContent);
286
327
  }
287
328
 
288
329
  async writePreloadFunction(section) {
@@ -296,7 +337,7 @@ class BundleBuilder {
296
337
 
297
338
  await this.rewriteAMDModules(sequence);
298
339
  if ( sequence.length > 0 ) {
299
- this.targetBundleFormat.beforePreloads(outW, section);
340
+ this.writeWithSourceMap(this.targetBundleFormat.beforePreloads(section));
300
341
  let i = 0;
301
342
  for ( const module of sequence ) {
302
343
  const resource = await this.pool.findResourceWithInfo(module);
@@ -304,7 +345,7 @@ class BundleBuilder {
304
345
  if ( i>0 ) {
305
346
  outW.writeln(",");
306
347
  }
307
- this.beforeWritePreloadModule(module, resource.info, resource);
348
+ // this.beforeWritePreloadModule(module, resource.info, resource);
308
349
  outW.write(`\t"${module.toString()}":`);
309
350
  outW.startSegment(module);
310
351
  await this.writePreloadModule(module, resource.info, resource);
@@ -320,7 +361,7 @@ class BundleBuilder {
320
361
  if ( i > 0 ) {
321
362
  outW.writeln();
322
363
  }
323
- this.targetBundleFormat.afterPreloads(outW, section);
364
+ outW.write(this.targetBundleFormat.afterPreloads(section));
324
365
  }
325
366
 
326
367
  // this.afterWriteFunctionPreloadSection();
@@ -331,32 +372,72 @@ class BundleBuilder {
331
372
  sequence.sort();
332
373
  }
333
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
+
334
399
  async rewriteAMDModules(sequence) {
335
400
  if ( this.options.usePredefineCalls ) {
336
401
  const outW = this.outW;
337
402
 
338
403
  const remaining = [];
339
- for ( const module of sequence ) {
340
- if ( /\.js$/.test(module) ) {
341
- // console.log("Processing " + module);
342
- const resource = await this.pool.findResourceWithInfo(module);
343
- let moduleContent = await resource.string();
344
- moduleContent = rewriteDefine(this.targetBundleFormat, moduleContent, module);
345
- if ( moduleContent ) {
346
- 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);
347
425
  outW.ensureNewLine();
426
+ if (moduleSourceMap) {
427
+ this.addSourceMap(moduleName, moduleSourceMap);
428
+ }
348
429
  outW.write(moduleContent);
349
430
  outW.ensureNewLine();
350
431
  const compressedSize = outW.endSegment();
351
- log.verbose(" %s (%d,%d)", module,
432
+ log.verbose(" %s (%d,%d)", moduleName,
352
433
  resource.info != null ? resource.info.size : -1, compressedSize);
353
434
  } else {
354
435
  // keep unprocessed modules
355
- remaining.push(module);
436
+ remaining.push(moduleName);
356
437
  }
357
438
  } else {
358
439
  // keep unprocessed modules
359
- remaining.push(module);
440
+ remaining.push(moduleName);
360
441
  }
361
442
  }
362
443
 
@@ -372,42 +453,54 @@ class BundleBuilder {
372
453
 
373
454
  /**
374
455
  *
375
- * @param {string} module module name
456
+ * @param {string} moduleName module name
376
457
  * @param {ModuleInfo} info
377
458
  * @param {module:@ui5/fs.Resource} resource
378
459
  * @returns {Promise<boolean>}
379
460
  */
380
- async writePreloadModule(module, info, resource) {
461
+ async writePreloadModule(moduleName, info, resource) {
381
462
  const outW = this.outW;
382
463
 
383
- if ( /\.js$/.test(module) && (info == null || !info.requiresTopLevelScope) ) {
384
- const moduleContent = await resource.string();
385
- outW.write(`function(){`);
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
+ }
386
480
  outW.write(moduleContent);
387
481
  this.exportGlobalNames(info);
388
482
  outW.ensureNewLine();
389
483
  outW.write(`}`);
390
- } else if ( /\.js$/.test(module) /* implicitly: && info != null && info.requiresTopLevelScope */ ) {
484
+ } else if ( /\.js$/.test(moduleName) /* implicitly: && info != null && info.requiresTopLevelScope */ ) {
391
485
  log.warn("**** warning: module %s requires top level scope" +
392
- " and can only be embedded as a string (requires 'eval')", module);
393
- const moduleContent = await resource.buffer();
394
- outW.write(makeStringLiteral(moduleContent));
395
- } else if ( /\.html$/.test(module) ) {
396
- 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();
397
490
  outW.write( makeStringLiteral( fileContent ) );
398
- } else if ( /\.json$/.test(module) ) {
399
- let fileContent = await resource.buffer();
491
+ } else if ( /\.json$/.test(moduleName) ) {
492
+ let fileContent = (await resource.buffer()).toString();
400
493
  if ( this.optimize ) {
401
494
  try {
402
495
  fileContent = JSON.stringify( JSON.parse( fileContent) );
403
496
  } catch (e) {
404
- 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);
405
498
  log.verbose(e);
406
499
  }
407
500
  }
408
501
  outW.write(makeStringLiteral(fileContent));
409
- } else if ( /\.xml$/.test(module) ) {
410
- let fileContent = await resource.string();
502
+ } else if ( /\.xml$/.test(moduleName) ) {
503
+ let fileContent = (await resource.buffer()).toString();
411
504
  if ( this.optimize ) {
412
505
  // For XML we use the pretty data
413
506
  // Do not minify if XML(View) contains an <*:pre> tag,
@@ -417,14 +510,14 @@ class BundleBuilder {
417
510
  }
418
511
  }
419
512
  outW.write( makeStringLiteral( fileContent ) );
420
- } else if ( /\.properties$/.test(module) ) {
513
+ } else if ( /\.properties$/.test(moduleName) ) {
421
514
  // Since the Builder is also used when building non-project resources (e.g. dependencies)
422
515
  // *.properties files should be escaped if encoding option is specified
423
516
  const fileContent = await escapePropertiesFile(resource);
424
517
 
425
518
  outW.write( makeStringLiteral( fileContent ) );
426
519
  } else {
427
- 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?
428
521
  }
429
522
 
430
523
  return true;
@@ -442,18 +535,19 @@ class BundleBuilder {
442
535
  this.outW.ensureNewLine();
443
536
  info.exposedGlobals.forEach( (globalName) => {
444
537
  // Note: globalName can be assumed to be a valid identifier as it is used as variable name anyhow
445
- this.outW.writeln(`this.${globalName}=${globalName};`);
538
+ this.writeWithSourceMap(`this.${globalName}=${globalName};\n`);
446
539
  });
447
540
  }
448
541
 
449
542
  writeBundleInfos(sections) {
450
543
  this.outW.ensureNewLine();
451
544
 
545
+ let bundleInfoStr = "";
452
546
  if ( sections.length > 0 ) {
453
- this.targetBundleFormat.beforeBundleInfo(this.outW);
547
+ bundleInfoStr = this.targetBundleFormat.beforeBundleInfo();
454
548
  sections.forEach((section, idx) => {
455
549
  if ( idx > 0 ) {
456
- this.outW.writeln(",");
550
+ bundleInfoStr += ",\n";
457
551
  }
458
552
 
459
553
  if (!section.name) {
@@ -464,31 +558,116 @@ class BundleBuilder {
464
558
  `The info might not work as expected. ` +
465
559
  `The name must match the bundle filename (incl. extension such as '.js')`);
466
560
  }
467
- this.outW.write(`"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`);
561
+ bundleInfoStr += `"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`;
468
562
  });
469
- this.outW.writeln();
470
- this.targetBundleFormat.afterBundleInfo(this.outW);
563
+ bundleInfoStr += "\n";
564
+ bundleInfoStr += this.targetBundleFormat.afterBundleInfo();
565
+
566
+ this.writeWithSourceMap(bundleInfoStr);
471
567
  }
472
568
  }
473
569
 
474
570
  writeRequires(section) {
475
571
  this.outW.ensureNewLine();
476
572
  section.modules.forEach( (module) => {
477
- this.targetBundleFormat.requireSync(this.outW, module);
573
+ this.writeWithSourceMap(this.targetBundleFormat.requireSync(module));
478
574
  });
479
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
+ }
480
652
  }
481
653
 
482
654
  const CALL_SAP_UI_DEFINE = ["sap", "ui", "define"];
483
655
 
484
- 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}) {
485
665
  let ast;
486
- const codeStr = code.toString();
487
666
  try {
488
- ast = parseJS(codeStr, {range: true});
667
+ ast = parseJS(moduleContent, {range: true});
489
668
  } catch (e) {
490
669
  log.error("error while parsing %s: %s", moduleName, e.message);
491
- return;
670
+ return {};
492
671
  }
493
672
 
494
673
  if ( ast.type === Syntax.Program &&
@@ -497,7 +676,6 @@ function rewriteDefine(targetBundleFormat, code, moduleName) {
497
676
  const changes = [];
498
677
  const defineCall = ast.body[0].expression;
499
678
 
500
-
501
679
  // Inject module name if missing
502
680
  if ( defineCall.arguments.length == 0 ||
503
681
  defineCall.arguments[0].type !== Syntax.Literal ) {
@@ -515,7 +693,6 @@ function rewriteDefine(targetBundleFormat, code, moduleName) {
515
693
 
516
694
  changes.push({
517
695
  index,
518
- count: 0,
519
696
  value
520
697
  });
521
698
  }
@@ -527,29 +704,109 @@ function rewriteDefine(targetBundleFormat, code, moduleName) {
527
704
  changes.push({
528
705
  // asterisk marks the index: sap.ui.*define()
529
706
  index: defineCall.callee.property.range[0],
530
- count: 0,
531
707
  value: "pre"
532
708
  });
533
709
  }
534
710
 
535
- return applyChanges(codeStr, changes);
711
+ return transform(changes, moduleContent, moduleSourceMap);
536
712
  }
537
713
 
538
- return false;
714
+ return null;
539
715
  }
540
716
 
541
- function applyChanges(string, changes) {
542
- // No sorting needed as changes are added in correct order
543
-
544
- 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
545
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
546
752
  array.splice(
547
753
  change.index,
548
- change.count,
754
+ 0,
549
755
  change.value
550
756
  );
551
757
  });
552
- 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;
553
804
  }
554
805
 
555
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
+ }