test(bazel): Build and test ts-api-guardian locally (#22544)
Also use it to test the public API for core and common Once we have an ng_package for every package, we can remove the npm dependency on ts-api-guardian and the gulp-based public api check. PR Close #22544
This commit is contained in:
parent
25faf808a5
commit
4f60968704
|
@ -2,8 +2,16 @@ package(default_visibility = ["//visibility:public"])
|
|||
|
||||
exports_files([
|
||||
"tsconfig.json",
|
||||
"LICENSE",
|
||||
])
|
||||
|
||||
# Developers should always run `bazel run :install`
|
||||
# This ensures that package.json in subdirectories get installed as well.
|
||||
alias(
|
||||
name = "install",
|
||||
actual = "@yarn//:yarn",
|
||||
)
|
||||
|
||||
# This rule belongs in node_modules/BUILD
|
||||
# It's here as a workaround for
|
||||
# https://github.com/bazelbuild/bazel/issues/374#issuecomment-296217940
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
workspace(name = "angular")
|
||||
|
||||
# Using a pre-release snapshot to pick up a commit that makes all nodejs_binary
|
||||
# programs produce source-mapped stack traces and uglify sourcemaps.
|
||||
RULES_NODEJS_VERSION = "f3fc23b7e1f32984a3e5d0c7eabe3baa127fb32a"
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_nodejs",
|
||||
url = "https://github.com/bazelbuild/rules_nodejs/archive/0.5.0.zip",
|
||||
|
@ -14,7 +10,10 @@ http_archive(
|
|||
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
|
||||
|
||||
check_bazel_version("0.9.0")
|
||||
node_repositories(package_json = ["//:package.json"])
|
||||
node_repositories(package_json = [
|
||||
"//:package.json",
|
||||
"//tools/ts-api-guardian:package.json",
|
||||
])
|
||||
|
||||
http_archive(
|
||||
name = "build_bazel_rules_typescript",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
exports_files(["tsconfig.json"])
|
||||
|
||||
# Executes the workspace_status_command and provides the result.
|
||||
# See the section on stamping in docs/BAZEL.md
|
||||
genrule(
|
||||
|
|
|
@ -26,6 +26,7 @@ const srcsToFmt = [
|
|||
`!${I18N_FOLDER}/currencies.ts`,
|
||||
`!${I18N_FOLDER}/locale_en.ts`,
|
||||
'!tools/gulp-tasks/cldr/extract.js',
|
||||
'!tools/ts-api-guardian/test/fixtures/**',
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
load("//tools/ts-api-guardian:index.bzl", "ts_api_guardian_test")
|
||||
|
||||
[
|
||||
ts_api_guardian_test(
|
||||
name = "%s_api" % i.replace("/", "_"),
|
||||
actual = "packages/%s/npm_package/%s.d.ts" % (
|
||||
i.split("/")[0],
|
||||
"/".join(i.split("/")[1:]),
|
||||
),
|
||||
data = glob([
|
||||
"%s/**/*.d.ts" % i.split("/")[0],
|
||||
]) + [
|
||||
"//packages/%s:npm_package" % i.split("/")[0],
|
||||
],
|
||||
golden = "tools/public_api_guard/%s.d.ts" % i,
|
||||
)
|
||||
for i in [
|
||||
"core/core",
|
||||
"core/testing",
|
||||
"common/http/testing",
|
||||
"common/common",
|
||||
"common/http",
|
||||
"common/testing",
|
||||
# TODO(alexeagle): add remaining packages here once they have ng_package's
|
||||
]
|
||||
]
|
|
@ -0,0 +1,84 @@
|
|||
load(
|
||||
"@build_bazel_rules_nodejs//:defs.bzl",
|
||||
"nodejs_binary",
|
||||
"jasmine_node_test",
|
||||
"npm_package",
|
||||
"node_modules_filegroup",
|
||||
)
|
||||
load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")
|
||||
|
||||
exports_files(["bin/ts-api-guardian"])
|
||||
|
||||
node_modules_filegroup(
|
||||
name = "compile_time_deps",
|
||||
packages = [
|
||||
"chalk",
|
||||
"typescript",
|
||||
"@types",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "lib",
|
||||
srcs = glob(["lib/*.ts"]),
|
||||
module_name = "ts-api-guardian",
|
||||
node_modules = ":compile_time_deps",
|
||||
tsconfig = "//tools:tsconfig.json",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
node_modules_filegroup(
|
||||
name = "runtime_deps",
|
||||
packages = [
|
||||
"chai",
|
||||
"chalk",
|
||||
"jasmine",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
# Copy Angular's license to govern ts-api-guardian as well.
|
||||
# We use a genrule to put it in this package, so it will be in the right root directory.
|
||||
genrule(
|
||||
name = "license",
|
||||
srcs = ["//:LICENSE"],
|
||||
outs = ["LICENSE"],
|
||||
cmd = "cp $< $@",
|
||||
)
|
||||
|
||||
npm_package(
|
||||
name = "ts-api-guardian",
|
||||
srcs = [
|
||||
"README.md",
|
||||
"bin/ts-api-guardian",
|
||||
"package.json",
|
||||
],
|
||||
deps = [
|
||||
":lib",
|
||||
":license",
|
||||
],
|
||||
)
|
||||
|
||||
#######################################3
|
||||
# Tests for this package
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(["test/*.ts"]),
|
||||
node_modules = ":compile_time_deps",
|
||||
deps = [":lib"],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "tests",
|
||||
srcs = [":test_lib"],
|
||||
bootstrap = ["angular/tools/ts-api-guardian/test/bootstrap.js"],
|
||||
data = glob([
|
||||
"test/fixtures/*.ts",
|
||||
"test/fixtures/*.patch",
|
||||
]) + [
|
||||
":ts-api-guardian",
|
||||
],
|
||||
node_modules = ":runtime_deps",
|
||||
)
|
|
@ -1,3 +1,20 @@
|
|||
# Typescript API Guardian
|
||||
|
||||
Keeps track of public API surface of a typescript library.
|
||||
|
||||
# For developers
|
||||
|
||||
Build and test this library:
|
||||
|
||||
```sh
|
||||
$ bazel run //:install
|
||||
$ bazel test //tools/ts-api-guardian:all
|
||||
```
|
||||
|
||||
Publish to NPM:
|
||||
|
||||
```sh
|
||||
$ npm whoami # should be logged in as angular
|
||||
$ grep version tools/ts-api-guardian/package.json # advance as needed
|
||||
$ bazel run //tools/ts-api-guardian:ts-api-guardian.publish
|
||||
```
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
require('../build/lib/cli').startCli();
|
||||
require('../lib/cli').startCli();
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
# Copyright 2017 The Bazel Authors. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Runs ts_api_guardian
|
||||
"""
|
||||
load("@build_bazel_rules_nodejs//internal/node:node.bzl", "nodejs_test", "nodejs_binary")
|
||||
|
||||
COMMON_MODULE_IDENTIFIERS = ["angular", "jasmine", "protractor"]
|
||||
|
||||
def ts_api_guardian_test(name, golden, actual, data = [], **kwargs):
|
||||
"""Runs ts_api_guardian
|
||||
"""
|
||||
data += [
|
||||
"//tools/ts-api-guardian:lib",
|
||||
"//tools/ts-api-guardian:bin/ts-api-guardian",
|
||||
]
|
||||
|
||||
args = [
|
||||
"--stripExportPattern", "^\(__\|ɵ\)",
|
||||
"--onStabilityMissing", "error",
|
||||
]
|
||||
for i in COMMON_MODULE_IDENTIFIERS:
|
||||
args += ["--allowModuleIdentifiers", i]
|
||||
|
||||
nodejs_test(
|
||||
name = name,
|
||||
data = data,
|
||||
node_modules = "//tools/ts-api-guardian:runtime_deps",
|
||||
entry_point = "angular/tools/ts-api-guardian/bin/ts-api-guardian",
|
||||
templated_args = args + ["--verify", golden, actual],
|
||||
testonly = 1,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
nodejs_binary(
|
||||
name = name + ".accept",
|
||||
data = data,
|
||||
node_modules = "//tools/ts-api-guardian:runtime_deps",
|
||||
entry_point = "angular/tools/ts-api-guardian/bin/ts-api-guardian",
|
||||
templated_args = args + ["--out", golden, actual],
|
||||
**kwargs
|
||||
)
|
|
@ -1,6 +1,17 @@
|
|||
import chalk from 'chalk';
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
// TODO(alexeagle): why not import chalk from 'chalk'?
|
||||
// Something to do with TS default export in UMD emit...
|
||||
const chalk = require('chalk');
|
||||
import * as minimist from 'minimist';
|
||||
import {ParsedArgs} from 'minimist';
|
||||
import * as path from 'path';
|
||||
|
||||
import {SerializationOptions, generateGoldenFile, verifyAgainstGoldenFile} from './main';
|
||||
|
@ -35,7 +46,7 @@ export function startCli() {
|
|||
onStabilityMissing: argv['onStabilityMissing'] || 'none'
|
||||
};
|
||||
|
||||
if (['warn', 'error', 'none'].indexOf(options.onStabilityMissing) < 0) {
|
||||
if (['warn', 'error', 'none'].indexOf(options.onStabilityMissing as string) < 0) {
|
||||
throw new Error(
|
||||
'Argument for "--onStabilityMissing" option must be one of: "warn", "error", "none"');
|
||||
}
|
||||
|
@ -65,7 +76,8 @@ export function startCli() {
|
|||
lines.pop(); // Remove trailing newline
|
||||
}
|
||||
for (const line of lines) {
|
||||
const chalkMap = {'-': chalk.red, '+': chalk.green, '@': chalk.cyan};
|
||||
const chalkMap: {[key: string]:
|
||||
any} = {'-': chalk.red, '+': chalk.green, '@': chalk.cyan};
|
||||
const chalkFunc = chalkMap[line[0]] || chalk.reset;
|
||||
console.log(chalkFunc(line));
|
||||
}
|
||||
|
@ -73,6 +85,11 @@ export function startCli() {
|
|||
}
|
||||
|
||||
if (hasDiff) {
|
||||
// Under bazel, give instructions how to use bazel run to accept the golden file.
|
||||
if (!!process.env['BAZEL_TARGET']) {
|
||||
console.error('\n\nAccept the new golden file:');
|
||||
console.error(` bazel run ${process.env['BAZEL_TARGET']}.accept`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +97,9 @@ export function startCli() {
|
|||
}
|
||||
|
||||
export function parseArguments(input: string[]):
|
||||
{argv: ParsedArgs, mode: string, errors?: string[]} {
|
||||
{argv: minimist.ParsedArgs, mode: string, errors: string[]} {
|
||||
let help = false;
|
||||
const errors = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
const argv = minimist(input, {
|
||||
string: [
|
||||
|
@ -95,7 +112,7 @@ export function parseArguments(input: string[]):
|
|||
'color', 'no-color'
|
||||
],
|
||||
alias: {'outFile': 'out', 'verifyFile': 'verify'},
|
||||
unknown: option => {
|
||||
unknown: (option: string) => {
|
||||
if (option[0] === '-') {
|
||||
errors.push(`Unknown option: ${option}`);
|
||||
help = true;
|
||||
|
@ -171,7 +188,7 @@ Options:
|
|||
}
|
||||
|
||||
export function generateFileNamePairs(
|
||||
argv: ParsedArgs, mode: string): {entrypoint: string, goldenFile: string}[] {
|
||||
argv: minimist.ParsedArgs, mode: string): {entrypoint: string, goldenFile: string}[] {
|
||||
if (argv[mode]) {
|
||||
return [{entrypoint: argv._[0], goldenFile: argv[mode]}];
|
||||
|
||||
|
@ -179,7 +196,7 @@ export function generateFileNamePairs(
|
|||
let rootDir = argv['rootDir'] || '.';
|
||||
const goldenDir = argv[mode + 'Dir'];
|
||||
|
||||
return argv._.map(fileName => {
|
||||
return argv._.map((fileName: string) => {
|
||||
return {
|
||||
entrypoint: fileName,
|
||||
goldenFile: path.join(goldenDir, path.relative(rootDir, fileName))
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {createPatch} from 'diff';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
|
@ -37,8 +45,8 @@ export function publicApi(fileName: string, options: SerializationOptions = {}):
|
|||
}
|
||||
|
||||
export function publicApiInternal(
|
||||
host: ts.CompilerHost, fileName: string, tsOptions: ts.CompilerOptions,
|
||||
options: SerializationOptions = {}): string {
|
||||
host: ts.CompilerHost, fileName: string, tsOptions: ts.CompilerOptions,
|
||||
options: SerializationOptions = {}): string {
|
||||
const entrypoint = path.normalize(fileName);
|
||||
|
||||
if (!entrypoint.match(/\.d\.ts$/)) {
|
||||
|
@ -50,7 +58,7 @@ export function publicApiInternal(
|
|||
}
|
||||
|
||||
interface Diagnostic {
|
||||
type: DiagnosticSeverity;
|
||||
type?: DiagnosticSeverity;
|
||||
message: string;
|
||||
}
|
||||
|
||||
|
@ -87,7 +95,8 @@ class ResolvedDeclarationEmitter {
|
|||
continue;
|
||||
}
|
||||
|
||||
let decl: ts.Node = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0];
|
||||
let decl: ts.Node|undefined =
|
||||
symbol.valueDeclaration || symbol.declarations && symbol.declarations[0];
|
||||
if (!decl) {
|
||||
this.diagnostics.push({
|
||||
type: 'warn',
|
||||
|
@ -115,11 +124,11 @@ class ResolvedDeclarationEmitter {
|
|||
const match = stabilityAnnotationPattern.exec(trivia);
|
||||
if (match) {
|
||||
output += `/** @${match[1]} */\n`;
|
||||
} else if (['warn', 'error'].indexOf(this.options.onStabilityMissing) >= 0) {
|
||||
} else if (['warn', 'error'].indexOf(this.options.onStabilityMissing as string) >= 0) {
|
||||
this.diagnostics.push({
|
||||
type: this.options.onStabilityMissing,
|
||||
message: createErrorMessage(
|
||||
decl, `No stability annotation found for symbol "${symbol.name}"`)
|
||||
decl, `No stability annotation found for symbol "${symbol.name}"`)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -129,7 +138,7 @@ class ResolvedDeclarationEmitter {
|
|||
this.diagnostics.push({
|
||||
type: 'warn',
|
||||
message:
|
||||
createErrorMessage(decl, `No export declaration found for symbol "${symbol.name}"`)
|
||||
createErrorMessage(decl, `No export declaration found for symbol "${symbol.name}"`)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -161,8 +170,8 @@ class ResolvedDeclarationEmitter {
|
|||
return s;
|
||||
}
|
||||
throw new Error(
|
||||
`Symbol "${resolvedSymbol.name}" was aliased as "${s.name}". ` +
|
||||
`Aliases are not supported."`);
|
||||
`Symbol "${resolvedSymbol.name}" was aliased as "${s.name}". ` +
|
||||
`Aliases are not supported."`);
|
||||
}
|
||||
|
||||
return resolvedSymbol;
|
||||
|
@ -177,28 +186,31 @@ class ResolvedDeclarationEmitter {
|
|||
return '';
|
||||
}
|
||||
|
||||
const firstQualifier: ts.Identifier = getFirstQualifier(node);
|
||||
const firstQualifier: ts.Identifier|null = getFirstQualifier(node);
|
||||
|
||||
if (firstQualifier) {
|
||||
let isAllowed = false;
|
||||
|
||||
// Try to resolve the qualifier.
|
||||
const resolvedSymbol = this.typeChecker.getSymbolAtLocation(firstQualifier);
|
||||
if (resolvedSymbol && resolvedSymbol.declarations.length > 0) {
|
||||
// If the qualifier can be resolved, and it's not a namespaced import, then it should be allowed.
|
||||
isAllowed = resolvedSymbol.declarations.every(decl => decl.kind !== ts.SyntaxKind.NamespaceImport);
|
||||
if (resolvedSymbol && resolvedSymbol.declarations && resolvedSymbol.declarations.length > 0) {
|
||||
// If the qualifier can be resolved, and it's not a namespaced import, then it should be
|
||||
// allowed.
|
||||
isAllowed =
|
||||
resolvedSymbol.declarations.every(decl => decl.kind !== ts.SyntaxKind.NamespaceImport);
|
||||
}
|
||||
|
||||
// If it is not allowed otherwise, it's allowed if it's on the list of allowed identifiers.
|
||||
isAllowed = isAllowed || !(!this.options.allowModuleIdentifiers ||
|
||||
this.options.allowModuleIdentifiers.indexOf(firstQualifier.text) < 0);
|
||||
isAllowed = isAllowed ||
|
||||
!(!this.options.allowModuleIdentifiers ||
|
||||
this.options.allowModuleIdentifiers.indexOf(firstQualifier.text) < 0);
|
||||
if (!isAllowed) {
|
||||
this.diagnostics.push({
|
||||
type: 'error',
|
||||
message: createErrorMessage(
|
||||
firstQualifier,
|
||||
`Module identifier "${firstQualifier.text}" is not allowed. Remove it ` +
|
||||
`from source or whitelist it via --allowModuleIdentifiers.`)
|
||||
firstQualifier,
|
||||
`Module identifier "${firstQualifier.text}" is not allowed. Remove it ` +
|
||||
`from source or whitelist it via --allowModuleIdentifiers.`)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -220,15 +232,14 @@ class ResolvedDeclarationEmitter {
|
|||
children.sort((a: ts.NamedDeclaration, b: ts.NamedDeclaration) => {
|
||||
// Static after normal
|
||||
return compareFunction(
|
||||
hasModifier(a, ts.SyntaxKind.StaticKeyword),
|
||||
hasModifier(b, ts.SyntaxKind.StaticKeyword)
|
||||
) ||
|
||||
// Our predefined order
|
||||
compareFunction(
|
||||
memberDeclarationOrder[a.kind], memberDeclarationOrder[b.kind]) ||
|
||||
// Alphebetical order
|
||||
// We need safe dereferencing due to edge cases, e.g. having two call signatures
|
||||
compareFunction((a.name || a).getText(), (b.name || b).getText());
|
||||
hasModifier(a, ts.SyntaxKind.StaticKeyword),
|
||||
hasModifier(b, ts.SyntaxKind.StaticKeyword)) ||
|
||||
// Our predefined order
|
||||
compareFunction(
|
||||
memberDeclarationOrder[a.kind], memberDeclarationOrder[b.kind]) ||
|
||||
// Alphebetical order
|
||||
// We need safe dereferencing due to edge cases, e.g. having two call signatures
|
||||
compareFunction((a.name || a).getText(), (b.name || b).getText());
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
@ -236,10 +247,9 @@ class ResolvedDeclarationEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
let output = children
|
||||
.filter(x => x.kind !== ts.SyntaxKind.JSDocComment)
|
||||
.map(n => this.emitNode(n))
|
||||
.join('');
|
||||
let output: string = children.filter(x => x.kind !== ts.SyntaxKind.JSDocComment)
|
||||
.map(n => this.emitNode(n))
|
||||
.join('');
|
||||
|
||||
// Print stability annotation for fields
|
||||
if (node.kind in memberDeclarationOrder) {
|
||||
|
@ -273,7 +283,7 @@ function compareFunction<T>(a: T, b: T) {
|
|||
return a === b ? 0 : a > b ? 1 : -1;
|
||||
}
|
||||
|
||||
const memberDeclarationOrder = {
|
||||
const memberDeclarationOrder: {[key: number]: number} = {
|
||||
[ts.SyntaxKind.PropertySignature]: 0,
|
||||
[ts.SyntaxKind.PropertyDeclaration]: 0,
|
||||
[ts.SyntaxKind.GetAccessor]: 0,
|
||||
|
@ -295,7 +305,7 @@ function stripEmptyLines(text: string): string {
|
|||
/**
|
||||
* Returns the first qualifier if the input node is a dotted expression.
|
||||
*/
|
||||
function getFirstQualifier(node: ts.Node): ts.Identifier {
|
||||
function getFirstQualifier(node: ts.Node): ts.Identifier|null {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.PropertyAccessExpression: {
|
||||
// For expression position
|
||||
|
@ -324,7 +334,7 @@ function createErrorMessage(node: ts.Node, message: string): string {
|
|||
const sourceFile = node.getSourceFile();
|
||||
let position;
|
||||
if (sourceFile) {
|
||||
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
||||
const {line, character} = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
||||
position = `${sourceFile.fileName}(${line + 1},${character + 1})`;
|
||||
} else {
|
||||
position = '<unknown>';
|
||||
|
|
|
@ -33,14 +33,11 @@
|
|||
"gulp-typescript": "^4.0.1",
|
||||
"gulp-util": "^3.0.8",
|
||||
"merge2": "^1.2.1",
|
||||
"jasmine": "^3.1.0",
|
||||
"source-map": "^0.7.1",
|
||||
"source-map-support": "^0.5.3",
|
||||
"typescript": "~2.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublish": "gulp compile",
|
||||
"test": "gulp test.unit"
|
||||
},
|
||||
"repository": {},
|
||||
"keywords": [
|
||||
"typescript"
|
||||
|
@ -54,7 +51,7 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/angular/ts-api-guardian/issues"
|
||||
"url": "https://github.com/angular/angular/issues"
|
||||
},
|
||||
"homepage": "https://github.com/angular/ts-api-guardian"
|
||||
"homepage": "https://github.com/angular/angular/tools/ts-api-guardian"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
// Before running tests, change directories to our directory under the runfiles
|
||||
// From runfiles we want to go to the angular/tools/ts-api-guardian subfolder.
|
||||
process.chdir(path.join(process.env['TEST_SRCDIR'], 'angular', 'tools', 'ts-api-guardian'));
|
|
@ -1,13 +1,21 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import chai = require('chai');
|
||||
import * as child_process from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {assertFileEqual, unlinkRecursively} from './helpers';
|
||||
|
||||
const BINARY = path.resolve(__dirname, '../../bin/ts-api-guardian');
|
||||
const BINARY = 'ts-api-guardian/bin/ts-api-guardian';
|
||||
|
||||
describe('cli: e2e test', () => {
|
||||
const outDir = path.resolve(__dirname, '../../build/tmp');
|
||||
const outDir = path.join(process.env['TEST_TMPDIR'], 'tmp');
|
||||
|
||||
beforeEach(() => {
|
||||
if (!fs.existsSync(outDir)) {
|
||||
|
@ -29,8 +37,8 @@ describe('cli: e2e test', () => {
|
|||
|
||||
it('should generate golden file with --out', () => {
|
||||
const simpleFile = path.join(outDir, 'simple.d.ts');
|
||||
const {status} = execute(['--out', simpleFile, 'test/fixtures/simple.d.ts']);
|
||||
chai.assert.equal(status, 0);
|
||||
const {status, stderr} = execute(['--out', simpleFile, 'test/fixtures/simple.d.ts']);
|
||||
chai.assert.equal(status, 0, stderr);
|
||||
assertFileEqual(simpleFile, 'test/fixtures/simple_expected.d.ts');
|
||||
});
|
||||
|
||||
|
@ -117,7 +125,7 @@ describe('cli: e2e test', () => {
|
|||
stderr,
|
||||
'test/fixtures/simple.d.ts(1,1): error: No stability annotation found for symbol "A"\n' +
|
||||
'test/fixtures/simple.d.ts(2,1): error: No stability annotation found for symbol "B"\n');
|
||||
chai.assert.equal(status, 0);
|
||||
chai.assert.equal(status, 0, stderr);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -126,8 +134,12 @@ function copyFile(sourceFile: string, targetFile: string) {
|
|||
}
|
||||
|
||||
function execute(args: string[]): {stdout: string, stderr: string, status: number} {
|
||||
const output = child_process.spawnSync(BINARY, args);
|
||||
chai.assert(!output.error, 'Child process failed or timed out');
|
||||
const output = child_process.spawnSync(process.execPath, [path.resolve(BINARY), ...args], {
|
||||
env: {
|
||||
'NODE_PATH': process.cwd(),
|
||||
}
|
||||
});
|
||||
chai.assert(!output.error, 'Child process failed or timed out: ' + output.error);
|
||||
chai.assert(!output.signal, `Child process killed by signal ${output.signal}`);
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import chai = require('chai');
|
||||
import * as ts from 'typescript';
|
||||
import {parseArguments, generateFileNamePairs} from '../lib/cli';
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
|
@ -1,3 +1,11 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as chai from 'chai';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as chai from 'chai';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
@ -5,7 +13,7 @@ import * as main from '../lib/main';
|
|||
import {assertFileEqual, unlinkRecursively} from './helpers';
|
||||
|
||||
describe('integration test: public api', () => {
|
||||
let _warn = null;
|
||||
let _warn: any = null;
|
||||
let warnings: string[] = [];
|
||||
beforeEach(() => {
|
||||
_warn = console.warn;
|
||||
|
@ -71,7 +79,7 @@ describe('integration test: public api', () => {
|
|||
});
|
||||
|
||||
describe('integration test: generateGoldenFile', () => {
|
||||
const outDir = path.resolve(__dirname, '../../build/tmp');
|
||||
const outDir = path.join(process.env['TEST_TMPDIR'], 'tmp');
|
||||
const outFile = path.join(outDir, 'out.d.ts');
|
||||
const deepOutFile = path.join(outDir, 'a/b/c/out.d.ts');
|
||||
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as chai from 'chai';
|
||||
import * as ts from 'typescript';
|
||||
import { publicApiInternal, SerializationOptions } from '../lib/serializer';
|
||||
|
||||
import {SerializationOptions, publicApiInternal} from '../lib/serializer';
|
||||
|
||||
const classesAndInterfaces = `
|
||||
export declare class A {
|
||||
|
@ -20,7 +29,7 @@ const classesAndInterfaces = `
|
|||
`;
|
||||
|
||||
describe('unit test', () => {
|
||||
let _warn = null;
|
||||
let _warn: any = null;
|
||||
let warnings: string[] = [];
|
||||
beforeEach(() => {
|
||||
_warn = console.warn;
|
||||
|
@ -47,7 +56,7 @@ describe('unit test', () => {
|
|||
protected fb(): void;
|
||||
}
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected);
|
||||
check({'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should ignore private props', () => {
|
||||
|
@ -64,7 +73,7 @@ describe('unit test', () => {
|
|||
protected fb: any;
|
||||
}
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected);
|
||||
check({'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should support imports without capturing imports', () => {
|
||||
|
@ -79,7 +88,7 @@ describe('unit test', () => {
|
|||
field: A;
|
||||
}
|
||||
`;
|
||||
check({ 'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input }, expected);
|
||||
check({'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should throw on aliased reexports', () => {
|
||||
|
@ -87,8 +96,8 @@ describe('unit test', () => {
|
|||
export { A as Apple } from './classes_and_interfaces';
|
||||
`;
|
||||
checkThrows(
|
||||
{ 'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input },
|
||||
'Symbol "A" was aliased as "Apple". Aliases are not supported.');
|
||||
{'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input},
|
||||
'Symbol "A" was aliased as "Apple". Aliases are not supported.');
|
||||
});
|
||||
|
||||
it('should remove reexported external symbols', () => {
|
||||
|
@ -97,9 +106,9 @@ describe('unit test', () => {
|
|||
`;
|
||||
const expected = `
|
||||
`;
|
||||
check({ 'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input }, expected);
|
||||
check({'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input}, expected);
|
||||
chai.assert.deepEqual(
|
||||
warnings, ['file.d.ts(1,1): error: No export declaration found for symbol "Foo"']);
|
||||
warnings, ['file.d.ts(1,1): error: No export declaration found for symbol "Foo"']);
|
||||
});
|
||||
|
||||
it('should sort exports', () => {
|
||||
|
@ -134,7 +143,7 @@ describe('unit test', () => {
|
|||
|
||||
export declare type E = string;
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected);
|
||||
check({'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should sort class members', () => {
|
||||
|
@ -158,7 +167,7 @@ describe('unit test', () => {
|
|||
static foo(): void;
|
||||
}
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected);
|
||||
check({'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should sort interface members', () => {
|
||||
|
@ -180,7 +189,7 @@ describe('unit test', () => {
|
|||
c(): void;
|
||||
}
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected);
|
||||
check({'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should sort class members including readonly', () => {
|
||||
|
@ -215,7 +224,7 @@ describe('unit test', () => {
|
|||
constructor(nativeNode: any, parent: DebugNode | null, _debugContext: any);
|
||||
}
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected);
|
||||
check({'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should sort two call signatures', () => {
|
||||
|
@ -231,7 +240,7 @@ describe('unit test', () => {
|
|||
(b: number): void;
|
||||
}
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected);
|
||||
check({'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should sort exports including re-exports', () => {
|
||||
|
@ -269,7 +278,7 @@ describe('unit test', () => {
|
|||
|
||||
export declare type E = string;
|
||||
`;
|
||||
check({ 'submodule.d.ts': submodule, 'file.d.ts': input }, expected);
|
||||
check({'submodule.d.ts': submodule, 'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should remove module comments', () => {
|
||||
|
@ -289,7 +298,7 @@ describe('unit test', () => {
|
|||
|
||||
export declare function foo(): boolean;
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected);
|
||||
check({'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should remove class and field comments', () => {
|
||||
|
@ -314,7 +323,7 @@ describe('unit test', () => {
|
|||
name: string;
|
||||
}
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected);
|
||||
check({'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should skip symbols matching specified pattern', () => {
|
||||
|
@ -327,7 +336,7 @@ describe('unit test', () => {
|
|||
export class B {
|
||||
}
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected, { stripExportPattern: /^__.*/ });
|
||||
check({'file.d.ts': input}, expected, {stripExportPattern: /^__.*/});
|
||||
});
|
||||
|
||||
it('should throw on using non-whitelisted module imports in expression position', () => {
|
||||
|
@ -337,8 +346,8 @@ describe('unit test', () => {
|
|||
}
|
||||
`;
|
||||
checkThrows(
|
||||
{ 'file.d.ts': input }, 'file.d.ts(2,32): error: Module identifier "foo" is not allowed. ' +
|
||||
'Remove it from source or whitelist it via --allowModuleIdentifiers.');
|
||||
{'file.d.ts': input}, 'file.d.ts(2,32): error: Module identifier "foo" is not allowed. ' +
|
||||
'Remove it from source or whitelist it via --allowModuleIdentifiers.');
|
||||
});
|
||||
|
||||
it('should throw on using non-whitelisted module imports in type position', () => {
|
||||
|
@ -347,8 +356,8 @@ describe('unit test', () => {
|
|||
export type A = foo.A;
|
||||
`;
|
||||
checkThrows(
|
||||
{ 'file.d.ts': input }, 'file.d.ts(2,17): error: Module identifier "foo" is not allowed. ' +
|
||||
'Remove it from source or whitelist it via --allowModuleIdentifiers.');
|
||||
{'file.d.ts': input}, 'file.d.ts(2,17): error: Module identifier "foo" is not allowed. ' +
|
||||
'Remove it from source or whitelist it via --allowModuleIdentifiers.');
|
||||
});
|
||||
|
||||
it('should not throw on using whitelisted module imports', () => {
|
||||
|
@ -361,7 +370,7 @@ describe('unit test', () => {
|
|||
export declare class A extends foo.A {
|
||||
}
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected, { allowModuleIdentifiers: ['foo'] });
|
||||
check({'file.d.ts': input}, expected, {allowModuleIdentifiers: ['foo']});
|
||||
});
|
||||
|
||||
it('should not throw if non-whitelisted module imports are not written', () => {
|
||||
|
@ -374,7 +383,7 @@ describe('unit test', () => {
|
|||
export declare class A {
|
||||
}
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected);
|
||||
check({'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should keep stability annotations of exports in docstrings', () => {
|
||||
|
@ -404,7 +413,7 @@ describe('unit test', () => {
|
|||
/** @stable */
|
||||
export declare var c: number;
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected);
|
||||
check({'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should keep stability annotations of fields in docstrings', () => {
|
||||
|
@ -431,7 +440,7 @@ describe('unit test', () => {
|
|||
/** @deprecated */ foo(): void;
|
||||
}
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected);
|
||||
check({'file.d.ts': input}, expected);
|
||||
});
|
||||
|
||||
it('should warn on onStabilityMissing: warn', () => {
|
||||
|
@ -445,20 +454,20 @@ describe('unit test', () => {
|
|||
constructor();
|
||||
}
|
||||
`;
|
||||
check({ 'file.d.ts': input }, expected, { onStabilityMissing: 'warn' });
|
||||
check({'file.d.ts': input}, expected, {onStabilityMissing: 'warn'});
|
||||
chai.assert.deepEqual(
|
||||
warnings, ['file.d.ts(1,1): error: No stability annotation found for symbol "A"']);
|
||||
warnings, ['file.d.ts(1,1): error: No stability annotation found for symbol "A"']);
|
||||
});
|
||||
});
|
||||
|
||||
function getMockHost(files: { [name: string]: string }): ts.CompilerHost {
|
||||
function getMockHost(files: {[name: string]: string}): ts.CompilerHost {
|
||||
return {
|
||||
getSourceFile: (sourceName, languageVersion) => {
|
||||
if (!files[sourceName]) return undefined;
|
||||
return ts.createSourceFile(
|
||||
sourceName, stripExtraIndentation(files[sourceName]), languageVersion, true);
|
||||
sourceName, stripExtraIndentation(files[sourceName]), languageVersion, true);
|
||||
},
|
||||
writeFile: (name, text, writeByteOrderMark) => { },
|
||||
writeFile: (name, text, writeByteOrderMark) => {},
|
||||
fileExists: (filename) => !!files[filename],
|
||||
readFile: (filename) => stripExtraIndentation(files[filename]),
|
||||
getDefaultLibFileName: () => 'lib.ts',
|
||||
|
@ -471,12 +480,12 @@ function getMockHost(files: { [name: string]: string }): ts.CompilerHost {
|
|||
}
|
||||
|
||||
function check(
|
||||
files: { [name: string]: string }, expected: string, options: SerializationOptions = {}) {
|
||||
files: {[name: string]: string}, expected: string, options: SerializationOptions = {}) {
|
||||
const actual = publicApiInternal(getMockHost(files), 'file.d.ts', {}, options);
|
||||
chai.assert.equal(actual.trim(), stripExtraIndentation(expected).trim());
|
||||
}
|
||||
|
||||
function checkThrows(files: { [name: string]: string }, error: string) {
|
||||
function checkThrows(files: {[name: string]: string}, error: string) {
|
||||
chai.assert.throws(() => { publicApiInternal(getMockHost(files), 'file.d.ts', {}); }, error);
|
||||
}
|
||||
|
||||
|
@ -485,7 +494,7 @@ function stripExtraIndentation(text: string) {
|
|||
// Ignore first and last new line
|
||||
lines = lines.slice(1, lines.length - 1);
|
||||
const commonIndent = lines.reduce((min, line) => {
|
||||
const indent = /^( *)/.exec(line)[1].length;
|
||||
const indent = /^( *)/.exec(line) ![1].length;
|
||||
// Ignore empty line
|
||||
return line.length ? Math.min(min, indent) : min;
|
||||
}, text.length);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -23,6 +23,7 @@
|
|||
"exclude": [
|
||||
"testing",
|
||||
"node_modules",
|
||||
"ts-api-guardian",
|
||||
"typings-test",
|
||||
"public_api_guard",
|
||||
"docs"
|
||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -288,9 +288,9 @@ ansi-styles@^2.2.1:
|
|||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
|
||||
|
||||
ansi-styles@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
dependencies:
|
||||
color-convert "^1.9.0"
|
||||
|
||||
|
@ -1008,12 +1008,12 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
|
|||
supports-color "^2.0.0"
|
||||
|
||||
chalk@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796"
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65"
|
||||
dependencies:
|
||||
ansi-styles "^3.2.0"
|
||||
ansi-styles "^3.2.1"
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.2.0"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
char-spinner@^1.0.1:
|
||||
version "1.0.1"
|
||||
|
@ -6871,9 +6871,9 @@ supports-color@^3.1.0:
|
|||
dependencies:
|
||||
has-flag "^1.0.0"
|
||||
|
||||
supports-color@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a"
|
||||
supports-color@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0"
|
||||
dependencies:
|
||||
has-flag "^3.0.0"
|
||||
|
||||
|
|
Loading…
Reference in New Issue