thingsboard/ui-ngx/src/app/core/services/resources.service.ts

241 lines
8.5 KiB
TypeScript
Raw Normal View History

///
2021-01-11 13:42:16 +02:00
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// 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';
import { DOCUMENT } from '@angular/common';
2020-02-11 20:03:37 +02:00
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
import { HttpClient } from '@angular/common/http';
2021-12-06 12:54:48 +02:00
import { IModulesMap } from '@modules/common/modules-map.models';
2021-12-06 12:54:48 +02:00
declare const System;
2019-12-23 14:36:44 +02:00
@Injectable({
providedIn: 'root'
})
export class ResourcesService {
private loadedResources: { [url: string]: ReplaySubject<any> } = {};
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
private loadedFactories: { [url: string]: ReplaySubject<ComponentFactory<any>[]> } = {};
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,
private http: HttpClient,
2019-12-23 14:36:44 +02:00
private injector: Injector) {}
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);
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);
}
2021-12-06 12:54:48 +02:00
public loadFactories(url: string, modulesMap: IModulesMap): Observable<ComponentFactory<any>[]> {
if (this.loadedFactories[url]) {
return this.loadedFactories[url].asObservable();
2019-12-23 14:36:44 +02:00
}
2021-12-06 12:54:48 +02:00
modulesMap.init();
2020-02-11 20:03:37 +02:00
const subject = new ReplaySubject<ComponentFactory<any>[]>();
this.loadedFactories[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);
}
this.loadedFactories[url].next(componentFactories);
this.loadedFactories[url].complete();
} catch (e) {
this.loadedFactories[url].error(new Error(`Unable to init module from url: ${url}`));
delete this.loadedFactories[url];
}
},
(e) => {
this.loadedFactories[url].error(new Error(`Unable to compile module from url: ${url}`));
delete this.loadedFactories[url];
2021-12-06 12:54:48 +02:00
});
} else {
this.loadedFactories[url].error(new Error(`Module '${url}' doesn't have default export!`));
delete this.loadedFactories[url];
}
},
(e) => {
this.loadedFactories[url].error(new Error(`Unable to load module from url: ${url}`));
delete this.loadedFactories[url];
}
);
}
);
return subject.asObservable();
}
2021-12-06 12:54:48 +02:00
public loadModules(url: string, modulesMap: IModulesMap): Observable<Type<any>[]> {
if (this.loadedModules[url]) {
return this.loadedModules[url].asObservable();
}
2021-12-06 12:54:48 +02:00
modulesMap.init();
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-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}`));
delete this.loadedModules[url];
2021-12-06 12:54:48 +02:00
console.error(`Unable to load module from url: ${url}`, e);
}
2021-12-06 12:54:48 +02:00
);
2019-12-23 14:36:44 +02:00
}
);
return subject.asObservable();
}
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);
} 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]);
}
}
}
}
potentialModules = newPotentialModules;
currentScanDepth++;
2020-02-11 20:03:37 +02:00
}
2021-12-06 12:54:48 +02:00
} catch (e) {
console.log('Could not load NgModule', e);
2020-02-11 20:03:37 +02:00
}
return modules;
}
private isPrimitive(test) {
return test !== Object(test);
}
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';
el.async = true;
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();
}
}