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

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 (60) hide show
  1. package/CHANGELOG.md +850 -0
  2. package/index.js +0 -43
  3. package/lib/lbt/analyzer/JSModuleAnalyzer.js +27 -8
  4. package/lib/lbt/analyzer/XMLTemplateAnalyzer.js +2 -1
  5. package/lib/lbt/bundle/Builder.js +364 -137
  6. package/lib/lbt/bundle/BundleWriter.js +17 -0
  7. package/lib/lbt/bundle/Resolver.js +2 -2
  8. package/lib/lbt/resources/LocatorResource.js +7 -7
  9. package/lib/lbt/resources/LocatorResourcePool.js +8 -4
  10. package/lib/lbt/resources/ResourceCollector.js +22 -15
  11. package/lib/lbt/resources/ResourceInfoList.js +0 -1
  12. package/lib/lbt/resources/ResourcePool.js +7 -6
  13. package/lib/lbt/utils/escapePropertiesFile.js +3 -6
  14. package/lib/lbt/utils/parseUtils.js +1 -1
  15. package/lib/processors/bundlers/moduleBundler.js +31 -10
  16. package/lib/processors/jsdoc/lib/createIndexFiles.js +1 -1
  17. package/lib/processors/jsdoc/lib/transformApiJson.js +20 -20
  18. package/lib/processors/jsdoc/lib/ui5/plugin.js +111 -6
  19. package/lib/processors/jsdoc/lib/ui5/template/publish.js +112 -91
  20. package/lib/processors/jsdoc/lib/ui5/template/utils/versionUtil.js +1 -1
  21. package/lib/processors/manifestCreator.js +8 -45
  22. package/lib/processors/minifier.js +11 -5
  23. package/lib/tasks/buildThemes.js +1 -1
  24. package/lib/tasks/bundlers/generateBundle.js +70 -30
  25. package/lib/tasks/bundlers/generateComponentPreload.js +26 -19
  26. package/lib/tasks/bundlers/generateFlexChangesBundle.js +10 -5
  27. package/lib/tasks/bundlers/generateLibraryPreload.js +113 -94
  28. package/lib/tasks/bundlers/generateManifestBundle.js +8 -10
  29. package/lib/tasks/bundlers/generateStandaloneAppBundle.js +42 -10
  30. package/lib/tasks/bundlers/utils/createModuleNameMapping.js +31 -0
  31. package/lib/tasks/generateCachebusterInfo.js +7 -3
  32. package/lib/tasks/generateLibraryManifest.js +6 -8
  33. package/lib/tasks/generateResourcesJson.js +3 -3
  34. package/lib/tasks/generateThemeDesignerResources.js +118 -2
  35. package/lib/tasks/generateVersionInfo.js +5 -5
  36. package/lib/tasks/jsdoc/generateJsdoc.js +1 -1
  37. package/lib/tasks/minify.js +14 -4
  38. package/lib/tasks/taskRepository.js +1 -13
  39. package/lib/tasks/transformBootstrapHtml.js +6 -1
  40. package/package.json +11 -10
  41. package/lib/builder/BuildContext.js +0 -56
  42. package/lib/builder/ProjectBuildContext.js +0 -57
  43. package/lib/builder/builder.js +0 -419
  44. package/lib/tasks/TaskUtil.js +0 -160
  45. package/lib/types/AbstractBuilder.js +0 -270
  46. package/lib/types/AbstractFormatter.js +0 -66
  47. package/lib/types/AbstractUi5Formatter.js +0 -95
  48. package/lib/types/application/ApplicationBuilder.js +0 -211
  49. package/lib/types/application/ApplicationFormatter.js +0 -227
  50. package/lib/types/application/applicationType.js +0 -15
  51. package/lib/types/library/LibraryBuilder.js +0 -231
  52. package/lib/types/library/LibraryFormatter.js +0 -519
  53. package/lib/types/library/libraryType.js +0 -15
  54. package/lib/types/module/ModuleBuilder.js +0 -7
  55. package/lib/types/module/ModuleFormatter.js +0 -54
  56. package/lib/types/module/moduleType.js +0 -15
  57. package/lib/types/themeLibrary/ThemeLibraryBuilder.js +0 -63
  58. package/lib/types/themeLibrary/ThemeLibraryFormatter.js +0 -90
  59. package/lib/types/themeLibrary/themeLibraryType.js +0 -15
  60. package/lib/types/typeRepository.js +0 -46
@@ -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("@jridgewell/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 = {
@@ -38,61 +40,30 @@ function isEmptyBundle(resolvedBundle) {
38
40
  return resolvedBundle.sections.every((section) => section.modules.length === 0);
39
41
  }
40
42
 
41
- const UI5BundleFormat = {
42
- beforePreloads(outW, section) {
43
- outW.write(`jQuery.sap.registerPreloadedModules(`);
44
- outW.writeln(`{`);
45
- if ( section.name ) {
46
- outW.writeln(`"name":"${section.name}",`);
47
- }
48
- outW.writeln(`"version":"2.0",`);
49
- outW.writeln(`"modules":{`);
50
- },
51
-
52
- afterPreloads(outW, section) {
53
- outW.writeln(`}});`);
54
- },
55
-
56
- beforeBundleInfo(outW) {
57
- outW.writeln("\"unsupported\"; /* 'bundleInfo' section mode not supported (requires ui5loader)");
58
- },
59
-
60
- afterBundleInfo(outW) {
61
- outW.writeln("*/");
62
- },
63
-
64
- requireSync(outW, moduleName) {
65
- outW.writeln(`sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");`);
66
- },
67
-
68
- shouldDecorate(resolvedModule) {
69
- return resolvedModule.executes(UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL);
70
- }
71
- };
72
-
73
43
  const EVOBundleFormat = {
74
- beforePreloads(outW, section) {
75
- outW.writeln(`sap.ui.require.preload({`);
44
+ beforePreloads(section) {
45
+ return `sap.ui.require.preload({\n`;
76
46
  },
77
47
 
78
- afterPreloads(outW, section) {
79
- outW.write(`}`);
48
+ afterPreloads(section) {
49
+ let str = `}`;
80
50
  if ( section.name ) {
81
- outW.write(`,"${section.name}"`);
51
+ str += `,"${section.name}"`;
82
52
  }
83
- outW.writeln(`);`);
53
+ str += `);\n`;
54
+ return str;
84
55
  },
85
56
 
86
- beforeBundleInfo(outW) {
87
- outW.writeln("sap.ui.loader.config({bundlesUI5:{");
57
+ beforeBundleInfo() {
58
+ return "sap.ui.loader.config({bundlesUI5:{\n";
88
59
  },
89
60
 
90
- afterBundleInfo(outW) {
91
- outW.writeln("}});");
61
+ afterBundleInfo() {
62
+ return "}});\n";
92
63
  },
93
64
 
94
- requireSync(outW, moduleName) {
95
- outW.writeln(`sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");`);
65
+ requireSync(moduleName) {
66
+ return `sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");\n`;
96
67
  },
97
68
 
98
69
  shouldDecorate(resolvedModule) {
@@ -112,7 +83,6 @@ class BundleBuilder {
112
83
  }
113
84
 
114
85
  async createBundle(module, options) {
115
- await this._prepare();
116
86
  if ( options.numberOfParts > 1 ) {
117
87
  const bundleInfos = [];
118
88
  const submodules = await this.splitter.run( module, options );
@@ -125,17 +95,6 @@ class BundleBuilder {
125
95
  }
126
96
  }
127
97
 
128
- _prepare() {
129
- return Promise.all([
130
- // check whether the resource pool contains debug and optimized sources
131
- this.pool.findResource( ModuleName.getDebugName(UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL) ).
132
- then( () => this.optimizedSources = true, () => this.optimizedSources = false ),
133
- // check whether EVO modules are available. If so, use EVO APIs, else use old UI5 APIs.
134
- this.pool.findResource(UI5ClientConstants.EVO_MARKER_RESOURCE).
135
- then( () => this.targetBundleFormat = EVOBundleFormat, () => this.targetBundleFormat = UI5BundleFormat )
136
- ]);
137
- }
138
-
139
98
  async _createBundle(module, options) {
140
99
  const resolvedModule = await this.resolver.resolve(module);
141
100
  if ( options.skipIfEmpty && isEmptyBundle(resolvedModule) ) {
@@ -146,15 +105,29 @@ class BundleBuilder {
146
105
 
147
106
  this.options = options || {};
148
107
  this.optimize = !!this.options.optimize;
108
+ if (this.options.sourceMap === undefined) {
109
+ this.options.sourceMap = true;
110
+ }
111
+
112
+ // Since UI5 Tooling 3.0: Always use modern API
113
+ this.targetBundleFormat = EVOBundleFormat;
149
114
 
150
- // when decorateBootstrapModule is set to false, we don't write the optimized flag
151
- // and don't write the try catch wrapper
115
+ // when decorateBootstrapModule is false,
116
+ // we don't write the optimized flag and don't write the try catch wrapper
152
117
  this.shouldDecorate = this.options.decorateBootstrapModule &&
153
- (((this.optimizedSources && !this.options.debugMode) || this.optimize) &&
154
- this.targetBundleFormat.shouldDecorate(resolvedModule));
118
+ this.targetBundleFormat.shouldDecorate(resolvedModule);
155
119
  // TODO is the following condition ok or should the availability of jquery.sap.global.js be configurable?
156
120
  this.jqglobalAvailable = !resolvedModule.containsGlobal;
157
121
  this.openModule(resolvedModule.name);
122
+
123
+ this._sourceMap = {
124
+ version: 3,
125
+ file: path.posix.basename(resolvedModule.name),
126
+ sections: [],
127
+ };
128
+ this._bundleName = resolvedModule.name;
129
+ this._inlineContentCounter = 0;
130
+
158
131
  let bundleInfos = [];
159
132
  // create all sections in sequence
160
133
  for ( const section of resolvedModule.sections ) {
@@ -183,6 +156,7 @@ class BundleBuilder {
183
156
  return {
184
157
  name: module.name,
185
158
  content: this.outW.toString(),
159
+ sourceMap: this.options.sourceMap ? JSON.stringify(this._sourceMap) : null,
186
160
  bundleInfo: bundleInfo
187
161
  };
188
162
  }
@@ -200,23 +174,36 @@ class BundleBuilder {
200
174
  }
201
175
  }
202
176
 
177
+ writeWithSourceMap(content) {
178
+ const transientSourceName =
179
+ `${path.posix.basename(this._bundleName)}?bundle-code-${this._inlineContentCounter++}`;
180
+ const sourceMap = createTransientSourceMap({
181
+ moduleName: transientSourceName,
182
+ moduleContent: content,
183
+ includeContent: true
184
+ });
185
+ this.addSourceMap(this._bundleName, sourceMap);
186
+ this.outW.write(content);
187
+ this.outW.ensureNewLine();
188
+ }
189
+
203
190
  closeModule(resolvedModule) {
204
191
  if ( resolvedModule.containsCore ) {
205
192
  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();`);
193
+ this.writeWithSourceMap(
194
+ `// as this module contains the Core, we ensure that the Core has been booted\n` +
195
+ `sap.ui.getCore().boot && sap.ui.getCore().boot();`);
208
196
  }
209
197
  if ( this.shouldDecorate && this.options.addTryCatchRestartWrapper ) {
210
198
  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(`}`);
199
+ this.writeWithSourceMap(
200
+ `} catch(oError) {\n` +
201
+ `if (oError.name != "Restart") { throw oError; }\n` +
202
+ `}`);
203
+ }
204
+ if (this.options.sourceMap) {
205
+ this.outW.writeln(`//# sourceMappingURL=${path.posix.basename(resolvedModule.name)}.map`);
214
206
  }
215
- /* NODE-TODO
216
- if ( writeSourceMap && writeSourceMapAnnotation ) {
217
- outW.ensureNewLine();
218
- outW.write("//# sourceMappingURL=" + moduleName.getBaseName().replaceFirst("\\.js$", ".js.map"));
219
- }*/
220
207
  }
221
208
 
222
209
  addSection(section) {
@@ -258,31 +245,43 @@ class BundleBuilder {
258
245
  // TODO check that there are only JS modules contained
259
246
  async writeRaw(section) {
260
247
  // write all modules in sequence
261
- for ( const module of section.modules ) {
262
- const resource = await this.pool.findResourceWithInfo(module);
248
+ for ( const moduleName of section.modules ) {
249
+ const resource = await this.pool.findResourceWithInfo(moduleName);
263
250
  if ( resource != null ) {
264
- this.outW.startSegment(module);
251
+ this.outW.startSegment(moduleName);
265
252
  this.outW.ensureNewLine();
266
- this.outW.writeln("//@ui5-bundle-raw-include " + module);
267
- await this.writeRawModule(module, resource);
253
+ this.outW.writeln("//@ui5-bundle-raw-include " + moduleName);
254
+ await this.writeRawModule(moduleName, resource);
268
255
  const compressedSize = this.outW.endSegment();
269
- log.verbose(" %s (%d,%d)", module, resource.info != null ? resource.info.size : -1, compressedSize);
256
+ log.verbose(" %s (%d,%d)", moduleName,
257
+ resource.info != null ? resource.info.size : -1, compressedSize);
270
258
  if ( section.declareRawModules ) {
271
- this.missingRawDeclarations.push(module);
259
+ this.missingRawDeclarations.push(moduleName);
272
260
  }
273
- if ( module === UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL ) {
261
+ if ( moduleName === UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL ) {
274
262
  this.jqglobalAvailable = true;
275
263
  }
276
264
  } else {
277
- log.error(" couldn't find %s", module);
265
+ log.error(" couldn't find %s", moduleName);
278
266
  }
279
267
  }
280
268
  }
281
269
 
282
- async writeRawModule(module, resource) {
283
- const fileContent = await resource.string();
270
+ async writeRawModule(moduleName, resource) {
271
+ this.outW.ensureNewLine();
272
+ let moduleContent = (await resource.buffer()).toString();
273
+ if (this.options.sourceMap) {
274
+ let moduleSourceMap;
275
+ ({moduleContent, moduleSourceMap} =
276
+ await this.getSourceMapForModule({
277
+ moduleName,
278
+ moduleContent,
279
+ resourcePath: resource.getPath()
280
+ }));
281
+ this.addSourceMap(moduleName, moduleSourceMap);
282
+ }
283
+ this.outW.write(moduleContent);
284
284
  this.outW.ensureNewLine();
285
- this.outW.write(fileContent);
286
285
  }
287
286
 
288
287
  async writePreloadFunction(section) {
@@ -296,7 +295,7 @@ class BundleBuilder {
296
295
 
297
296
  await this.rewriteAMDModules(sequence);
298
297
  if ( sequence.length > 0 ) {
299
- this.targetBundleFormat.beforePreloads(outW, section);
298
+ this.writeWithSourceMap(this.targetBundleFormat.beforePreloads(section));
300
299
  let i = 0;
301
300
  for ( const module of sequence ) {
302
301
  const resource = await this.pool.findResourceWithInfo(module);
@@ -304,7 +303,7 @@ class BundleBuilder {
304
303
  if ( i>0 ) {
305
304
  outW.writeln(",");
306
305
  }
307
- this.beforeWritePreloadModule(module, resource.info, resource);
306
+ // this.beforeWritePreloadModule(module, resource.info, resource);
308
307
  outW.write(`\t"${module.toString()}":`);
309
308
  outW.startSegment(module);
310
309
  await this.writePreloadModule(module, resource.info, resource);
@@ -320,7 +319,7 @@ class BundleBuilder {
320
319
  if ( i > 0 ) {
321
320
  outW.writeln();
322
321
  }
323
- this.targetBundleFormat.afterPreloads(outW, section);
322
+ outW.write(this.targetBundleFormat.afterPreloads(section));
324
323
  }
325
324
 
326
325
  // this.afterWriteFunctionPreloadSection();
@@ -331,32 +330,72 @@ class BundleBuilder {
331
330
  sequence.sort();
332
331
  }
333
332
 
333
+ addSourceMap(moduleName, map) {
334
+ if (!map) {
335
+ throw new Error("No source map provided");
336
+ }
337
+
338
+ if (map.mappings.startsWith(";")) {
339
+ // If first line is not already mapped (typical for comments or parentheses), add a mapping to
340
+ // make sure that dev-tools (especially Chrome's) don't choose the end of the preceding module
341
+ // when the user tries to set a breakpoint from the bundle file
342
+ map.mappings = "AAAA" + map.mappings;
343
+ }
344
+
345
+ map.sourceRoot = path.posix.relative(
346
+ path.posix.dirname(this._bundleName), path.posix.dirname(moduleName));
347
+
348
+ this._sourceMap.sections.push({
349
+ offset: {
350
+ line: this.outW.lineOffset,
351
+ column: this.outW.columnOffset
352
+ },
353
+ map
354
+ });
355
+ }
356
+
334
357
  async rewriteAMDModules(sequence) {
335
358
  if ( this.options.usePredefineCalls ) {
336
359
  const outW = this.outW;
337
360
 
338
361
  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);
362
+ for ( const moduleName of sequence ) {
363
+ if ( /\.js$/.test(moduleName) ) {
364
+ // console.log("Processing " + moduleName);
365
+ const resource = await this.pool.findResourceWithInfo(moduleName);
366
+ let moduleContent = (await resource.buffer()).toString();
367
+ let moduleSourceMap;
368
+ if (this.options.sourceMap) {
369
+ ({moduleContent, moduleSourceMap} =
370
+ await this.getSourceMapForModule({
371
+ moduleName,
372
+ moduleContent,
373
+ resourcePath: resource.getPath()
374
+ }));
375
+ }
376
+
377
+ const rewriteRes = await rewriteDefine({
378
+ moduleName, moduleContent, moduleSourceMap
379
+ });
380
+ if (rewriteRes) {
381
+ const {moduleContent, moduleSourceMap} = rewriteRes;
382
+ outW.startSegment(moduleName);
347
383
  outW.ensureNewLine();
384
+ if (moduleSourceMap) {
385
+ this.addSourceMap(moduleName, moduleSourceMap);
386
+ }
348
387
  outW.write(moduleContent);
349
388
  outW.ensureNewLine();
350
389
  const compressedSize = outW.endSegment();
351
- log.verbose(" %s (%d,%d)", module,
390
+ log.verbose(" %s (%d,%d)", moduleName,
352
391
  resource.info != null ? resource.info.size : -1, compressedSize);
353
392
  } else {
354
393
  // keep unprocessed modules
355
- remaining.push(module);
394
+ remaining.push(moduleName);
356
395
  }
357
396
  } else {
358
397
  // keep unprocessed modules
359
- remaining.push(module);
398
+ remaining.push(moduleName);
360
399
  }
361
400
  }
362
401
 
@@ -372,42 +411,54 @@ class BundleBuilder {
372
411
 
373
412
  /**
374
413
  *
375
- * @param {string} module module name
414
+ * @param {string} moduleName module name
376
415
  * @param {ModuleInfo} info
377
416
  * @param {module:@ui5/fs.Resource} resource
378
417
  * @returns {Promise<boolean>}
379
418
  */
380
- async writePreloadModule(module, info, resource) {
419
+ async writePreloadModule(moduleName, info, resource) {
381
420
  const outW = this.outW;
382
421
 
383
- if ( /\.js$/.test(module) && (info == null || !info.requiresTopLevelScope) ) {
384
- const moduleContent = await resource.string();
385
- outW.write(`function(){`);
422
+ if ( /\.js$/.test(moduleName) && (info == null || !info.requiresTopLevelScope) ) {
423
+ outW.writeln(`function(){`);
424
+ // The module should be written to a new line in order for dev-tools to map breakpoints to it
425
+ outW.ensureNewLine();
426
+ let moduleContent = (await resource.buffer()).toString();
427
+ if (this.options.sourceMap) {
428
+ let moduleSourceMap;
429
+ ({moduleContent, moduleSourceMap} =
430
+ await this.getSourceMapForModule({
431
+ moduleName,
432
+ moduleContent,
433
+ resourcePath: resource.getPath()
434
+ }));
435
+
436
+ this.addSourceMap(moduleName, moduleSourceMap);
437
+ }
386
438
  outW.write(moduleContent);
387
439
  this.exportGlobalNames(info);
388
440
  outW.ensureNewLine();
389
441
  outW.write(`}`);
390
- } else if ( /\.js$/.test(module) /* implicitly: && info != null && info.requiresTopLevelScope */ ) {
442
+ } else if ( /\.js$/.test(moduleName) /* implicitly: && info != null && info.requiresTopLevelScope */ ) {
391
443
  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();
444
+ " and can only be embedded as a string (requires 'eval')", moduleName);
445
+ outW.write( makeStringLiteral( (await resource.buffer()).toString() ) );
446
+ } else if ( /\.html$/.test(moduleName) ) {
447
+ const fileContent = (await resource.buffer()).toString();
397
448
  outW.write( makeStringLiteral( fileContent ) );
398
- } else if ( /\.json$/.test(module) ) {
399
- let fileContent = await resource.buffer();
449
+ } else if ( /\.json$/.test(moduleName) ) {
450
+ let fileContent = (await resource.buffer()).toString();
400
451
  if ( this.optimize ) {
401
452
  try {
402
453
  fileContent = JSON.stringify( JSON.parse( fileContent) );
403
454
  } catch (e) {
404
- log.verbose("Failed to parse JSON file %s. Ignoring error, skipping compression.", module);
455
+ log.verbose("Failed to parse JSON file %s. Ignoring error, skipping compression.", moduleName);
405
456
  log.verbose(e);
406
457
  }
407
458
  }
408
459
  outW.write(makeStringLiteral(fileContent));
409
- } else if ( /\.xml$/.test(module) ) {
410
- let fileContent = await resource.string();
460
+ } else if ( /\.xml$/.test(moduleName) ) {
461
+ let fileContent = (await resource.buffer()).toString();
411
462
  if ( this.optimize ) {
412
463
  // For XML we use the pretty data
413
464
  // Do not minify if XML(View) contains an <*:pre> tag,
@@ -417,14 +468,14 @@ class BundleBuilder {
417
468
  }
418
469
  }
419
470
  outW.write( makeStringLiteral( fileContent ) );
420
- } else if ( /\.properties$/.test(module) ) {
471
+ } else if ( /\.properties$/.test(moduleName) ) {
421
472
  // Since the Builder is also used when building non-project resources (e.g. dependencies)
422
473
  // *.properties files should be escaped if encoding option is specified
423
474
  const fileContent = await escapePropertiesFile(resource);
424
475
 
425
476
  outW.write( makeStringLiteral( fileContent ) );
426
477
  } else {
427
- log.error("don't know how to embed module " + module); // TODO throw?
478
+ log.error("don't know how to embed module " + moduleName); // TODO throw?
428
479
  }
429
480
 
430
481
  return true;
@@ -442,18 +493,19 @@ class BundleBuilder {
442
493
  this.outW.ensureNewLine();
443
494
  info.exposedGlobals.forEach( (globalName) => {
444
495
  // 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};`);
496
+ this.writeWithSourceMap(`this.${globalName}=${globalName};\n`);
446
497
  });
447
498
  }
448
499
 
449
500
  writeBundleInfos(sections) {
450
501
  this.outW.ensureNewLine();
451
502
 
503
+ let bundleInfoStr = "";
452
504
  if ( sections.length > 0 ) {
453
- this.targetBundleFormat.beforeBundleInfo(this.outW);
505
+ bundleInfoStr = this.targetBundleFormat.beforeBundleInfo();
454
506
  sections.forEach((section, idx) => {
455
507
  if ( idx > 0 ) {
456
- this.outW.writeln(",");
508
+ bundleInfoStr += ",\n";
457
509
  }
458
510
 
459
511
  if (!section.name) {
@@ -464,31 +516,128 @@ class BundleBuilder {
464
516
  `The info might not work as expected. ` +
465
517
  `The name must match the bundle filename (incl. extension such as '.js')`);
466
518
  }
467
- this.outW.write(`"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`);
519
+ bundleInfoStr += `"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`;
468
520
  });
469
- this.outW.writeln();
470
- this.targetBundleFormat.afterBundleInfo(this.outW);
521
+ bundleInfoStr += "\n";
522
+ bundleInfoStr += this.targetBundleFormat.afterBundleInfo();
523
+
524
+ this.writeWithSourceMap(bundleInfoStr);
471
525
  }
472
526
  }
473
527
 
474
528
  writeRequires(section) {
475
529
  this.outW.ensureNewLine();
476
530
  section.modules.forEach( (module) => {
477
- this.targetBundleFormat.requireSync(this.outW, module);
531
+ this.writeWithSourceMap(this.targetBundleFormat.requireSync(module));
478
532
  });
479
533
  }
534
+
535
+ async getSourceMapForModule({moduleName, moduleContent, resourcePath}) {
536
+ let moduleSourceMap = null;
537
+ let newModuleContent = moduleContent;
538
+
539
+ const sourceMapUrlMatch = moduleContent.match(sourceMappingUrlPattern);
540
+ if (sourceMapUrlMatch) {
541
+ const sourceMapUrl = sourceMapUrlMatch[1];
542
+ log.silly(`Found source map reference in content of module ${moduleName}: ${sourceMapUrl}`);
543
+
544
+ // Strip sourceMappingURL from module code to be bundled
545
+ // It has no effect and might be cause for confusion
546
+ newModuleContent = moduleContent.replace(sourceMappingUrlPattern, "");
547
+
548
+ if (sourceMapUrl) {
549
+ if (sourceMapUrl.startsWith("data:")) {
550
+ // Data-URI indicates an inline source map
551
+ const expectedTypeAndEncoding = "data:application/json;charset=utf-8;base64,";
552
+ if (sourceMapUrl.startsWith(expectedTypeAndEncoding)) {
553
+ const base64Content = sourceMapUrl.slice(expectedTypeAndEncoding.length);
554
+ moduleSourceMap = Buffer.from(base64Content, "base64").toString();
555
+ } else {
556
+ log.warn(
557
+ `Source map reference in module ${moduleName} is a data URI but has an unexpected` +
558
+ `encoding: ${sourceMapUrl}. Expected it to start with ` +
559
+ `"data:application/json;charset=utf-8;base64,"`);
560
+ }
561
+ } else if (httpPattern.test(sourceMapUrl)) {
562
+ log.warn(`Source map reference in module ${moduleName} is an absolute URL. ` +
563
+ `Currently, only relative URLs are supported.`);
564
+ } else if (path.posix.isAbsolute(sourceMapUrl)) {
565
+ log.warn(`Source map reference in module ${moduleName} is an absolute path. ` +
566
+ `Currently, only relative paths are supported.`);
567
+ } else {
568
+ const sourceMapPath = path.posix.join(path.posix.dirname(moduleName), sourceMapUrl);
569
+
570
+ try {
571
+ const sourceMapResource = await this.pool.findResource(sourceMapPath);
572
+ moduleSourceMap = (await sourceMapResource.buffer()).toString();
573
+ } catch (e) {
574
+ // No input source map
575
+ log.warn(`Unable to read source map for module ${moduleName}: ${e.message}`);
576
+ }
577
+ }
578
+ }
579
+ } else {
580
+ const sourceMapFileCandidate = resourcePath.slice("/resources/".length) + ".map";
581
+ log.verbose(`Could not find a sourceMappingURL reference in content of module ${moduleName}. ` +
582
+ `Attempting to find a source map resource based on the module's path: ${sourceMapFileCandidate}`);
583
+ try {
584
+ const sourceMapResource = await this.pool.findResource(sourceMapFileCandidate);
585
+ if (sourceMapResource) {
586
+ moduleSourceMap = (await sourceMapResource.buffer()).toString();
587
+ }
588
+ } catch (e) {
589
+ // No input source map
590
+ log.verbose(`Could not find a source map for module ${moduleName}: ${e.message}`);
591
+ }
592
+ }
593
+
594
+
595
+ if (moduleSourceMap) {
596
+ moduleSourceMap = JSON.parse(moduleSourceMap);
597
+
598
+ // Check for index map, which is currently not supported
599
+ if (Array.isArray(moduleSourceMap.sections)) {
600
+ log.warn(
601
+ `Module ${moduleName} references an index source map which is currently not supported. ` +
602
+ `A transient source map will be created instead...`
603
+ );
604
+ moduleSourceMap = createTransientSourceMap({
605
+ moduleName: path.posix.basename(resourcePath),
606
+ moduleContent
607
+ });
608
+ }
609
+ } else {
610
+ log.verbose(`No source map available for module ${moduleName}. Creating transient source map...`);
611
+ moduleSourceMap = createTransientSourceMap({
612
+ moduleName: path.posix.basename(resourcePath),
613
+ moduleContent
614
+ });
615
+ }
616
+
617
+ return {
618
+ moduleSourceMap,
619
+ moduleContent: newModuleContent
620
+ };
621
+ }
480
622
  }
481
623
 
482
624
  const CALL_SAP_UI_DEFINE = ["sap", "ui", "define"];
483
625
 
484
- function rewriteDefine(targetBundleFormat, code, moduleName) {
626
+ /*
627
+ * @param {object} parameters
628
+ * @param {string} parameters.moduleName
629
+ * @param {string} parameters.moduleContent
630
+ * @param {object} [parameters.moduleSourceMap]
631
+ * @returns {Promise<object|null>} Object containing <code>moduleContent</code> and
632
+ * <code>moduleSourceMap</code> (if one was provided) or <code>null</code> if no rewrite was applicable
633
+ */
634
+ async function rewriteDefine({moduleName, moduleContent, moduleSourceMap}) {
485
635
  let ast;
486
- const codeStr = code.toString();
487
636
  try {
488
- ast = parseJS(codeStr, {range: true});
637
+ ast = parseJS(moduleContent, {range: true});
489
638
  } catch (e) {
490
639
  log.error("error while parsing %s: %s", moduleName, e.message);
491
- return;
640
+ return {};
492
641
  }
493
642
 
494
643
  if ( ast.type === Syntax.Program &&
@@ -497,7 +646,6 @@ function rewriteDefine(targetBundleFormat, code, moduleName) {
497
646
  const changes = [];
498
647
  const defineCall = ast.body[0].expression;
499
648
 
500
-
501
649
  // Inject module name if missing
502
650
  if ( defineCall.arguments.length == 0 ||
503
651
  defineCall.arguments[0].type !== Syntax.Literal ) {
@@ -515,7 +663,6 @@ function rewriteDefine(targetBundleFormat, code, moduleName) {
515
663
 
516
664
  changes.push({
517
665
  index,
518
- count: 0,
519
666
  value
520
667
  });
521
668
  }
@@ -527,29 +674,109 @@ function rewriteDefine(targetBundleFormat, code, moduleName) {
527
674
  changes.push({
528
675
  // asterisk marks the index: sap.ui.*define()
529
676
  index: defineCall.callee.property.range[0],
530
- count: 0,
531
677
  value: "pre"
532
678
  });
533
679
  }
534
680
 
535
- return applyChanges(codeStr, changes);
681
+ return transform(changes, moduleContent, moduleSourceMap);
536
682
  }
537
683
 
538
- return false;
684
+ return null;
539
685
  }
540
686
 
541
- function applyChanges(string, changes) {
542
- // No sorting needed as changes are added in correct order
543
-
544
- const array = Array.from(string);
687
+ /*
688
+ * @param {object[]} changes Changes that should be applied to the code
689
+ * @param {string} moduleContent Code to transform
690
+ * @param {object} [moduleSourceMap] Optional source map that should be aligned with the content change
691
+ * @returns {Promise<object>} Object containing <code>moduleContent</code> and
692
+ * <code>moduleSourceMap</code> (if one was provided)
693
+ */
694
+ async function transform(changes, moduleContent, moduleSourceMap) {
695
+ const mappingChanges = [];
696
+
697
+ const array = Array.from(moduleContent);
698
+ // No sorting needed as changes are added in correct (reverse) order
545
699
  changes.forEach((change) => {
700
+ if (moduleSourceMap) {
701
+ // Compute line and column for given index to re-align source map with inserted characters
702
+ const precedingCode = array.slice(0, change.index);
703
+
704
+ const line = precedingCode.reduce((lineCount, char) => {
705
+ if (char === "\n") {
706
+ lineCount++;
707
+ }
708
+ return lineCount;
709
+ }, 0);
710
+ const lineStartIndex = precedingCode.lastIndexOf("\n") + 1;
711
+ const column = change.index - lineStartIndex;
712
+
713
+ // Source map re-alignment needs to be done from front to back
714
+ mappingChanges.unshift({
715
+ line,
716
+ column,
717
+ columnDiff: change.value.length
718
+ });
719
+ }
720
+
721
+ // Apply modification
546
722
  array.splice(
547
723
  change.index,
548
- change.count,
724
+ 0,
549
725
  change.value
550
726
  );
551
727
  });
552
- return array.join("");
728
+ const transformedCode = array.join("");
729
+
730
+ if (moduleSourceMap) {
731
+ const mappings = decodeMappings(moduleSourceMap.mappings);
732
+ mappingChanges.forEach((mappingChange) => {
733
+ const lineMapping = mappings[mappingChange.line];
734
+ if (!lineMapping) {
735
+ // No mapping available that could be transformed
736
+ return;
737
+ }
738
+ // Mapping structure:
739
+ // [generatedCodeColumn, sourceIndex, sourceCodeLine, sourceCodeColumn, nameIndex]
740
+ lineMapping.forEach((mapping) => {
741
+ if (mapping[0] > mappingChange.column) {
742
+ // All column mappings for the generated code after any change
743
+ // need to be moved by the amount of inserted characters
744
+ mapping[0] = mapping[0] + mappingChange.columnDiff;
745
+ }
746
+ });
747
+ });
748
+
749
+ moduleSourceMap.mappings = encodeMappings(mappings);
750
+
751
+ // No need for file information in source map since the bundled code does not exist in any file anyways
752
+ delete moduleSourceMap.file;
753
+ }
754
+
755
+ return {
756
+ moduleContent: transformedCode,
757
+ moduleSourceMap
758
+ };
759
+ }
760
+
761
+ function createTransientSourceMap({moduleName, moduleContent, includeContent = false}) {
762
+ const sourceMap = {
763
+ version: 3,
764
+ sources: [moduleName],
765
+ // TODO: check whether moduleContent.match() with \n is better w.r.t performance/memory usage
766
+ mappings: encodeMappings(moduleContent.split("\n").map((line, i) => {
767
+ return [[0, 0, i, 0]];
768
+ }))
769
+ };
770
+ if (includeContent) {
771
+ sourceMap.sourcesContent = [moduleContent];
772
+ }
773
+ return sourceMap;
553
774
  }
554
775
 
555
776
  module.exports = BundleBuilder;
777
+
778
+ // Export local functions for testing only
779
+ /* istanbul ignore else */
780
+ if (process.env.NODE_ENV === "test") {
781
+ module.exports.__localFunctions__ = {rewriteDefine, createTransientSourceMap};
782
+ }