@ui5/builder 2.11.5 → 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 (70) hide show
  1. package/CHANGELOG.md +125 -12
  2. package/index.js +4 -59
  3. package/jsdoc.json +0 -1
  4. package/lib/lbt/analyzer/JSModuleAnalyzer.js +31 -5
  5. package/lib/lbt/analyzer/XMLTemplateAnalyzer.js +2 -1
  6. package/lib/lbt/bundle/AutoSplitter.js +10 -24
  7. package/lib/lbt/bundle/Builder.js +367 -168
  8. package/lib/lbt/bundle/BundleWriter.js +17 -0
  9. package/lib/lbt/bundle/Resolver.js +3 -3
  10. package/lib/lbt/resources/LocatorResource.js +7 -9
  11. package/lib/lbt/resources/LocatorResourcePool.js +8 -4
  12. package/lib/lbt/resources/Resource.js +7 -0
  13. package/lib/lbt/resources/ResourceCollector.js +43 -18
  14. package/lib/lbt/resources/ResourceInfoList.js +0 -1
  15. package/lib/lbt/resources/ResourcePool.js +7 -6
  16. package/lib/lbt/utils/escapePropertiesFile.js +3 -6
  17. package/lib/lbt/utils/parseUtils.js +1 -1
  18. package/lib/processors/bundlers/moduleBundler.js +42 -17
  19. package/lib/processors/jsdoc/lib/createIndexFiles.js +1 -1
  20. package/lib/processors/jsdoc/lib/transformApiJson.js +7 -16
  21. package/lib/processors/jsdoc/lib/ui5/plugin.js +111 -6
  22. package/lib/processors/jsdoc/lib/ui5/template/publish.js +112 -91
  23. package/lib/processors/jsdoc/lib/ui5/template/utils/versionUtil.js +1 -1
  24. package/lib/processors/manifestCreator.js +8 -45
  25. package/lib/processors/minifier.js +90 -0
  26. package/lib/processors/resourceListCreator.js +2 -16
  27. package/lib/tasks/buildThemes.js +1 -1
  28. package/lib/tasks/bundlers/generateBundle.js +82 -14
  29. package/lib/tasks/bundlers/generateComponentPreload.js +31 -17
  30. package/lib/tasks/bundlers/generateFlexChangesBundle.js +7 -3
  31. package/lib/tasks/bundlers/generateLibraryPreload.js +127 -79
  32. package/lib/tasks/bundlers/generateManifestBundle.js +8 -10
  33. package/lib/tasks/bundlers/generateStandaloneAppBundle.js +54 -15
  34. package/lib/tasks/bundlers/utils/createModuleNameMapping.js +31 -0
  35. package/lib/tasks/generateCachebusterInfo.js +7 -3
  36. package/lib/tasks/generateLibraryManifest.js +6 -8
  37. package/lib/tasks/generateResourcesJson.js +15 -9
  38. package/lib/tasks/generateThemeDesignerResources.js +118 -2
  39. package/lib/tasks/generateVersionInfo.js +5 -5
  40. package/lib/tasks/jsdoc/generateJsdoc.js +1 -1
  41. package/lib/tasks/minify.js +41 -0
  42. package/lib/tasks/replaceVersion.js +1 -1
  43. package/lib/tasks/taskRepository.js +7 -15
  44. package/lib/tasks/transformBootstrapHtml.js +6 -1
  45. package/package.json +20 -19
  46. package/lib/builder/BuildContext.js +0 -39
  47. package/lib/builder/ProjectBuildContext.js +0 -55
  48. package/lib/builder/builder.js +0 -420
  49. package/lib/processors/debugFileCreator.js +0 -52
  50. package/lib/processors/resourceCopier.js +0 -24
  51. package/lib/processors/uglifier.js +0 -45
  52. package/lib/tasks/TaskUtil.js +0 -160
  53. package/lib/tasks/createDebugFiles.js +0 -30
  54. package/lib/tasks/uglify.js +0 -33
  55. package/lib/types/AbstractBuilder.js +0 -270
  56. package/lib/types/AbstractFormatter.js +0 -66
  57. package/lib/types/AbstractUi5Formatter.js +0 -95
  58. package/lib/types/application/ApplicationBuilder.js +0 -220
  59. package/lib/types/application/ApplicationFormatter.js +0 -227
  60. package/lib/types/application/applicationType.js +0 -15
  61. package/lib/types/library/LibraryBuilder.js +0 -237
  62. package/lib/types/library/LibraryFormatter.js +0 -519
  63. package/lib/types/library/libraryType.js +0 -15
  64. package/lib/types/module/ModuleBuilder.js +0 -7
  65. package/lib/types/module/ModuleFormatter.js +0 -54
  66. package/lib/types/module/moduleType.js +0 -15
  67. package/lib/types/themeLibrary/ThemeLibraryBuilder.js +0 -62
  68. package/lib/types/themeLibrary/ThemeLibraryFormatter.js +0 -90
  69. package/lib/types/themeLibrary/themeLibraryType.js +0 -15
  70. package/lib/types/typeRepository.js +0 -46
@@ -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("@jridgewell/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 = {
@@ -40,61 +40,30 @@ function isEmptyBundle(resolvedBundle) {
40
40
  return resolvedBundle.sections.every((section) => section.modules.length === 0);
41
41
  }
42
42
 
43
- const UI5BundleFormat = {
44
- beforePreloads(outW, section) {
45
- outW.write(`jQuery.sap.registerPreloadedModules(`);
46
- outW.writeln(`{`);
47
- if ( section.name ) {
48
- outW.writeln(`"name":"${section.name}",`);
49
- }
50
- outW.writeln(`"version":"2.0",`);
51
- outW.writeln(`"modules":{`);
52
- },
53
-
54
- afterPreloads(outW, section) {
55
- outW.writeln(`}});`);
56
- },
57
-
58
- beforeBundleInfo(outW) {
59
- outW.writeln("\"unsupported\"; /* 'bundleInfo' section mode not supported (requires ui5loader)");
60
- },
61
-
62
- afterBundleInfo(outW) {
63
- outW.writeln("*/");
64
- },
65
-
66
- requireSync(outW, moduleName) {
67
- outW.writeln(`sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");`);
68
- },
69
-
70
- shouldDecorate(resolvedModule) {
71
- return resolvedModule.executes(UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL);
72
- }
73
- };
74
-
75
43
  const EVOBundleFormat = {
76
- beforePreloads(outW, section) {
77
- outW.writeln(`sap.ui.require.preload({`);
44
+ beforePreloads(section) {
45
+ return `sap.ui.require.preload({\n`;
78
46
  },
79
47
 
80
- afterPreloads(outW, section) {
81
- outW.write(`}`);
48
+ afterPreloads(section) {
49
+ let str = `}`;
82
50
  if ( section.name ) {
83
- outW.write(`,"${section.name}"`);
51
+ str += `,"${section.name}"`;
84
52
  }
85
- outW.writeln(`);`);
53
+ str += `);\n`;
54
+ return str;
86
55
  },
87
56
 
88
- beforeBundleInfo(outW) {
89
- outW.writeln("sap.ui.loader.config({bundlesUI5:{");
57
+ beforeBundleInfo() {
58
+ return "sap.ui.loader.config({bundlesUI5:{\n";
90
59
  },
91
60
 
92
- afterBundleInfo(outW) {
93
- outW.writeln("}});");
61
+ afterBundleInfo() {
62
+ return "}});\n";
94
63
  },
95
64
 
96
- requireSync(outW, moduleName) {
97
- outW.writeln(`sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");`);
65
+ requireSync(moduleName) {
66
+ return `sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");\n`;
98
67
  },
99
68
 
100
69
  shouldDecorate(resolvedModule) {
@@ -114,7 +83,6 @@ class BundleBuilder {
114
83
  }
115
84
 
116
85
  async createBundle(module, options) {
117
- await this._prepare();
118
86
  if ( options.numberOfParts > 1 ) {
119
87
  const bundleInfos = [];
120
88
  const submodules = await this.splitter.run( module, options );
@@ -127,17 +95,6 @@ class BundleBuilder {
127
95
  }
128
96
  }
129
97
 
130
- _prepare() {
131
- return Promise.all([
132
- // check whether the resource pool contains debug and optimized sources
133
- this.pool.findResource( ModuleName.getDebugName(UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL) ).
134
- then( () => this.optimizedSources = true, () => this.optimizedSources = false ),
135
- // check whether EVO modules are available. If so, use EVO APIs, else use old UI5 APIs.
136
- this.pool.findResource(UI5ClientConstants.EVO_MARKER_RESOURCE).
137
- then( () => this.targetBundleFormat = EVOBundleFormat, () => this.targetBundleFormat = UI5BundleFormat )
138
- ]);
139
- }
140
-
141
98
  async _createBundle(module, options) {
142
99
  const resolvedModule = await this.resolver.resolve(module);
143
100
  if ( options.skipIfEmpty && isEmptyBundle(resolvedModule) ) {
@@ -148,15 +105,29 @@ class BundleBuilder {
148
105
 
149
106
  this.options = options || {};
150
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;
151
114
 
152
- // when decorateBootstrapModule is set to false, we don't write the optimized flag
153
- // 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
154
117
  this.shouldDecorate = this.options.decorateBootstrapModule &&
155
- (((this.optimizedSources && !this.options.debugMode) || this.optimize) &&
156
- this.targetBundleFormat.shouldDecorate(resolvedModule));
118
+ this.targetBundleFormat.shouldDecorate(resolvedModule);
157
119
  // TODO is the following condition ok or should the availability of jquery.sap.global.js be configurable?
158
120
  this.jqglobalAvailable = !resolvedModule.containsGlobal;
159
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
+
160
131
  let bundleInfos = [];
161
132
  // create all sections in sequence
162
133
  for ( const section of resolvedModule.sections ) {
@@ -185,6 +156,7 @@ class BundleBuilder {
185
156
  return {
186
157
  name: module.name,
187
158
  content: this.outW.toString(),
159
+ sourceMap: this.options.sourceMap ? JSON.stringify(this._sourceMap) : null,
188
160
  bundleInfo: bundleInfo
189
161
  };
190
162
  }
@@ -202,23 +174,36 @@ class BundleBuilder {
202
174
  }
203
175
  }
204
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
+
205
190
  closeModule(resolvedModule) {
206
191
  if ( resolvedModule.containsCore ) {
207
192
  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();`);
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();`);
210
196
  }
211
197
  if ( this.shouldDecorate && this.options.addTryCatchRestartWrapper ) {
212
198
  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(`}`);
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`);
216
206
  }
217
- /* NODE-TODO
218
- if ( writeSourceMap && writeSourceMapAnnotation ) {
219
- outW.ensureNewLine();
220
- outW.write("//# sourceMappingURL=" + moduleName.getBaseName().replaceFirst("\\.js$", ".js.map"));
221
- }*/
222
207
  }
223
208
 
224
209
  addSection(section) {
@@ -260,34 +245,43 @@ class BundleBuilder {
260
245
  // TODO check that there are only JS modules contained
261
246
  async writeRaw(section) {
262
247
  // write all modules in sequence
263
- for ( const module of section.modules ) {
264
- const resource = await this.pool.findResourceWithInfo(module);
248
+ for ( const moduleName of section.modules ) {
249
+ const resource = await this.pool.findResourceWithInfo(moduleName);
265
250
  if ( resource != null ) {
266
- this.outW.startSegment(module);
251
+ this.outW.startSegment(moduleName);
267
252
  this.outW.ensureNewLine();
268
- this.outW.writeln("//@ui5-bundle-raw-include " + module);
269
- await this.writeRawModule(module, resource);
253
+ this.outW.writeln("//@ui5-bundle-raw-include " + moduleName);
254
+ await this.writeRawModule(moduleName, resource);
270
255
  const compressedSize = this.outW.endSegment();
271
- 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);
272
258
  if ( section.declareRawModules ) {
273
- this.missingRawDeclarations.push(module);
259
+ this.missingRawDeclarations.push(moduleName);
274
260
  }
275
- if ( module === UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL ) {
261
+ if ( moduleName === UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL ) {
276
262
  this.jqglobalAvailable = true;
277
263
  }
278
264
  } else {
279
- log.error(" couldn't find %s", module);
265
+ log.error(" couldn't find %s", moduleName);
280
266
  }
281
267
  }
282
268
  }
283
269
 
284
- async writeRawModule(module, resource) {
285
- let fileContent = await resource.buffer();
286
- if ( /\.js$/.test(module) ) {
287
- fileContent = await this.compressJS( fileContent, resource );
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);
288
282
  }
283
+ this.outW.write(moduleContent);
289
284
  this.outW.ensureNewLine();
290
- this.outW.write( fileContent );
291
285
  }
292
286
 
293
287
  async writePreloadFunction(section) {
@@ -301,7 +295,7 @@ class BundleBuilder {
301
295
 
302
296
  await this.rewriteAMDModules(sequence);
303
297
  if ( sequence.length > 0 ) {
304
- this.targetBundleFormat.beforePreloads(outW, section);
298
+ this.writeWithSourceMap(this.targetBundleFormat.beforePreloads(section));
305
299
  let i = 0;
306
300
  for ( const module of sequence ) {
307
301
  const resource = await this.pool.findResourceWithInfo(module);
@@ -309,7 +303,7 @@ class BundleBuilder {
309
303
  if ( i>0 ) {
310
304
  outW.writeln(",");
311
305
  }
312
- this.beforeWritePreloadModule(module, resource.info, resource);
306
+ // this.beforeWritePreloadModule(module, resource.info, resource);
313
307
  outW.write(`\t"${module.toString()}":`);
314
308
  outW.startSegment(module);
315
309
  await this.writePreloadModule(module, resource.info, resource);
@@ -325,66 +319,83 @@ class BundleBuilder {
325
319
  if ( i > 0 ) {
326
320
  outW.writeln();
327
321
  }
328
- this.targetBundleFormat.afterPreloads(outW, section);
322
+ outW.write(this.targetBundleFormat.afterPreloads(section));
329
323
  }
330
324
 
331
325
  // this.afterWriteFunctionPreloadSection();
332
326
  }
333
327
 
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
328
  beforeWriteFunctionPreloadSection(sequence) {
357
329
  // simple version: just sort alphabetically
358
330
  sequence.sort();
359
331
  }
360
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
+
361
357
  async rewriteAMDModules(sequence) {
362
358
  if ( this.options.usePredefineCalls ) {
363
359
  const outW = this.outW;
364
360
 
365
361
  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);
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);
374
383
  outW.ensureNewLine();
375
- const fileContent = await this.compressJS(code, resource);
376
- outW.write( fileContent );
384
+ if (moduleSourceMap) {
385
+ this.addSourceMap(moduleName, moduleSourceMap);
386
+ }
387
+ outW.write(moduleContent);
377
388
  outW.ensureNewLine();
378
389
  const compressedSize = outW.endSegment();
379
- log.verbose(" %s (%d,%d)", module,
390
+ log.verbose(" %s (%d,%d)", moduleName,
380
391
  resource.info != null ? resource.info.size : -1, compressedSize);
381
392
  } else {
382
393
  // keep unprocessed modules
383
- remaining.push(module);
394
+ remaining.push(moduleName);
384
395
  }
385
396
  } else {
386
397
  // keep unprocessed modules
387
- remaining.push(module);
398
+ remaining.push(moduleName);
388
399
  }
389
400
  }
390
401
 
@@ -400,59 +411,71 @@ class BundleBuilder {
400
411
 
401
412
  /**
402
413
  *
403
- * @param {string} module module name
414
+ * @param {string} moduleName module name
404
415
  * @param {ModuleInfo} info
405
416
  * @param {module:@ui5/fs.Resource} resource
406
417
  * @returns {Promise<boolean>}
407
418
  */
408
- async writePreloadModule(module, info, resource) {
419
+ async writePreloadModule(moduleName, info, resource) {
409
420
  const outW = this.outW;
410
421
 
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 );
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
+ }
438
+ outW.write(moduleContent);
415
439
  this.exportGlobalNames(info);
416
440
  outW.ensureNewLine();
417
441
  outW.write(`}`);
418
- } else if ( /\.js$/.test(module) /* implicitly: && info != null && info.requiresTopLevelScope */ ) {
442
+ } else if ( /\.js$/.test(moduleName) /* implicitly: && info != null && info.requiresTopLevelScope */ ) {
419
443
  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();
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();
425
448
  outW.write( makeStringLiteral( fileContent ) );
426
- } else if ( /\.json$/.test(module) ) {
427
- let fileContent = await resource.buffer();
449
+ } else if ( /\.json$/.test(moduleName) ) {
450
+ let fileContent = (await resource.buffer()).toString();
428
451
  if ( this.optimize ) {
429
452
  try {
430
453
  fileContent = JSON.stringify( JSON.parse( fileContent) );
431
454
  } catch (e) {
432
- 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);
433
456
  log.verbose(e);
434
457
  }
435
458
  }
436
459
  outW.write(makeStringLiteral(fileContent));
437
- } else if ( /\.xml$/.test(module) ) {
438
- let fileContent = await resource.buffer();
460
+ } else if ( /\.xml$/.test(moduleName) ) {
461
+ let fileContent = (await resource.buffer()).toString();
439
462
  if ( this.optimize ) {
440
463
  // For XML we use the pretty data
441
464
  // Do not minify if XML(View) contains an <*:pre> tag,
442
465
  // 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);
466
+ if (!xmlHtmlPrePattern.test(fileContent)) {
467
+ fileContent = pd.xmlmin(fileContent, false);
445
468
  }
446
469
  }
447
470
  outW.write( makeStringLiteral( fileContent ) );
448
- } else if ( /\.properties$/.test(module) ) {
471
+ } else if ( /\.properties$/.test(moduleName) ) {
449
472
  // Since the Builder is also used when building non-project resources (e.g. dependencies)
450
473
  // *.properties files should be escaped if encoding option is specified
451
474
  const fileContent = await escapePropertiesFile(resource);
452
475
 
453
476
  outW.write( makeStringLiteral( fileContent ) );
454
477
  } else {
455
- 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?
456
479
  }
457
480
 
458
481
  return true;
@@ -470,18 +493,19 @@ class BundleBuilder {
470
493
  this.outW.ensureNewLine();
471
494
  info.exposedGlobals.forEach( (globalName) => {
472
495
  // 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};`);
496
+ this.writeWithSourceMap(`this.${globalName}=${globalName};\n`);
474
497
  });
475
498
  }
476
499
 
477
500
  writeBundleInfos(sections) {
478
501
  this.outW.ensureNewLine();
479
502
 
503
+ let bundleInfoStr = "";
480
504
  if ( sections.length > 0 ) {
481
- this.targetBundleFormat.beforeBundleInfo(this.outW);
505
+ bundleInfoStr = this.targetBundleFormat.beforeBundleInfo();
482
506
  sections.forEach((section, idx) => {
483
507
  if ( idx > 0 ) {
484
- this.outW.writeln(",");
508
+ bundleInfoStr += ",\n";
485
509
  }
486
510
 
487
511
  if (!section.name) {
@@ -492,31 +516,128 @@ class BundleBuilder {
492
516
  `The info might not work as expected. ` +
493
517
  `The name must match the bundle filename (incl. extension such as '.js')`);
494
518
  }
495
- this.outW.write(`"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`);
519
+ bundleInfoStr += `"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`;
496
520
  });
497
- this.outW.writeln();
498
- this.targetBundleFormat.afterBundleInfo(this.outW);
521
+ bundleInfoStr += "\n";
522
+ bundleInfoStr += this.targetBundleFormat.afterBundleInfo();
523
+
524
+ this.writeWithSourceMap(bundleInfoStr);
499
525
  }
500
526
  }
501
527
 
502
528
  writeRequires(section) {
503
529
  this.outW.ensureNewLine();
504
530
  section.modules.forEach( (module) => {
505
- this.targetBundleFormat.requireSync(this.outW, module);
531
+ this.writeWithSourceMap(this.targetBundleFormat.requireSync(module));
506
532
  });
507
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
+ }
508
622
  }
509
623
 
510
624
  const CALL_SAP_UI_DEFINE = ["sap", "ui", "define"];
511
625
 
512
- 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}) {
513
635
  let ast;
514
- const codeStr = code.toString();
515
636
  try {
516
- ast = parseJS(codeStr, {range: true});
637
+ ast = parseJS(moduleContent, {range: true});
517
638
  } catch (e) {
518
639
  log.error("error while parsing %s: %s", moduleName, e.message);
519
- return;
640
+ return {};
520
641
  }
521
642
 
522
643
  if ( ast.type === Syntax.Program &&
@@ -525,7 +646,6 @@ function rewriteDefine(targetBundleFormat, code, moduleName) {
525
646
  const changes = [];
526
647
  const defineCall = ast.body[0].expression;
527
648
 
528
-
529
649
  // Inject module name if missing
530
650
  if ( defineCall.arguments.length == 0 ||
531
651
  defineCall.arguments[0].type !== Syntax.Literal ) {
@@ -543,7 +663,6 @@ function rewriteDefine(targetBundleFormat, code, moduleName) {
543
663
 
544
664
  changes.push({
545
665
  index,
546
- count: 0,
547
666
  value
548
667
  });
549
668
  }
@@ -555,29 +674,109 @@ function rewriteDefine(targetBundleFormat, code, moduleName) {
555
674
  changes.push({
556
675
  // asterisk marks the index: sap.ui.*define()
557
676
  index: defineCall.callee.property.range[0],
558
- count: 0,
559
677
  value: "pre"
560
678
  });
561
679
  }
562
680
 
563
- return applyChanges(codeStr, changes);
681
+ return transform(changes, moduleContent, moduleSourceMap);
564
682
  }
565
683
 
566
- return false;
684
+ return null;
567
685
  }
568
686
 
569
- function applyChanges(string, changes) {
570
- // No sorting needed as changes are added in correct order
571
-
572
- 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
573
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
574
722
  array.splice(
575
723
  change.index,
576
- change.count,
724
+ 0,
577
725
  change.value
578
726
  );
579
727
  });
580
- 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;
581
774
  }
582
775
 
583
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
+ }