monaco editor wip

This commit is contained in:
Dan Percic 2021-05-20 17:06:42 +03:00
parent c28aac77f3
commit 1db0eb5737
13 changed files with 191 additions and 206 deletions

View File

@ -1,5 +1,5 @@
import { BrowserModule } from '@angular/platform-browser';
import { APP_INITIALIZER, Inject, NgModule } from '@angular/core';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { ActivatedRoute, Router } from '@angular/router';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@ -20,7 +20,6 @@ import { AuthErrorComponent } from '@components/auth-error/auth-error.component'
import { ToastComponent } from '@components/toast/toast.component';
import { HttpCacheInterceptor } from '@redaction/red-cache';
import { NotificationsComponent } from '@components/notifications/notifications.component';
import { KeycloakService } from 'keycloak-angular';
import { DownloadsListScreenComponent } from '@components/downloads-list-screen/downloads-list-screen.component';
import { AppRoutingModule } from './app-routing.module';
import { SharedModule } from '@shared/shared.module';
@ -28,8 +27,9 @@ import { FileUploadDownloadModule } from '@upload-download/file-upload-download.
import { UserProfileScreenComponent } from '@components/user-profile/user-profile-screen.component';
import { PlatformLocation } from '@angular/common';
import { BASE_HREF } from './tokens';
import { MONACO_PATH, MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
declare let ace;
// declare let ace;
export function httpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
@ -67,6 +67,7 @@ const components = [
AuthModule,
ApiModule,
AppRoutingModule,
MonacoEditorModule,
ToastrModule.forRoot({
closeButton: true,
enableHtml: true,
@ -102,20 +103,23 @@ const components = [
multi: true,
useFactory: languageInitializer,
deps: [LanguageService]
},
{
provide: MONACO_PATH,
useValue: 'https://unpkg.com/monaco-editor@0.24.0/min/vs'
}
],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(
@Inject(BASE_HREF) private readonly _baseHref: string,
// @Inject(BASE_HREF) private readonly _baseHref: string,
private readonly _router: Router,
private readonly _route: ActivatedRoute,
private readonly _keyCloakService: KeycloakService
private readonly _route: ActivatedRoute
) {
this._configureKeyCloakRouteHandling();
ace.config.set('basePath', _baseHref + '/assets/ace-builds/');
// ace.config.set('basePath', _baseHref + '/assets/ace-builds/');
}
private _configureKeyCloakRouteHandling() {

View File

@ -63,7 +63,7 @@
<redaction-admin-side-nav type="project-templates"></redaction-admin-side-nav>
<redaction-dictionary-manager
[initialDictionaryEntries]="entries"
[initialEntries]="entries"
[canEdit]="permissionsService.isAdmin()"
(saveDictionary)="saveEntries($event)"
#dictionaryManager
@ -79,7 +79,7 @@
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:entries"></mat-icon>
{{ dictionaryManager.initialDictionaryEntries?.length }}
{{ dictionaryManager.initialEntries?.length }}
</div>
</div>
<div class="small-label stats-subtitle">

View File

@ -3,11 +3,9 @@ import { DictionaryControllerService, TypeValue } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { PermissionsService } from '@services/permissions.service';
import { ActivatedRoute, Router } from '@angular/router';
import { NotificationService } from '@services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { saveAs } from 'file-saver';
import { ComponentHasChanges } from '@guards/can-deactivate.guard';
import { FormBuilder } from '@angular/forms';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { DictionaryManagerComponent } from '../../../shared/components/dictionary-manager/dictionary-manager.component';
import { DictionarySaveService } from '../../../shared/services/dictionary-save.service';
@ -22,20 +20,18 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
entries: string[] = [];
@ViewChild('dictionaryManager', { static: false })
private _dictionaryManager: DictionaryManagerComponent;
@ViewChild('fileInput') private _fileInput: ElementRef;
private readonly _dictionaryManager: DictionaryManagerComponent;
@ViewChild('fileInput') private readonly _fileInput: ElementRef;
constructor(
readonly permissionsService: PermissionsService,
private readonly _notificationService: NotificationService,
protected readonly _translateService: TranslateService,
private readonly _dictionarySaveService: DictionarySaveService,
private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _dialogService: AdminDialogService,
private readonly _router: Router,
private readonly _activatedRoute: ActivatedRoute,
private readonly _appStateService: AppStateService,
private readonly _formBuilder: FormBuilder
private readonly _appStateService: AppStateService
) {
super(_translateService);
this._appStateService.activateDictionary(

View File

@ -7,7 +7,7 @@
#dictionaryManager
[withFloatingActions]="false"
[canEdit]="canEdit"
[initialDictionaryEntries]="project.type?.entries"
[initialEntries]="project.type?.entries"
></redaction-dictionary-manager>
</div>

View File

@ -31,8 +31,8 @@ export class DossierDictionaryDialogComponent {
saveDossierDictionary() {
this._dictionarySaveService
.saveEntries(
this._dictionaryManager.currentDictionaryEntries,
this._dictionaryManager.initialDictionaryEntries,
this._dictionaryManager.currentEntries,
this._dictionaryManager.initialEntries,
this.project.ruleSetId,
'dossier_redaction',
this.project.projectId

View File

@ -1,5 +1,5 @@
<redaction-dictionary-manager
[canEdit]="canEdit"
[initialDictionaryEntries]="projectWrapper.type?.entries || []"
[initialEntries]="projectWrapper.type?.entries || []"
[withFloatingActions]="false"
></redaction-dictionary-manager>

View File

@ -39,8 +39,8 @@ export class EditProjectDictionaryComponent implements EditProjectSectionInterfa
save() {
this._dictionarySaveService
.saveEntries(
this._dictionaryManager.currentDictionaryEntries,
this._dictionaryManager.initialDictionaryEntries,
this._dictionaryManager.currentEntries,
this._dictionaryManager.initialEntries,
this.projectWrapper.ruleSetId,
'dossier_redaction',
this.projectWrapper.projectId,

View File

@ -36,7 +36,7 @@
</div>
</div>
</div>
<form [formGroup]="compareForm">
<form [formGroup]="form">
<div class="red-input-group mr-16">
<mat-checkbox color="primary" formControlName="active">
{{ 'dictionary-overview.compare.compare' | translate }}
@ -64,45 +64,52 @@
</div>
<div class="editor-container">
<ace-editor
#editorComponent
(textChanged)="textChanged($event)"
[autoUpdateContent]="true"
[mode]="'text'"
[options]="aceOptions"
[readOnly]="!canEdit"
[theme]="'eclipse'"
class="ace-redaction"
>
</ace-editor>
<!-- <ace-editor-->
<!-- #editorComponent-->
<!-- (textChanged)="textChanged($event)"-->
<!-- [autoUpdateContent]="true"-->
<!-- [mode]="'text'"-->
<!-- [options]="aceOptions"-->
<!-- [readOnly]="!canEdit"-->
<!-- [theme]="'eclipse'"-->
<!-- class="ace-redaction"-->
<!-- >-->
<!-- </ace-editor>-->
<ngx-monaco-editor
*ngIf="!showDiffEditor"
[options]="editorOptions"
[(ngModel)]="codeEditorText"
(init)="onCodeEditorInit($event)"
></ngx-monaco-editor>
<ngx-monaco-diff-editor
*ngIf="showDiffEditor"
[options]="editorOptions"
[original]="diffEditorText"
[modified]="codeEditorText"
(init)="onDiffEditorInit($event)"
></ngx-monaco-diff-editor>
<div
*ngIf="
compareForm.get('active').value &&
compareForm.get('dictionary').value === selectDictionary
"
*ngIf="form.get('active').value && form.get('dictionary').value === selectDictionary"
class="no-dictionary-selected"
>
<mat-icon svgIcon="red:dictionary"></mat-icon>
<span class="heading-l" translate="dictionary-overview.select-dictionary"></span>
</div>
<ace-editor
#compareEditorComponent
*ngIf="
compareForm.get('active').value &&
compareForm.get('dictionary').value !== selectDictionary
"
[mode]="'text'"
[options]="aceOptions"
[readOnly]="true"
[theme]="'eclipse'"
class="ace-redaction"
>
</ace-editor>
<!-- <ace-editor-->
<!-- #compareEditorComponent-->
<!-- *ngIf=""-->
<!-- [mode]="'text'"-->
<!-- [options]="aceOptions"-->
<!-- [readOnly]="true"-->
<!-- [theme]="'eclipse'"-->
<!-- class="ace-redaction"-->
<!-- >-->
<!-- </ace-editor>-->
</div>
<div
*ngIf="withFloatingActions && hasChanges && canEdit"
[class.offset]="compareForm.get('active').value"
[class.offset]="form.get('active').value"
class="changes-box"
>
<redaction-icon-button

View File

@ -37,6 +37,12 @@ form {
}
}
ngx-monaco-diff-editor,
ngx-monaco-editor {
height: 100%;
width: 100%;
}
.content-container {
height: 100%;

View File

@ -1,184 +1,177 @@
import {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
ViewChild
Output
} from '@angular/core';
import { DictionaryControllerService, RuleSetModel, TypeValue } from '@redaction/red-ui-http';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AceEditorComponent } from 'ng2-ace-editor';
import { NotificationService } from '@services/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AppStateService } from '@state/app-state.service';
import { debounce } from '@utils/debounce';
import ICodeEditor = monaco.editor.ICodeEditor;
import IDiffEditor = monaco.editor.IDiffEditor;
import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
declare let ace;
const MIN_WORD_LENGTH = 2;
interface ISearchPosition {
row: number;
column: number;
length: number;
}
@Component({
selector: 'redaction-dictionary-manager',
templateUrl: './dictionary-manager.component.html',
styleUrls: ['./dictionary-manager.component.scss']
})
export class DictionaryManagerComponent implements OnInit, OnChanges {
export class DictionaryManagerComponent implements OnChanges {
@Input()
withFloatingActions = true;
@Input()
initialDictionaryEntries: string[];
initialEntries: string[];
@Input()
canEdit = false;
@Output()
saveDictionary = new EventEmitter<string[]>();
activeEditMarkers: any[] = [];
activeSearchMarkers: any[] = [];
searchPositions: any[] = [];
editDecorations: IModelDeltaDecoration[] = [];
searchDecorations: IModelDeltaDecoration[] = [];
searchPositions: ISearchPosition[] = [];
currentMatch = 1;
currentDictionaryEntries: string[] = [];
compareDictionaryEntries: string[] = [];
currentEntries: string[] = [];
changedLines: number[] = [];
aceOptions = { showPrintMargin: false };
editorOptions = { theme: 'vs-light', language: 'text/plain', automaticLayout: true };
diffEditorText = '';
searchText = '';
selectRuleSet = { name: 'dictionary-overview.compare.select-ruleset' };
selectDictionary = { label: 'dictionary-overview.compare.select-dictionary' };
ruleSets: RuleSetModel[];
dictionaries: TypeValue[] = [this.selectDictionary];
compareForm: FormGroup;
form: FormGroup;
@ViewChild('editorComponent', { static: true }) private _editorComponent: AceEditorComponent;
@ViewChild('compareEditorComponent') private _compareEditorComponent: AceEditorComponent;
private _codeEditor: ICodeEditor;
private _diffEditor: IDiffEditor;
constructor(
private readonly _notificationService: NotificationService,
protected readonly _translateService: TranslateService,
private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _router: Router,
private readonly _activatedRoute: ActivatedRoute,
private readonly _appStateService: AppStateService,
private readonly _formBuilder: FormBuilder
private readonly _formBuilder: FormBuilder,
private readonly _changeDetector: ChangeDetectorRef
) {
this.compareForm = this._formBuilder.group({
this.form = this._formBuilder.group({
active: [false],
ruleSet: [{ value: this.selectRuleSet, disabled: true }],
dictionary: [{ value: this.selectDictionary, disabled: true }]
});
this.compareForm.valueChanges.subscribe((value) => {
this.form.valueChanges.subscribe((value) => {
this._setFieldStatus('ruleSet', value.active);
this._setFieldStatus(
'dictionary',
value.active && this.compareForm.get('ruleSet').value !== this.selectRuleSet
value.active && this.form.get('ruleSet').value !== this.selectRuleSet
);
this._loadDictionaries();
});
this.ruleSets = [this.selectRuleSet, ...this._appStateService.ruleSets];
this.compareForm.controls.ruleSet.valueChanges.subscribe(() => {
this.form.controls.ruleSet.valueChanges.subscribe(() => {
this._onRuleSetChanged();
});
this.compareForm.controls.dictionary.valueChanges.subscribe((dictionary) => {
this.form.controls.dictionary.valueChanges.subscribe((dictionary) => {
this._onDictionaryChanged(dictionary);
});
}
private static _setEditorValue(editor: AceEditorComponent, entries: string[]) {
const dictionaryEntriesAsText = entries.join('\n');
editor.getEditor().setValue(dictionaryEntriesAsText);
editor.getEditor().gotoLine(1);
onDiffEditorInit(editor: IDiffEditor): void {
this._diffEditor = editor;
}
onCodeEditorInit(editor: ICodeEditor): void {
this._codeEditor = editor;
}
get showDiffEditor(): boolean {
return (
this.form.get('active').value &&
this.form.get('dictionary').value !== this.selectDictionary
);
}
get editorValue() {
return this._editorComponent.getEditor().getValue();
return this._codeEditor.getModel().getValue();
}
set editorValue(value: any) {
this._editorComponent.getEditor().setValue(value);
}
ngOnInit(): void {
this._editorComponent.getEditor().selection.on('changeCursor', () => {
this._syncActiveLines();
});
this._codeEditor.getModel().setValue(value);
}
revert() {
DictionaryManagerComponent._setEditorValue(
this._editorComponent,
this.initialDictionaryEntries
);
this.currentEntries = this.initialEntries;
this.searchChanged('');
}
@debounce()
searchChanged(text: string) {
this.searchText = text.toLowerCase();
this._applySearchMarkers();
this._applySearchDecorations();
this.currentMatch = 0;
this.nextSearchMatch();
}
@debounce(500)
textChanged($event: any) {
this._applySearchMarkers();
this.currentDictionaryEntries = $event.split('\n');
get codeEditorText(): string {
return this.currentEntries.join('\n');
}
set codeEditorText(text: string) {
this._applySearchDecorations();
this.currentEntries = text.split('\n');
this.changedLines = [];
this.activeEditMarkers.forEach((am) => {
this._editorComponent.getEditor().getSession().removeMarker(am);
this.editDecorations = [];
this._codeEditor.deltaDecorations([], this.editDecorations);
this.currentEntries.forEach((entry, index) => {
if (this.initialEntries.indexOf(entry) < 0) {
this.changedLines.push(index);
}
});
this.activeEditMarkers = [];
for (let i = 0; i < this.currentDictionaryEntries.length; i++) {
const currentEntry = this.currentDictionaryEntries[i];
if (this.initialDictionaryEntries.indexOf(currentEntry) < 0) {
this.changedLines.push(i);
}
}
this.changedLines.forEach((line) => this._makeDecorations(line));
this._codeEditor.deltaDecorations([], this.editDecorations);
}
const range = ace.require('ace/range').Range;
for (const i of this.changedLines) {
const entry = this.currentDictionaryEntries[i];
if (entry?.trim().length > 0) {
// only mark non-empty lines
this.activeEditMarkers.push(
this._editorComponent
.getEditor()
.getSession()
.addMarker(new range(i, 0, i, 1), 'changed-row-marker', 'fullLine')
);
}
if (entry?.trim().length > 0 && entry.trim().length < MIN_WORD_LENGTH) {
// show lines that are too short
this.activeEditMarkers.push(
this._editorComponent
.getEditor()
.getSession()
.addMarker(new range(i, 0, i, 1), 'too-short-marker', 'fullLine')
);
}
private _makeDecorations(line: number) {
const entry = this.currentEntries[line];
if (entry.trim().length === 0) return;
const wholeLine = new monaco.Range(line, 0, line, 1);
const decoration: IModelDeltaDecoration = {
range: wholeLine,
options: { isWholeLine: true, inlineClassName: 'changed-row-marker' }
};
this.editDecorations.push(decoration);
if (entry.trim().length < MIN_WORD_LENGTH) {
this.editDecorations.push({
range: wholeLine,
options: { isWholeLine: true, inlineClassName: 'too-short-marker' }
});
}
}
get hasChanges() {
return (
this.currentDictionaryEntries.length &&
(this.activeEditMarkers.length > 0 ||
this.currentDictionaryEntries.filter((e) => e && e.trim().length > 0).length <
this.initialDictionaryEntries.length)
this.currentEntries.length &&
(this.editDecorations.length > 0 ||
this.currentEntries.filter((e) => e.trim().length > 0).length <
this.initialEntries.length)
);
}
nextSearchMatch() {
// length = 3
if (this.searchPositions.length > 0) {
this.currentMatch =
this.currentMatch < this.searchPositions.length ? this.currentMatch + 1 : 1;
@ -195,11 +188,11 @@ export class DictionaryManagerComponent implements OnInit, OnChanges {
}
private _setFieldStatus(field: 'ruleSet' | 'dictionary', enabled: boolean) {
this.compareForm.get(field)[enabled ? 'enable' : 'disable']({ emitEvent: false });
this.form.get(field)[enabled ? 'enable' : 'disable']({ emitEvent: false });
}
private _loadDictionaries() {
const ruleSetId = this.compareForm.get('ruleSet').value.ruleSetId;
const ruleSetId = this.form.get('ruleSet').value.ruleSetId;
if (!ruleSetId) {
this.dictionaries = [this.selectDictionary];
return;
@ -207,46 +200,29 @@ export class DictionaryManagerComponent implements OnInit, OnChanges {
const appStateDictionaryData = this._appStateService.dictionaryData[ruleSetId];
this.dictionaries = [
this.selectDictionary,
...Object.keys(appStateDictionaryData)
.map((key) => appStateDictionaryData[key])
.filter((d) => !d.virtual || d.type === 'false_positive')
...Object.values(appStateDictionaryData).filter(
(d) => !d.virtual || d.type === 'false_positive'
)
];
}
private _applySearchMarkers() {
private _applySearchDecorations() {
this.searchPositions = this._getSearchPositions();
this.activeSearchMarkers.forEach((am) => {
this._editorComponent.getEditor().getSession().removeMarker(am);
});
this.activeSearchMarkers = [];
this.searchDecorations = this.searchPositions.map(({ row, column, length }) => ({
range: new monaco.Range(row, column, row, column + length),
options: { inlineClassName: 'search-marker' }
}));
const range = ace.require('ace/range').Range;
for (const position of this.searchPositions) {
this.activeSearchMarkers.push(
this._editorComponent
.getEditor()
.getSession()
.addMarker(
new range(
position.row,
position.column,
position.row,
position.column + position.length
),
'search-marker',
'text'
)
);
}
this._codeEditor.deltaDecorations([], this.searchDecorations);
}
private _getSearchPositions() {
const lowerCaseSearchText = this.searchText.toLowerCase();
return this.currentDictionaryEntries
private _getSearchPositions(): ISearchPosition[] {
const searchText = this.searchText.toLowerCase();
return this.currentEntries
.map((val, index) => {
const columnIndex = val.toLowerCase().indexOf(lowerCaseSearchText);
const columnIndex = val.toLowerCase().indexOf(searchText);
if (columnIndex >= 0) {
return { row: index, column: columnIndex, length: lowerCaseSearchText.length };
return { row: index, column: columnIndex, length: searchText.length };
}
})
.filter((entry) => !!entry);
@ -254,47 +230,34 @@ export class DictionaryManagerComponent implements OnInit, OnChanges {
private _gotoLine() {
const position = this.searchPositions[this.currentMatch - 1];
this._editorComponent.getEditor().scrollToLine(position.row, true, true, () => {});
this._editorComponent.getEditor().gotoLine(position.row + 1, position.column, true);
this._codeEditor.setScrollPosition({ scrollTop: position.row });
// this._editorComponent.getEditor().scrollToLine(position.row, true, true, () => {});
// this._editorComponent.getEditor().gotoLine(position.row + 1, position.column, true);
}
private _onRuleSetChanged() {
this._loadDictionaries();
this.compareForm.patchValue({ dictionary: this.selectDictionary });
this.form.patchValue({ dictionary: this.selectDictionary });
}
private _onDictionaryChanged(dictionary: TypeValue) {
if (dictionary !== this.selectDictionary) {
this._dictionaryControllerService
.getDictionaryForType(dictionary.ruleSetId, dictionary.type)
.subscribe(
(data) => {
this.compareDictionaryEntries = data.entries.sort((str1, str2) =>
str1.localeCompare(str2, undefined, { sensitivity: 'accent' })
);
DictionaryManagerComponent._setEditorValue(
this._compareEditorComponent,
this.compareDictionaryEntries
);
this._syncActiveLines();
},
() => {}
);
if (dictionary === this.selectDictionary) {
return;
}
}
private _syncActiveLines() {
if (this._compareEditorComponent) {
this._compareEditorComponent.getEditor().gotoLine(this._activeRow);
}
}
private get _activeRow(): number {
return this._editorComponent.getEditor().selection.getCursor().row + 1;
this._dictionaryControllerService
.getDictionaryForType(dictionary.ruleSetId, dictionary.type)
.subscribe((data) => {
this.diffEditorText = data.entries
.sort((str1, str2) =>
str1.localeCompare(str2, undefined, { sensitivity: 'accent' })
)
.join('\n');
this._changeDetector.detectChanges();
});
}
saveEntries() {
this.saveDictionary.emit(this.currentDictionaryEntries);
this.saveDictionary.emit(this.currentEntries);
}
ngOnChanges(): void {

View File

@ -35,6 +35,7 @@ import { NavigateLastProjectsScreenDirective } from './directives/navigate-last-
import { DictionaryManagerComponent } from './components/dictionary-manager/dictionary-manager.component';
import { AceEditorModule } from 'ng2-ace-editor';
import { SideNavComponent } from '@shared/components/side-nav/side-nav.component';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
const buttons = [
ChevronButtonComponent,
@ -84,7 +85,7 @@ const modules = [
@NgModule({
declarations: [...components, ...utils, DictionaryManagerComponent],
imports: [CommonModule, ...modules, AceEditorModule],
imports: [CommonModule, ...modules, AceEditorModule, MonacoEditorModule],
exports: [...modules, ...components, ...utils, DictionaryManagerComponent],
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },

View File

@ -47,6 +47,7 @@
"@angular/platform-browser-dynamic": "12.0.0",
"@angular/router": "12.0.0",
"@angular/service-worker": "12.0.0",
"@materia-ui/ngx-monaco-editor": "^5.1.0",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@nrwl/angular": "12.3.3",
@ -108,7 +109,7 @@
"superagent-promise": "^1.1.0",
"ts-jest": "26.5.6",
"ts-node": "9.1.1",
"webpack": "^4.18.1",
"typescript": "4.2.4"
"typescript": "4.2.4",
"webpack": "^4.18.1"
}
}

View File

@ -2379,6 +2379,13 @@
merge-source-map "^1.1.0"
schema-utils "^2.7.0"
"@materia-ui/ngx-monaco-editor@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@materia-ui/ngx-monaco-editor/-/ngx-monaco-editor-5.1.0.tgz#76c2f7ec3654f4ad30fa4a5b8439dcf0fa0f47f4"
integrity sha512-5k4yJzh1rbygbgwomcTOA63NABr/pYMZZNmtwN/2/eo07ZNxiJY3puNKjJkNN2cuWeZA5Qu7LKDS0E8oYpN/cg==
dependencies:
tslib "^1.10.0"
"@ngtools/webpack@12.0.0":
version "12.0.0"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-12.0.0.tgz#b2f6cc8f727cc9fdf54faac27ce1b4865c471b1c"