Compare commits
10 Commits
6.1.10
...
5.0.0-rc.6
Author | SHA1 | Date | |
---|---|---|---|
47bc6f105d | |||
40fa2593a9 | |||
680bcf7b8a | |||
ef08330341 | |||
6cc042e2ba | |||
9b26455740 | |||
18bce5987c | |||
f1108fea76 | |||
64b3e3e41a | |||
a82f863e24 |
23
CHANGELOG.md
23
CHANGELOG.md
@ -1,3 +1,26 @@
|
||||
<a name="5.0.0-rc.6"></a>
|
||||
# [5.0.0-rc.6](https://github.com/angular/angular/compare/5.0.0-rc.5...5.0.0-rc.6) (2017-10-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler:** automatically set `emitDecoratorMetadata` when `"annotationsAs": "static fields”` ([#19927](https://github.com/angular/angular/issues/19927)) ([ef08330](https://github.com/angular/angular/commit/ef08330)), closes [#19916](https://github.com/angular/angular/issues/19916)
|
||||
* **compiler:** don’t type check templates with `skipTemplateCodegen` ([#19909](https://github.com/angular/angular/issues/19909)) ([18bce59](https://github.com/angular/angular/commit/18bce59))
|
||||
* **compiler-cli:** only use error collector when needed. ([#19912](https://github.com/angular/angular/issues/19912)) ([9b26455](https://github.com/angular/angular/commit/9b26455))
|
||||
* **compiler-cli:** produce correct paths for windows output ([#19915](https://github.com/angular/angular/issues/19915)) ([6cc042e](https://github.com/angular/angular/commit/6cc042e))
|
||||
|
||||
|
||||
|
||||
<a name="5.0.0-rc.5"></a>
|
||||
# [5.0.0-rc.5](https://github.com/angular/angular/compare/5.0.0-rc.4...5.0.0-rc.5) (2017-10-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **compiler-cli:** report all diagnostic error messages ([#19886](https://github.com/angular/angular/issues/19886)) ([a82f863](https://github.com/angular/angular/commit/a82f863))
|
||||
|
||||
|
||||
|
||||
<a name="5.0.0-rc.4"></a>
|
||||
# [5.0.0-rc.4](https://github.com/angular/angular/compare/5.0.0-rc.3...5.0.0-rc.4) (2017-10-24)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "angular-srcs",
|
||||
"version": "5.0.0-rc.4",
|
||||
"version": "5.0.0-rc.6",
|
||||
"private": true,
|
||||
"branchPattern": "2.0.*",
|
||||
"description": "Angular - a web framework for modern web apps",
|
||||
@ -24,7 +24,7 @@
|
||||
"dependencies": {
|
||||
"core-js": "^2.4.1",
|
||||
"reflect-metadata": "^0.1.3",
|
||||
"rxjs": "^5.5.0",
|
||||
"rxjs": "^5.5.2",
|
||||
"tslib": "^1.7.1",
|
||||
"zone.js": "^0.8.12"
|
||||
},
|
||||
|
@ -45,6 +45,12 @@ function createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|un
|
||||
if (!transformDecorators && !transformTypesToClosure) {
|
||||
return undefined;
|
||||
}
|
||||
if (transformDecorators) {
|
||||
// This is needed as a workaround for https://github.com/angular/tsickle/issues/635
|
||||
// Otherwise tsickle might emit references to non imported values
|
||||
// as TypeScript elided the import.
|
||||
options.emitDecoratorMetadata = true;
|
||||
}
|
||||
const tsickleHost: tsickle.TsickleHost = {
|
||||
shouldSkipTsickleProcessing: (fileName) =>
|
||||
/\.d\.ts$/.test(fileName) || GENERATED_FILES.test(fileName),
|
||||
|
@ -89,7 +89,7 @@ export class NgTools_InternalApi_NG_2 {
|
||||
// as we only needed this to support Angular CLI 1.5.0 rc.*
|
||||
const ngProgram = createProgram({
|
||||
rootNames: options.program.getRootFileNames(),
|
||||
options: options.angularCompilerOptions,
|
||||
options: {...options.angularCompilerOptions, collectAllErrors: true},
|
||||
host: options.host
|
||||
});
|
||||
const lazyRoutes = ngProgram.listLazyRoutes(options.entryModule);
|
||||
|
@ -150,6 +150,9 @@ export interface CompilerOptions extends ts.CompilerOptions {
|
||||
* in JIT mode. This is off by default.
|
||||
*/
|
||||
enableSummariesForJit?: boolean;
|
||||
|
||||
/** @internal */
|
||||
collectAllErrors?: boolean;
|
||||
}
|
||||
|
||||
export interface CompilerHost extends ts.CompilerHost {
|
||||
|
@ -426,6 +426,12 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter implements ts.CompilerHos
|
||||
}
|
||||
|
||||
isSourceFile(filePath: string): boolean {
|
||||
// Don't generate any files nor typecheck them
|
||||
// if skipTemplateCodegen is set and fullTemplateTypeCheck is not yet set,
|
||||
// for backwards compatibility.
|
||||
if (this.options.skipTemplateCodegen && !this.options.fullTemplateTypeCheck) {
|
||||
return false;
|
||||
}
|
||||
// If we have a summary from a previous compilation,
|
||||
// treat the file never as a source file.
|
||||
if (this.librarySummaries.has(filePath)) {
|
||||
|
@ -181,11 +181,13 @@ function createVariableStatementForDeclarations(declarations: Declaration[]): ts
|
||||
/* modifiers */ undefined, ts.createVariableDeclarationList(varDecls, ts.NodeFlags.Const));
|
||||
}
|
||||
|
||||
export function getExpressionLoweringTransformFactory(requestsMap: RequestsMap):
|
||||
(context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile {
|
||||
export function getExpressionLoweringTransformFactory(
|
||||
requestsMap: RequestsMap, program: ts.Program): (context: ts.TransformationContext) =>
|
||||
(sourceFile: ts.SourceFile) => ts.SourceFile {
|
||||
// Return the factory
|
||||
return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile): ts.SourceFile => {
|
||||
const requests = requestsMap.getRequests(sourceFile);
|
||||
// We need to use the original SourceFile for reading metadata, and not the transformed one.
|
||||
const requests = requestsMap.getRequests(program.getSourceFile(sourceFile.fileName));
|
||||
if (requests && requests.size) {
|
||||
return transformSourceFile(sourceFile, requests, context);
|
||||
}
|
||||
|
@ -386,7 +386,7 @@ class AngularCompilerProgram implements Program {
|
||||
customTransformers?: CustomTransformers): ts.CustomTransformers {
|
||||
const beforeTs: ts.TransformerFactory<ts.SourceFile>[] = [];
|
||||
if (!this.options.disableExpressionLowering) {
|
||||
beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache));
|
||||
beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache, this.tsProgram));
|
||||
}
|
||||
beforeTs.push(getAngularEmitterTransformFactory(genFiles));
|
||||
if (customTransformers && customTransformers.beforeTs) {
|
||||
@ -422,14 +422,15 @@ class AngularCompilerProgram implements Program {
|
||||
this.oldProgramLibrarySummaries);
|
||||
const aotOptions = getAotCompilerOptions(this.options);
|
||||
this._structuralDiagnostics = [];
|
||||
const errorCollector = (err: any) => {
|
||||
this._structuralDiagnostics !.push({
|
||||
messageText: err.toString(),
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
source: SOURCE,
|
||||
code: DEFAULT_ERROR_CODE
|
||||
});
|
||||
};
|
||||
const errorCollector =
|
||||
(this.options.collectAllErrors || this.options.fullTemplateTypeCheck) ? (err: any) => {
|
||||
this._structuralDiagnostics !.push({
|
||||
messageText: err.toString(),
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
source: SOURCE,
|
||||
code: DEFAULT_ERROR_CODE
|
||||
});
|
||||
} : undefined;
|
||||
this._compiler = createAotCompiler(this._hostAdapter, aotOptions, errorCollector).compiler;
|
||||
}
|
||||
|
||||
@ -510,21 +511,25 @@ class AngularCompilerProgram implements Program {
|
||||
if (isSyntaxError(e)) {
|
||||
const parserErrors = getParseErrors(e);
|
||||
if (parserErrors && parserErrors.length) {
|
||||
this._structuralDiagnostics =
|
||||
parserErrors.map<Diagnostic>(e => ({
|
||||
messageText: e.contextualMessage(),
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
span: e.span,
|
||||
source: SOURCE,
|
||||
code: DEFAULT_ERROR_CODE
|
||||
}));
|
||||
this._structuralDiagnostics = [
|
||||
...(this._structuralDiagnostics || []),
|
||||
...parserErrors.map<Diagnostic>(e => ({
|
||||
messageText: e.contextualMessage(),
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
span: e.span,
|
||||
source: SOURCE,
|
||||
code: DEFAULT_ERROR_CODE
|
||||
}))
|
||||
];
|
||||
} else {
|
||||
this._structuralDiagnostics = [{
|
||||
messageText: e.message,
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
source: SOURCE,
|
||||
code: DEFAULT_ERROR_CODE
|
||||
}];
|
||||
this._structuralDiagnostics = [
|
||||
...(this._structuralDiagnostics || []), {
|
||||
messageText: e.message,
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
source: SOURCE,
|
||||
code: DEFAULT_ERROR_CODE
|
||||
}
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -702,6 +707,10 @@ function getNgOptionDiagnostics(options: CompilerOptions): Diagnostic[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeSeparators(path: string): string {
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that can adjust a path from source path to out path,
|
||||
* based on an existing mapping from source to out path.
|
||||
@ -723,18 +732,19 @@ export function createSrcToOutPathMapper(
|
||||
} = path): (srcFileName: string) => string {
|
||||
let srcToOutPath: (srcFileName: string) => string;
|
||||
if (outDir) {
|
||||
let path: {} = {}; // Ensure we error if we use `path` instead of `host`.
|
||||
if (sampleSrcFileName == null || sampleOutFileName == null) {
|
||||
throw new Error(`Can't calculate the rootDir without a sample srcFileName / outFileName. `);
|
||||
}
|
||||
const srcFileDir = host.dirname(sampleSrcFileName).replace(/\\/g, '/');
|
||||
const outFileDir = host.dirname(sampleOutFileName).replace(/\\/g, '/');
|
||||
const srcFileDir = normalizeSeparators(host.dirname(sampleSrcFileName));
|
||||
const outFileDir = normalizeSeparators(host.dirname(sampleOutFileName));
|
||||
if (srcFileDir === outFileDir) {
|
||||
return (srcFileName) => srcFileName;
|
||||
}
|
||||
// calculate the common suffix, stopping
|
||||
// at `outDir`.
|
||||
const srcDirParts = srcFileDir.split('/');
|
||||
const outDirParts = path.relative(outDir, outFileDir).split('/');
|
||||
const outDirParts = normalizeSeparators(host.relative(outDir, outFileDir)).split('/');
|
||||
let i = 0;
|
||||
while (i < Math.min(srcDirParts.length, outDirParts.length) &&
|
||||
srcDirParts[srcDirParts.length - 1 - i] === outDirParts[outDirParts.length - 1 - i])
|
||||
@ -750,7 +760,7 @@ export function createSrcToOutPathMapper(
|
||||
export function i18nExtract(
|
||||
formatName: string | null, outFile: string | null, host: ts.CompilerHost,
|
||||
options: CompilerOptions, bundle: MessageBundle): string[] {
|
||||
formatName = formatName || 'null';
|
||||
formatName = formatName || 'xlf';
|
||||
// Checks the format and returns the extension
|
||||
const ext = i18nGetExtension(formatName);
|
||||
const content = i18nSerialize(bundle, formatName, options);
|
||||
@ -784,7 +794,7 @@ export function i18nSerialize(
|
||||
}
|
||||
|
||||
export function i18nGetExtension(formatName: string): string {
|
||||
const format = (formatName || 'xlf').toLowerCase();
|
||||
const format = formatName.toLowerCase();
|
||||
|
||||
switch (format) {
|
||||
case 'xmb':
|
||||
|
@ -502,29 +502,69 @@ describe('ngc transformer command-line', () => {
|
||||
it('should add metadata as decorators', () => {
|
||||
writeConfig(`{
|
||||
"extends": "./tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"annotationsAs": "decorators"
|
||||
},
|
||||
"files": ["mymodule.ts"]
|
||||
}`);
|
||||
write('aclass.ts', `export class AClass {}`);
|
||||
write('mymodule.ts', `
|
||||
import {NgModule, Component} from '@angular/core';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {AClass} from './aclass';
|
||||
|
||||
@Component({template: ''})
|
||||
export class MyComp {
|
||||
fn(p: any) {}
|
||||
}
|
||||
|
||||
@NgModule({declarations: [MyComp]})
|
||||
export class MyModule {}
|
||||
`);
|
||||
@NgModule({declarations: []})
|
||||
export class MyModule {
|
||||
constructor(importedClass: AClass) {}
|
||||
}
|
||||
`);
|
||||
|
||||
const exitCode = main(['-p', basePath], errorSpy);
|
||||
expect(exitCode).toEqual(0);
|
||||
|
||||
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
||||
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
||||
expect(mymoduleSource).toContain('MyComp = __decorate([');
|
||||
expect(mymoduleSource).toContain('MyModule = __decorate([');
|
||||
expect(mymoduleSource).toContain(`import { AClass } from './aclass';`);
|
||||
expect(mymoduleSource).toContain(`__metadata("design:paramtypes", [AClass])`);
|
||||
});
|
||||
|
||||
it('should add metadata as static fields', () => {
|
||||
// Note: Don't specify emitDecoratorMetadata here on purpose,
|
||||
// as regression test for https://github.com/angular/angular/issues/19916.
|
||||
writeConfig(`{
|
||||
"extends": "./tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"emitDecoratorMetadata": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"annotationsAs": "static fields"
|
||||
},
|
||||
"files": ["mymodule.ts"]
|
||||
}`);
|
||||
write('aclass.ts', `export class AClass {}`);
|
||||
write('mymodule.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
import {AClass} from './aclass';
|
||||
|
||||
@NgModule({declarations: []})
|
||||
export class MyModule {
|
||||
constructor(importedClass: AClass) {}
|
||||
}
|
||||
`);
|
||||
|
||||
const exitCode = main(['-p', basePath], errorSpy);
|
||||
expect(exitCode).toEqual(0);
|
||||
|
||||
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
||||
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
||||
expect(mymoduleSource).not.toContain('__decorate');
|
||||
expect(mymoduleSource).toContain('args: [{ declarations: [] },] }');
|
||||
expect(mymoduleSource).not.toContain(`__metadata`);
|
||||
expect(mymoduleSource).toContain(`import { AClass } from './aclass';`);
|
||||
expect(mymoduleSource).toContain(`{ type: AClass, }`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1391,5 +1431,41 @@ describe('ngc transformer command-line', () => {
|
||||
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message));
|
||||
expect(exitCode).toBe(0, 'Compile failed unexpectedly.\n ' + messages.join('\n '));
|
||||
});
|
||||
|
||||
it('should emit all structural errors', () => {
|
||||
write('src/tsconfig.json', `{
|
||||
"extends": "../tsconfig-base.json",
|
||||
"files": ["test-module.ts"]
|
||||
}`);
|
||||
write('src/lib/indirect2.ts', `
|
||||
declare var f: any;
|
||||
export const t2 = f\`<p>hello</p>\`;
|
||||
`);
|
||||
write('src/lib/indirect1.ts', `
|
||||
import {t2} from './indirect2';
|
||||
export const t1 = t2 + ' ';
|
||||
`);
|
||||
write('src/lib/test.component.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
import {t1} from './indirect1';
|
||||
|
||||
@Component({
|
||||
template: t1
|
||||
})
|
||||
export class TestComponent {}
|
||||
`);
|
||||
write('src/test-module.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
import {TestComponent} from './lib/test.component';
|
||||
|
||||
@NgModule({declarations: [TestComponent]})
|
||||
export class TestModule {}
|
||||
`);
|
||||
const messages: string[] = [];
|
||||
const exitCode =
|
||||
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message));
|
||||
expect(exitCode).toBe(2, 'Compile was expected to fail');
|
||||
expect(messages[0]).toContain(['Tagged template expressions are not supported in metadata']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -181,13 +181,15 @@ function convert(annotatedSource: string) {
|
||||
[fileName], {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017}, host);
|
||||
const moduleSourceFile = program.getSourceFile(fileName);
|
||||
const transformers: ts.CustomTransformers = {
|
||||
before: [getExpressionLoweringTransformFactory({
|
||||
getRequests(sourceFile: ts.SourceFile): RequestLocationMap{
|
||||
if (sourceFile.fileName == moduleSourceFile.fileName) {
|
||||
return requests;
|
||||
} else {return new Map();}
|
||||
}
|
||||
})]
|
||||
before: [getExpressionLoweringTransformFactory(
|
||||
{
|
||||
getRequests(sourceFile: ts.SourceFile): RequestLocationMap{
|
||||
if (sourceFile.fileName == moduleSourceFile.fileName) {
|
||||
return requests;
|
||||
} else {return new Map();}
|
||||
}
|
||||
},
|
||||
program)]
|
||||
};
|
||||
let result: string = '';
|
||||
const emitResult = program.emit(
|
||||
|
@ -11,7 +11,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {CompilerHost, LazyRoute} from '../../src/transformers/api';
|
||||
import {CompilerHost, EmitFlags, LazyRoute} from '../../src/transformers/api';
|
||||
import {createSrcToOutPathMapper} from '../../src/transformers/program';
|
||||
import {GENERATED_FILES, StructureIsReused, tsStructureIsReused} from '../../src/transformers/util';
|
||||
import {TestSupport, expectNoDiagnosticsInProgram, setup} from '../test_support';
|
||||
@ -309,11 +309,35 @@ describe('ng program', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should typecheck templates even if skipTemplateCodegen is set', () => {
|
||||
it('should not typecheck templates if skipTemplateCodegen is set but fullTemplateTypeCheck is not',
|
||||
() => {
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
@NgModule(() => {if (1==1) return null as any;})
|
||||
export class SomeClassWithInvalidMetadata {}
|
||||
`,
|
||||
});
|
||||
const options = testSupport.createCompilerOptions({skipTemplateCodegen: true});
|
||||
const host = ng.createCompilerHost({options});
|
||||
const program = ng.createProgram(
|
||||
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
||||
expectNoDiagnosticsInProgram(options, program);
|
||||
const emitResult = program.emit({emitFlags: EmitFlags.All});
|
||||
expect(emitResult.diagnostics.length).toBe(0);
|
||||
|
||||
testSupport.shouldExist('built/src/main.metadata.json');
|
||||
});
|
||||
|
||||
it('should typecheck templates if skipTemplateCodegen and fullTemplateTypeCheck is set', () => {
|
||||
testSupport.writeFiles({
|
||||
'src/main.ts': createModuleAndCompSource('main', `{{nonExistent}}`),
|
||||
});
|
||||
const options = testSupport.createCompilerOptions({skipTemplateCodegen: true});
|
||||
const options = testSupport.createCompilerOptions({
|
||||
skipTemplateCodegen: true,
|
||||
fullTemplateTypeCheck: true,
|
||||
});
|
||||
const host = ng.createCompilerHost({options});
|
||||
const program = ng.createProgram(
|
||||
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
||||
@ -554,8 +578,8 @@ describe('ng program', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function createProgram(rootNames: string[]) {
|
||||
const options = testSupport.createCompilerOptions();
|
||||
function createProgram(rootNames: string[], overrideOptions: ng.CompilerOptions = {}) {
|
||||
const options = testSupport.createCompilerOptions(overrideOptions);
|
||||
const host = ng.createCompilerHost({options});
|
||||
const program = ng.createProgram(
|
||||
{rootNames: rootNames.map(p => path.resolve(testSupport.basePath, p)), options, host});
|
||||
@ -797,7 +821,7 @@ describe('ng program', () => {
|
||||
export class ChildModule {}
|
||||
`,
|
||||
});
|
||||
const program = createProgram(['src/main.ts']).program;
|
||||
const program = createProgram(['src/main.ts'], {collectAllErrors: true}).program;
|
||||
expect(normalizeRoutes(program.listLazyRoutes('src/main#MainModule'))).toEqual([{
|
||||
module: {name: 'MainModule', filePath: path.resolve(testSupport.basePath, 'src/main.ts')},
|
||||
referencedModule:
|
||||
|
@ -54,7 +54,7 @@ export function createAotUrlResolver(host: {
|
||||
*/
|
||||
export function createAotCompiler(
|
||||
compilerHost: AotCompilerHost, options: AotCompilerOptions,
|
||||
errorCollector: (error: any, type?: any) =>
|
||||
errorCollector?: (error: any, type?: any) =>
|
||||
void): {compiler: AotCompiler, reflector: StaticReflector} {
|
||||
let translations: string = options.translations || '';
|
||||
|
||||
|
@ -6194,9 +6194,9 @@ rx-lite@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
|
||||
|
||||
rxjs@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.0.tgz#26d8f3866eb700e247e0728a147c3d628993d812"
|
||||
rxjs@^5.5.2:
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.2.tgz#28d403f0071121967f18ad665563255d54236ac3"
|
||||
dependencies:
|
||||
symbol-observable "^1.0.1"
|
||||
|
||||
|
Reference in New Issue
Block a user