UNPKG

14.2 kBJavaScriptView Raw
1var { reduce, forEach, isEmpty, get, find, map } = require("lodash");
2var { dirname, join: pathJoin } = require("path");
3var { warn } = require("console");
4var { existsSync, readFileSync } = require("fs");
5var findNodeModules = require('find-node-modules');
6var log = require("fancy-log");
7var colors = require("ansi-colors");
8var ui5Parser = require("./ui5_parser");
9
10var jsParser = {
11 parse: source => {
12 return require("recast").parse(source, { parser: ui5Parser });
13 }
14};
15
16var traverseSource = (source, options) => {
17 return require("@babel/traverse").default(jsParser.parse(source), options);
18};
19
20var fetch = require("node-fetch");
21var UglifyJS = require("uglify-js");
22var parseString = require("xml2js").parseString;
23var crypto = require("crypto");
24var { UI5Cache } = require("./cache");
25
26var { eachDeep } = require("deepdash")(require("lodash"));
27
28var persistCache = UI5Cache.load();
29
30var FIVE_MINUTES = 5 * 60 * 1000;
31
32var BASE64 = "base64";
33
34/**
35 * UI5 Library List
36 */
37var UI5Libraries = [
38 "sap/ui/core",
39 "sap/ui/layout",
40 "sap/ui/unified",
41 "sap/ui/table",
42 "sap/ui/commons",
43 "sap/ui/viz",
44 "sap/ui/suite",
45 "sap/ui/richtexteditor",
46 "sap/ui/comp",
47 "sap/m",
48 "sap/f",
49 "sap/gantt",
50 "sap/ushell",
51 "sap/tnt",
52 "sap/uxap"
53];
54
55/**
56 * md5 hash
57 */
58var md5 = s => {
59 var md5 = crypto.createHash("md5");
60 return md5.update(s).digest("hex");
61};
62
63var readBinary = async url => {
64 var GlobalResourceCache = persistCache.get("GlobalResourceCache") || {};
65 var hash = md5(url);
66 var base64Content = GlobalResourceCache[hash];
67
68 if (!base64Content) {
69 var response = await fetch(url, { timeout: FIVE_MINUTES });
70 var buf = await response.buffer();
71 GlobalResourceCache[hash] = buf.toString(BASE64);
72 persistCache.set("GlobalResourceCache", GlobalResourceCache);
73 return buf;
74 } else {
75 return Buffer.from(base64Content, BASE64);
76 }
77};
78
79var readURLFromCache = async url => {
80 var GlobalResourceCache = persistCache.get("GlobalResourceCache") || {};
81 var hash = md5(url);
82 var urlContent = GlobalResourceCache[hash];
83 if (!urlContent) {
84 var response = await fetch(url, { timeout: FIVE_MINUTES });
85 if (response.status == 404) {
86 log.warn("[preload]", colors.yellow(`Can not fetch module: ${url}`));
87 }
88 urlContent = await response.text();
89 GlobalResourceCache[hash] = urlContent;
90 persistCache.set("GlobalResourceCache", GlobalResourceCache);
91 }
92 return urlContent;
93};
94
95var isUi5CoreCoreJs = (mName = "") => {
96 return mName && (
97 mName.startsWith("jquery.") ||
98 mName.startsWith("sap-ui-") ||
99 mName.startsWith("ui5loader") ||
100 mName.startsWith("sap/ui/support/jQuery")
101 );
102};
103
104/**
105 * get the library name of a module
106 * @param {string} mName
107 */
108var getSourceLibraryName = mName => {
109 var rt;
110 if (isUi5CoreCoreJs(mName)) {
111 return "sap.ui.core";
112 }
113 forEach(UI5Libraries, libraryName => {
114 if (mName.startsWith(libraryName)) {
115 rt = libraryName;
116 }
117 });
118 return rt;
119};
120
121/**
122 * normalize library name sap/ui/core -> sap.ui.core
123 * @param {string} lName library name
124 */
125var normalizeLibraryName = (lName = "") => lName.replace(/\//g, ".");
126
127var normalizeModuleName = (mName = "") => {
128 if (isUi5CoreCoreJs(mName)) {
129 return mName;
130 } else {
131 return mName.replace(/\\/g, "/").replace(/\./g, "/");
132 }
133};
134
135var formatNodeModulesPath = mName => {
136 var nmPath = findNodeModules({ relative: false });
137 var libraryName = getSourceLibraryName(mName);
138 if (nmPath && libraryName) {
139 return pathJoin(nmPath[0], "@openui5", normalizeLibraryName(libraryName), "src", `${mName}.js`);
140 } else {
141 return "";
142 }
143};
144
145/**
146 * read file from path
147 *
148 * @param {string} u path
149 */
150var readFile = u => readFileSync(u, { encoding: "UTF-8" });
151
152var fetchSource = async(mName, resourceRoot = "") => {
153
154 var url = formatNodeModulesPath(mName);
155
156 if (existsSync(url)) {
157 // prefer read file from local node modules
158 return readFile(url);
159 } else {
160 url = `${resourceRoot}${mName}.js`;
161 try {
162 return await readURLFromCache(url);
163 } catch (error) {
164 warn(`fetch ${mName} failed ${error}`);
165 throw error;
166 }
167 }
168
169};
170
171var fetchAllResource = async(resourceList = [], resourceRoot = "") => {
172 var rt = {};
173
174 await Promise.all(
175 resourceList.map(async r => {
176 var url = `${resourceRoot}${r}`;
177 try {
178 rt[r] = await readURLFromCache(url);
179 return rt[r];
180 } catch (error) {
181 warn(`fetch ${r} failed ${error}`);
182 throw error;
183 }
184 })
185 );
186
187 return rt;
188};
189
190/**
191 * find Ui5 Module Name from source code
192 *
193 * @param {string} source string
194 */
195var findUi5ModuleName = source => {
196 var mName = "";
197
198 traverseSource(source, {
199 CallExpression({ node }) {
200 const nodeGet = path => get(node, path);
201 const callArguments = nodeGet("arguments");
202 if (callArguments) {
203 // with arguments
204
205 // sap.ui.define
206 if (
207 nodeGet("callee.object.object.name") == "sap" &&
208 nodeGet("callee.object.property.name") == "ui" &&
209 (nodeGet("callee.property.name") == "define" || nodeGet("callee.property.name") == "predefine")
210 ) {
211 // find name
212 var literal = find(callArguments, arg => (arg.type == "Literal" || arg.type == "StringLiteral"));
213 mName = literal.value;
214 }
215 }
216 }
217 });
218
219 return mName;
220};
221
222/**
223 * find modules in sap.ui.define pattern
224 */
225var findAllUi5StandardModules = (source, sourceName = "") => {
226 var base = dirname(sourceName);
227 var deps = [];
228 var addDependency = dependency => { if (dependency) {deps = deps.concat(dependency);} };
229
230 traverseSource(source, {
231 CallExpression({ node }) {
232 const nodeGet = path => get(node, path);
233 const callArguments = nodeGet("arguments");
234 if (callArguments) {// with arguments)
235
236 // sap.ui.define
237 if (
238 nodeGet("callee.object.object.name") == "sap" &&
239 nodeGet("callee.object.property.name") == "ui" &&
240 (nodeGet("callee.property.name") == "define" || nodeGet("callee.property.name") == "predefine")
241 ) {
242 // find []
243 var arrayExpression = find(nodeGet("arguments"), arg => arg.type == "ArrayExpression");
244 if (arrayExpression && arrayExpression.elements) {
245 addDependency(map(arrayExpression.elements, ele => ele.value));
246 }
247 }
248
249 // sap.ui.require
250 if (
251 nodeGet("callee.object.object.name") == "sap" &&
252 nodeGet("callee.object.property.name") == "ui" &&
253 nodeGet("callee.property.name") == "require"
254 ) {
255 // var JSONModel = sap.ui.require("sap/ui/model/json/JSONModel");
256 if (callArguments.length == 1 && (callArguments[0].type == "Literal" || callArguments[0].type == "StringLiteral")) {
257 addDependency(callArguments[0].value);
258 } else {
259 // sap.ui.require(['sap/ui/model/json/JSONModel', 'sap/ui/core/UIComponent'], function(JSONModel,UIComponent) {});
260 var e2 = find(nodeGet("arguments"), arg => arg.type == "ArrayExpression");
261 if (e2 && e2.elements) {
262 addDependency(map(e2.elements, ele => ele.value));
263 }
264 }
265 }
266
267 // sap.ui.requireSync
268 if (
269 nodeGet("callee.object.object.name") == "sap" &&
270 nodeGet("callee.object.property.name") == "ui" &&
271 nodeGet("callee.property.name") == "requireSync"
272 ) {
273 // var JSONModel = sap.ui.requireSync("sap/ui/model/json/JSONModel");
274 if (callArguments.length == 1 && (callArguments[0].type == "Literal" || callArguments[0].type == "StringLiteral")) {
275 addDependency(callArguments[0].value);
276 } else {
277 // sap.ui.requireSync(['sap/ui/model/json/JSONModel', 'sap/ui/core/UIComponent'], function(JSONModel,UIComponent) {});
278 var e3 = find(nodeGet("arguments"), arg => arg.type == "ArrayExpression");
279 if (e3 && e3.elements) {
280 addDependency(map(e3.elements, ele => ele.value));
281 }
282 }
283 }
284 // jQuery.sap.require
285 if (
286 nodeGet("callee.object.object.name") == "jQuery" &&
287 nodeGet("callee.object.property.name") == "ui" &&
288 nodeGet("callee.property.name") == "require"
289 ) {
290 if (callArguments.length == 1 && (callArguments[0].type == "Literal" || callArguments[0].type == "StringLiteral")) {
291 addDependency(callArguments[0].value);
292 }
293 }
294
295
296 // sap.ui.lazyRequire
297 if (
298 nodeGet("callee.object.object.name") == "sap" &&
299 nodeGet("callee.object.property.name") == "ui" &&
300 nodeGet("callee.property.name") == "lazyRequire"
301 ) {
302 if (callArguments.length == 1 && (callArguments[0].type == "Literal" || callArguments[0].type == "StringLiteral")) {
303 addDependency(callArguments[0].value);
304 } else {
305 var e4 = find(nodeGet("arguments"), arg => arg.type == "ArrayExpression");
306 if (e4 && e4.elements) {
307 addDependency(map(e4.elements, ele => ele.value));
308 }
309 }
310 }
311 }
312 }
313 });
314
315 return map(deps, d => {
316 if (d.startsWith("./") || d.startsWith("../")) {
317 d = pathJoin(base, d);
318 d = d.replace(/\\/g, "/");// replace \ to / after join
319 }
320 return normalizeModuleName(d);
321 });
322
323};
324
325var findAllUi5ViewModules = async(source, sourceName) => {
326 try {
327 return await new Promise((resolve, reject) => {
328 var ds = new Set();
329 parseString(source, { xmlns: true }, function(err, result) {
330 if (err) {
331 reject(err);
332 } else {
333 eachDeep(result, value => {
334 if (value && value.$ns) {
335 var mName = `${value.$ns.uri}.${value.$ns.local}`.replace(
336 /\./g,
337 "/"
338 );
339 ds.add(mName);
340 }
341 });
342 resolve(Array.from(ds));
343 }
344 });
345 });
346 } catch (error) {
347 warn(`parse ${sourceName} modules failed: ${error}`);
348 return [];
349 }
350};
351
352var findAllImportModules = (source, sourceName = "") => {
353 var base = dirname(sourceName);
354 var rt = [];
355 var addImportedModules = (m) => {
356 if (m.startsWith("./") || m.startsWith("../")) {
357 // relative module
358 rt = rt.concat(pathJoin(base, m).replace(/\\/g, "/"));
359 } else {
360 rt = rt.concat(m);
361 }
362 };
363
364 traverseSource(source, {
365 ImportDeclaration: ({ node }) => {
366 const nodeGet = path => get(node, path);
367 const importedModuleName = nodeGet("source.value");
368 if (importedModuleName) {
369 addImportedModules(importedModuleName);
370 }
371 }
372 });
373
374 return rt;
375};
376
377// change recursively to iteration
378var resolveUI5Module = async(sModuleNames = [], resourceRoot) => {
379 var globalModuleCache = persistCache.get("GlobalModuleCache") || {};
380 // this time used modules
381 var modules = {};
382 // without cache
383 var moduleDeps = {};
384
385 // set entry
386 moduleDeps["entry"] = sModuleNames;
387
388 for (; ;) {
389 var needToBeLoad = new Set();
390
391 forEach(moduleDeps, dep => {
392 forEach(dep, d => {
393 if (modules[d] == undefined) {
394 needToBeLoad.add(d);
395 }
396 });
397 });
398
399 if (isEmpty(needToBeLoad)) {
400 // no more dependencies need to be analyzed
401 // break from this loop
402 break;
403 } else {
404 await Promise.all(
405 Array.from(needToBeLoad).map(async mName => {
406 try {
407
408 var source = "";
409
410 try {
411 source = await fetchSource(mName, resourceRoot);
412 } catch (error) {
413 // retry once
414 source = await fetchSource(mName, resourceRoot);
415 }
416
417 modules[mName] = source;
418 var sourceHash = md5(source);
419
420 // use cache here
421 if (globalModuleCache[sourceHash]) {
422 moduleDeps[mName] = globalModuleCache[sourceHash];
423 }
424
425 // not found dependency from cache
426 if (!moduleDeps[mName]) {
427 moduleDeps[mName] = findAllUi5StandardModules(source, mName);
428 globalModuleCache[sourceHash] = moduleDeps[mName];
429 }
430
431 } catch (error) {
432 modules[mName] = "";
433 moduleDeps[mName] = [];
434 }
435 })
436 );
437 }
438 }
439
440 persistCache.set("GlobalModuleCache", globalModuleCache);
441
442 return modules;
443};
444
445
446
447/**
448 * find out all ui5 libraries
449 * @param {string[]} modules name
450 *
451 * @returns {string[]} lib names
452 */
453var findAllLibraries = (modules = []) => {
454 var rt = new Set();
455 forEach(modules, m => {
456 forEach(UI5Libraries, l => {
457 if (m.startsWith(l)) {
458 rt.add(l);
459 }
460 });
461 });
462 return Array.from(rt);
463};
464
465
466var isUI5StandardModule = sModuleName => {
467 var rt = false;
468 UI5Libraries.forEach(packageName => {
469 if (sModuleName && sModuleName.startsWith(packageName)) {
470 rt = true;
471 }
472 });
473 return rt;
474};
475
476/**
477 * temporary in memory uglify cache
478 */
479var TmpUglifyNameCache = {};
480
481/**
482 * To generate preload file content
483 * @param {*} cache object
484 * @param {*} resources list
485 */
486var generatePreloadFile = (cache = {}, resources = {}) => {
487 var modules = reduce(
488 cache,
489 (pre, moduleSource, moduleName) => {
490 // ignore core modules, will be load on bootstrap
491 if (!moduleName.startsWith("sap/ui/core")) {
492 var sourceHash = md5(moduleSource);
493 var compressed = TmpUglifyNameCache[sourceHash];
494 if (!compressed) {
495 compressed = UglifyJS.minify(moduleSource).code;
496 }
497 pre[`${moduleName}.js`] = compressed;
498 TmpUglifyNameCache[sourceHash] = compressed;
499 }
500 return pre;
501 },
502 {}
503 );
504
505 forEach(resources, (content, resourceName) => {
506 modules[resourceName] = content;
507 });
508
509 return `sap.ui.require.preload(${JSON.stringify(modules)})`;
510};
511
512module.exports = {
513 fetchAllResource,
514 generatePreloadFile,
515 fetchSource,
516 findAllUi5ViewModules,
517 isUI5StandardModule,
518 findAllImportModules,
519 findAllUi5StandardModules,
520 findUi5ModuleName,
521 resolveUI5Module,
522 findAllLibraries,
523 readURLFromCache,
524 readBinary,
525 persistCache
526};