2019-09-05 21:15:40 +03:00
|
|
|
///
|
2022-01-17 14:07:46 +02:00
|
|
|
/// Copyright © 2016-2022 The Thingsboard Authors
|
2019-09-05 21:15:40 +03:00
|
|
|
///
|
|
|
|
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
/// you may not use this file except in compliance with the License.
|
|
|
|
|
/// You may obtain a copy of the License at
|
|
|
|
|
///
|
|
|
|
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
///
|
|
|
|
|
/// Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
/// See the License for the specific language governing permissions and
|
|
|
|
|
/// limitations under the License.
|
|
|
|
|
///
|
|
|
|
|
|
2020-02-11 20:03:37 +02:00
|
|
|
import {
|
|
|
|
|
Compiler,
|
|
|
|
|
ComponentFactory,
|
|
|
|
|
Inject,
|
|
|
|
|
Injectable,
|
|
|
|
|
Injector,
|
|
|
|
|
ModuleWithComponentFactories,
|
|
|
|
|
Type
|
|
|
|
|
} from '@angular/core';
|
2019-09-05 21:15:40 +03:00
|
|
|
import { DOCUMENT } from '@angular/common';
|
2020-02-11 20:03:37 +02:00
|
|
|
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
|
2020-07-15 12:44:16 +03:00
|
|
|
import { HttpClient } from '@angular/common/http';
|
2021-12-06 12:54:48 +02:00
|
|
|
import { IModulesMap } from '@modules/common/modules-map.models';
|
2019-09-05 21:15:40 +03:00
|
|
|
|
2021-12-06 12:54:48 +02:00
|
|
|
declare const System;
|
2019-12-23 14:36:44 +02:00
|
|
|
|
2022-05-11 18:16:30 +03:00
|
|
|
export interface ModulesWithFactories {
|
|
|
|
|
modules: Type<any>[];
|
|
|
|
|
factories: ComponentFactory<any>[];
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 21:15:40 +03:00
|
|
|
@Injectable({
|
|
|
|
|
providedIn: 'root'
|
|
|
|
|
})
|
|
|
|
|
export class ResourcesService {
|
|
|
|
|
|
|
|
|
|
private loadedResources: { [url: string]: ReplaySubject<any> } = {};
|
2020-07-15 12:44:16 +03:00
|
|
|
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
|
2022-05-11 18:16:30 +03:00
|
|
|
private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {};
|
2019-09-05 21:15:40 +03:00
|
|
|
|
|
|
|
|
private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0];
|
|
|
|
|
|
2019-12-23 14:36:44 +02:00
|
|
|
constructor(@Inject(DOCUMENT) private readonly document: any,
|
|
|
|
|
private compiler: Compiler,
|
2020-07-15 12:44:16 +03:00
|
|
|
private http: HttpClient,
|
2019-12-23 14:36:44 +02:00
|
|
|
private injector: Injector) {}
|
2019-09-05 21:15:40 +03:00
|
|
|
|
|
|
|
|
public loadResource(url: string): Observable<any> {
|
|
|
|
|
if (this.loadedResources[url]) {
|
|
|
|
|
return this.loadedResources[url].asObservable();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let fileType;
|
2020-05-25 21:36:54 +03:00
|
|
|
const match = /[./](css|less|html|htm|js)?(([?#]).*)?$/.exec(url);
|
2019-09-05 21:15:40 +03:00
|
|
|
if (match !== null) {
|
|
|
|
|
fileType = match[1];
|
|
|
|
|
}
|
|
|
|
|
if (!fileType) {
|
|
|
|
|
return throwError(new Error(`Unable to detect file type from url: ${url}`));
|
|
|
|
|
} else if (fileType !== 'css' && fileType !== 'js') {
|
|
|
|
|
return throwError(new Error(`Unsupported file type: ${fileType}`));
|
|
|
|
|
}
|
|
|
|
|
return this.loadResourceByType(fileType, url);
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-11 18:16:30 +03:00
|
|
|
public loadFactories(url: string, modulesMap: IModulesMap): Observable<ModulesWithFactories> {
|
|
|
|
|
if (this.loadedModulesAndFactories[url]) {
|
|
|
|
|
return this.loadedModulesAndFactories[url].asObservable();
|
2019-12-23 14:36:44 +02:00
|
|
|
}
|
2021-12-06 12:54:48 +02:00
|
|
|
modulesMap.init();
|
2022-05-11 18:16:30 +03:00
|
|
|
const subject = new ReplaySubject<ModulesWithFactories>();
|
|
|
|
|
this.loadedModulesAndFactories[url] = subject;
|
2021-12-06 12:54:48 +02:00
|
|
|
import('@angular/compiler').then(
|
|
|
|
|
() => {
|
|
|
|
|
System.import(url).then(
|
|
|
|
|
(module) => {
|
|
|
|
|
const modules = this.extractNgModules(module);
|
|
|
|
|
if (modules.length) {
|
2020-12-30 17:13:01 +02:00
|
|
|
const tasks: Promise<ModuleWithComponentFactories<any>>[] = [];
|
|
|
|
|
for (const m of modules) {
|
|
|
|
|
tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m));
|
2019-12-23 14:36:44 +02:00
|
|
|
}
|
2020-12-30 17:13:01 +02:00
|
|
|
forkJoin(tasks).subscribe((compiled) => {
|
|
|
|
|
try {
|
|
|
|
|
const componentFactories: ComponentFactory<any>[] = [];
|
|
|
|
|
for (const c of compiled) {
|
|
|
|
|
c.ngModuleFactory.create(this.injector);
|
|
|
|
|
componentFactories.push(...c.componentFactories);
|
|
|
|
|
}
|
2022-05-11 18:16:30 +03:00
|
|
|
const modulesWithFactories: ModulesWithFactories = {
|
|
|
|
|
modules,
|
|
|
|
|
factories: componentFactories
|
|
|
|
|
};
|
|
|
|
|
this.loadedModulesAndFactories[url].next(modulesWithFactories);
|
|
|
|
|
this.loadedModulesAndFactories[url].complete();
|
2020-12-30 17:13:01 +02:00
|
|
|
} catch (e) {
|
2022-05-11 18:16:30 +03:00
|
|
|
this.loadedModulesAndFactories[url].error(new Error(`Unable to init module from url: ${url}`));
|
|
|
|
|
delete this.loadedModulesAndFactories[url];
|
2020-12-30 17:13:01 +02:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
(e) => {
|
2022-05-11 18:16:30 +03:00
|
|
|
this.loadedModulesAndFactories[url].error(new Error(`Unable to compile module from url: ${url}`));
|
|
|
|
|
delete this.loadedModulesAndFactories[url];
|
2021-12-06 12:54:48 +02:00
|
|
|
});
|
|
|
|
|
} else {
|
2022-05-11 18:16:30 +03:00
|
|
|
this.loadedModulesAndFactories[url].error(new Error(`Module '${url}' doesn't have default export!`));
|
|
|
|
|
delete this.loadedModulesAndFactories[url];
|
2021-12-06 12:54:48 +02:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
(e) => {
|
2022-05-11 18:16:30 +03:00
|
|
|
this.loadedModulesAndFactories[url].error(new Error(`Unable to load module from url: ${url}`));
|
|
|
|
|
delete this.loadedModulesAndFactories[url];
|
2021-12-06 12:54:48 +02:00
|
|
|
}
|
|
|
|
|
);
|
2020-07-15 12:44:16 +03:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
return subject.asObservable();
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-06 12:54:48 +02:00
|
|
|
public loadModules(url: string, modulesMap: IModulesMap): Observable<Type<any>[]> {
|
2020-07-15 12:44:16 +03:00
|
|
|
if (this.loadedModules[url]) {
|
|
|
|
|
return this.loadedModules[url].asObservable();
|
|
|
|
|
}
|
2021-12-06 12:54:48 +02:00
|
|
|
modulesMap.init();
|
2020-07-15 12:44:16 +03:00
|
|
|
const subject = new ReplaySubject<Type<any>[]>();
|
|
|
|
|
this.loadedModules[url] = subject;
|
2021-12-06 12:54:48 +02:00
|
|
|
import('@angular/compiler').then(
|
|
|
|
|
() => {
|
|
|
|
|
System.import(url).then(
|
|
|
|
|
(module) => {
|
|
|
|
|
try {
|
|
|
|
|
let modules;
|
|
|
|
|
try {
|
|
|
|
|
modules = this.extractNgModules(module);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
}
|
|
|
|
|
if (modules && modules.length) {
|
2020-12-30 17:13:01 +02:00
|
|
|
const tasks: Promise<ModuleWithComponentFactories<any>>[] = [];
|
|
|
|
|
for (const m of modules) {
|
|
|
|
|
tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m));
|
2020-07-15 12:44:16 +03:00
|
|
|
}
|
2020-12-30 17:13:01 +02:00
|
|
|
forkJoin(tasks).subscribe((compiled) => {
|
|
|
|
|
try {
|
|
|
|
|
for (const c of compiled) {
|
|
|
|
|
c.ngModuleFactory.create(this.injector);
|
|
|
|
|
}
|
|
|
|
|
this.loadedModules[url].next(modules);
|
|
|
|
|
this.loadedModules[url].complete();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`));
|
|
|
|
|
delete this.loadedModules[url];
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
(e) => {
|
|
|
|
|
this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`));
|
|
|
|
|
delete this.loadedModules[url];
|
|
|
|
|
});
|
2021-12-06 12:54:48 +02:00
|
|
|
} else {
|
|
|
|
|
this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`));
|
|
|
|
|
delete this.loadedModules[url];
|
2020-12-30 17:13:01 +02:00
|
|
|
}
|
2021-12-06 12:54:48 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`));
|
|
|
|
|
delete this.loadedModules[url];
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
(e) => {
|
|
|
|
|
this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`));
|
2020-07-15 14:56:38 +03:00
|
|
|
delete this.loadedModules[url];
|
2021-12-06 12:54:48 +02:00
|
|
|
console.error(`Unable to load module from url: ${url}`, e);
|
2020-07-15 14:56:38 +03:00
|
|
|
}
|
2021-12-06 12:54:48 +02:00
|
|
|
);
|
2019-12-23 14:36:44 +02:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
return subject.asObservable();
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-13 16:56:44 +03:00
|
|
|
private extractNgModules(module: any, modules: Type<any>[] = []): Type<any>[] {
|
|
|
|
|
try {
|
|
|
|
|
let potentialModules = [module];
|
|
|
|
|
let currentScanDepth = 0;
|
|
|
|
|
|
2021-12-06 12:54:48 +02:00
|
|
|
while (potentialModules.length && currentScanDepth < 10) {
|
|
|
|
|
const newPotentialModules = [];
|
|
|
|
|
for (const potentialModule of potentialModules) {
|
|
|
|
|
if (potentialModule && ('ɵmod' in potentialModule)) {
|
|
|
|
|
modules.push(potentialModule);
|
2021-05-13 16:56:44 +03:00
|
|
|
} else {
|
2021-12-06 12:54:48 +02:00
|
|
|
for (const k of Object.keys(potentialModule)) {
|
|
|
|
|
if (!this.isPrimitive(potentialModule[k])) {
|
|
|
|
|
newPotentialModules.push(potentialModule[k]);
|
2021-05-13 16:56:44 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
potentialModules = newPotentialModules;
|
|
|
|
|
currentScanDepth++;
|
2020-02-11 20:03:37 +02:00
|
|
|
}
|
2021-12-06 12:54:48 +02:00
|
|
|
} catch (e) {
|
2021-05-13 16:56:44 +03:00
|
|
|
console.log('Could not load NgModule', e);
|
2020-02-11 20:03:37 +02:00
|
|
|
}
|
|
|
|
|
return modules;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-13 16:56:44 +03:00
|
|
|
private isPrimitive(test) {
|
|
|
|
|
return test !== Object(test);
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 21:15:40 +03:00
|
|
|
private loadResourceByType(type: 'css' | 'js', url: string): Observable<any> {
|
|
|
|
|
const subject = new ReplaySubject();
|
|
|
|
|
this.loadedResources[url] = subject;
|
|
|
|
|
let el;
|
|
|
|
|
let loaded = false;
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'js':
|
|
|
|
|
el = this.document.createElement('script');
|
|
|
|
|
el.type = 'text/javascript';
|
2022-02-01 14:49:02 +02:00
|
|
|
el.async = false;
|
2019-09-05 21:15:40 +03:00
|
|
|
el.src = url;
|
|
|
|
|
break;
|
|
|
|
|
case 'css':
|
|
|
|
|
el = this.document.createElement('link');
|
|
|
|
|
el.type = 'text/css';
|
|
|
|
|
el.rel = 'stylesheet';
|
|
|
|
|
el.href = url;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
el.onload = el.onreadystatechange = (e) => {
|
|
|
|
|
if (el.readyState && !/^c|loade/.test(el.readyState) || loaded) { return; }
|
|
|
|
|
el.onload = el.onreadystatechange = null;
|
|
|
|
|
loaded = true;
|
|
|
|
|
this.loadedResources[url].next();
|
|
|
|
|
this.loadedResources[url].complete();
|
|
|
|
|
};
|
|
|
|
|
el.onerror = () => {
|
|
|
|
|
this.loadedResources[url].error(new Error(`Unable to load ${url}`));
|
|
|
|
|
delete this.loadedResources[url];
|
|
|
|
|
};
|
|
|
|
|
this.anchor.appendChild(el);
|
|
|
|
|
return subject.asObservable();
|
|
|
|
|
}
|
|
|
|
|
}
|