build: remove ts-api-guardian from repository (#42735)

This commit removes `ts-api-guardian` from the repository. We introduced
a new tool for testing API signature that is part of the shared
dev-infra package. The TS API guardian package will be deprecated for
the public in favor of Microsoft's API extractor that has support for
more parts of the syntax, such as alias exports.

PR Close #42735
This commit is contained in:
Paul Gschwendtner 2021-07-01 17:49:21 +02:00 committed by Dylan Hunn
parent 271700cbb6
commit 72c03a0d4f
57 changed files with 4 additions and 2443 deletions

View File

@ -798,11 +798,11 @@ jobs:
- setup_win
- run:
name: Build all windows CI targets
command: yarn bazel build --build_tag_filters=-ivy-only //packages/compiler-cli/... //tools/ts-api-guardian/...
command: yarn bazel build --build_tag_filters=-ivy-only //packages/compiler-cli/...
no_output_timeout: 15m
- run:
name: Test all windows CI targets
command: yarn bazel test --test_tag_filters="-ivy-only,-browser:chromium-local" //packages/compiler-cli/... //tools/ts-api-guardian/...
command: yarn bazel test --test_tag_filters="-ivy-only,-browser:chromium-local" //packages/compiler-cli/...
no_output_timeout: 15m
test_ivy_aot_win:
@ -811,11 +811,11 @@ jobs:
- setup_win
- run:
name: Build all windows CI targets
command: yarn bazel build --config=ivy --build_tag_filters=-no-ivy-aot,-fixme-ivy-aot //packages/compiler-cli/... //tools/ts-api-guardian/...
command: yarn bazel build --config=ivy --build_tag_filters=-no-ivy-aot,-fixme-ivy-aot //packages/compiler-cli/...
no_output_timeout: 15m
- run:
name: Test all windows CI targets
command: yarn bazel test --config=ivy --test_tag_filters="-no-ivy-aot,-fixme-ivy-aot,-browser:chromium-local" //packages/compiler-cli/... //tools/ts-api-guardian/... //packages/localize/...
command: yarn bazel test --config=ivy --test_tag_filters="-no-ivy-aot,-fixme-ivy-aot,-browser:chromium-local" //packages/compiler-cli/... //packages/localize/...
no_output_timeout: 15m
# Save dependencies to use on subsequent runs.
- save_cache:

3
.gitattributes vendored
View File

@ -5,8 +5,5 @@
*.js eol=lf
*.ts eol=lf
# API guardian patch must always use LF for tests to work
*.patch eol=lf
# Must keep Windows line ending to be parsed correctly
scripts/windows/packages.txt eol=crlf

View File

@ -1177,7 +1177,6 @@ groups:
'tools/source-map-test/**',
'tools/symbol-extractor/**',
'tools/testing/**',
'tools/ts-api-guardian/**',
'tools/tslint/**',
'tools/utils/**',
'tools/yarn/**',

View File

@ -53,7 +53,6 @@
"integration/bazel/WORKSPACE",
"package.json",
"packages/**/package.json",
"tools/ts-api-guardian/package.json",
"aio/package.json"
],
"packageRules": [

View File

@ -1,115 +0,0 @@
# BEGIN-INTERNAL
load("@build_bazel_rules_nodejs//:index.bzl", "pkg_npm")
load("@npm//@bazel/typescript:index.bzl", "ts_library")
load("//tools:defaults.bzl", "jasmine_node_test")
ts_library(
name = "lib",
srcs = glob(["lib/*.ts"]),
module_name = "ts-api-guardian",
tsconfig = "//tools:tsconfig.json",
visibility = ["//visibility:public"],
deps = [
"@npm//@types/diff",
"@npm//@types/minimist",
"@npm//@types/node",
"@npm//chalk",
"@npm//diff",
"@npm//minimist",
"@npm//typescript",
],
)
# 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 $< $@",
)
pkg_npm(
name = "ts-api-guardian",
package_name = "ts-api-guardian",
srcs = [
"BUILD.bazel",
"README.md",
"bin/ts-api-guardian",
"index.bzl",
"package.json",
],
substitutions = {
"@angular//tools/ts-api-guardian:bin": "//:node_modules/ts-api-guardian/bin",
"@angular//tools/ts-api-guardian:lib": "@npm//ts-api-guardian",
},
deps = [
":lib",
":license",
],
)
#######################################3
# Tests for this package
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(
["test/*.ts"],
exclude = ["test/bootstrap.ts"],
),
tsconfig = "//tools:tsconfig-test",
deps = [
":lib",
"@npm//@bazel/runfiles",
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//jasmine",
"@npm//typescript",
],
)
ts_library(
name = "bootstrap",
testonly = True,
srcs = ["test/bootstrap.ts"],
tsconfig = "//tools:tsconfig-test",
deps = ["@npm//@types/node"],
)
# Select the es5 .js output of the ts_library :boostrap target
# with `output_group = "es5_sources"` for use in the jasmine_node_test
# below. This exposes an internal detail of ts_library that is not ideal.
# TODO(gregmagolan): clean this up by using tsc() in this case rather than ts_library
filegroup(
name = "bootstrap_es5",
testonly = True,
srcs = [":bootstrap"],
output_group = "es5_sources",
)
jasmine_node_test(
name = "tests",
srcs = [
":test_lib",
],
bootstrap = [":bootstrap_es5"],
data = glob([
"test/fixtures/*.ts",
"test/fixtures/*.patch",
]) + [
":ts-api-guardian",
],
)
filegroup(
name = "bin",
srcs = glob(["lib/*.js"]) + ["bin/ts-api-guardian"],
visibility = ["//visibility:public"],
)
# Exported to be referenced as entry_point of the nodejs_binary
exports_files(["bin/ts-api-guardian"])
# END-INTERNAL

View File

@ -1,36 +0,0 @@
# Typescript API Guardian
Keeps track of public API surface of a typescript library.
Examples:
```sh
# Generate one declaration file
ts-api-guardian --out api_guard.d.ts index.d.ts
# Generate multiple declaration files
# (output location like typescript)
ts-api-guardian --outDir api_guard [--rootDir .] core/index.d.ts core/testing.d.ts
# Print usage
ts-api-guardian --help
# Check against one declaration file
ts-api-guardian --verify api_guard.d.ts index.d.ts
# Check against multiple declaration files
ts-api-guardian --verifyDir api_guard [--rootDir .] core/index.d.ts core/testing.d.ts
```
# For developers
Build and test this library:
```sh
$ yarn bazel run //:install
$ yarn bazel test //tools/ts-api-guardian:all
```
Publish to NPM:
```sh
$ yarn bazel run @nodejs//:npm whoami # should be logged in as angular
$ grep version tools/ts-api-guardian/package.json # advance as needed
$ yarn bazel run //tools/ts-api-guardian:ts-api-guardian.publish
```

View File

@ -1,3 +0,0 @@
#!/usr/bin/env node
require('../lib/cli').startCli();

View File

@ -1,148 +0,0 @@
# 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//:index.bzl", "nodejs_binary", "nodejs_test")
COMMON_MODULE_IDENTIFIERS = ["angular", "jasmine", "protractor", "Symbol"]
def ts_api_guardian_test(
name,
golden,
actual,
data = [],
strip_export_pattern = [],
allow_module_identifiers = COMMON_MODULE_IDENTIFIERS,
use_angular_tag_rules = True,
**kwargs):
"""Runs ts_api_guardian
"""
data += [
# Locally we need to add the TS build target
# But it will replaced to @npm//ts-api-guardian when publishing
"@angular//tools/ts-api-guardian:lib",
# BEGIN-INTERNAL
"@angular//tools/ts-api-guardian:bin",
# END-INTERNAL
# The below are required during runtime
"@npm//chalk",
"@npm//diff",
"@npm//minimist",
"@npm//typescript",
]
args = [
# Needed so that node doesn't walk back to the source directory.
# From there, the relative imports would point to .ts files.
"--node_options=--preserve-symlinks",
# TODO(josephperrott): update dependency usages to no longer need bazel patch module resolver
# See: https://github.com/bazelbuild/rules_nodejs/wiki#--bazel_patch_module_resolver-now-defaults-to-false-2324
"--bazel_patch_module_resolver",
]
for i in strip_export_pattern:
# Quote the regexp before passing it via the command line.
quoted_pattern = "\"%s\"" % i
args += ["--stripExportPattern", quoted_pattern]
for i in allow_module_identifiers:
args += ["--allowModuleIdentifiers", i]
if use_angular_tag_rules:
args += ["--useAngularTagRules"]
nodejs_test(
name = name,
data = data,
entry_point = Label("@angular//tools/ts-api-guardian:bin/ts-api-guardian"),
tags = kwargs.pop("tags", []) + ["api_guard"],
templated_args = args + ["--verify", golden, actual],
**kwargs
)
nodejs_binary(
name = name + ".accept",
testonly = True,
data = data,
entry_point = Label("@angular//tools/ts-api-guardian:bin/ts-api-guardian"),
tags = kwargs.pop("tags", []) + ["api_guard"],
templated_args = args + ["--out", golden, actual],
**kwargs
)
def ts_api_guardian_test_npm_package(
name,
goldenDir,
actualDir,
data = [],
strip_export_pattern = ["^ɵ(?!ɵdefineInjectable|ɵinject|ɵInjectableDef)"],
allow_module_identifiers = COMMON_MODULE_IDENTIFIERS,
use_angular_tag_rules = True,
**kwargs):
"""Runs ts_api_guardian
"""
data += [
# Locally we need to add the TS build target
# But it will replaced to @npm//ts-api-guardian when publishing
"@angular//tools/ts-api-guardian:lib",
"@angular//tools/ts-api-guardian:bin",
# The below are required during runtime
"@npm//chalk",
"@npm//diff",
"@npm//minimist",
"@npm//typescript",
]
args = [
# Needed so that node doesn't walk back to the source directory.
# From there, the relative imports would point to .ts files.
"--node_options=--preserve-symlinks",
# We automatically discover the enpoints for our NPM package.
"--autoDiscoverEntrypoints",
# TODO(josephperrott): update dependency usages to no longer need bazel patch module resolver
# See: https://github.com/bazelbuild/rules_nodejs/wiki#--bazel_patch_module_resolver-now-defaults-to-false-2324
"--bazel_patch_module_resolver",
]
for i in strip_export_pattern:
# Quote the regexp before passing it via the command line.
quoted_pattern = "\"%s\"" % i
args += ["--stripExportPattern", quoted_pattern]
for i in allow_module_identifiers:
args += ["--allowModuleIdentifiers", i]
if use_angular_tag_rules:
args += ["--useAngularTagRules"]
nodejs_test(
name = name,
data = data,
entry_point = "@angular//tools/ts-api-guardian:bin/ts-api-guardian",
tags = kwargs.pop("tags", []) + ["api_guard"],
templated_args = args + ["--autoDiscoverEntrypoints", "--verifyDir", goldenDir, "--rootDir", "$(rlocation %s)" % actualDir],
**kwargs
)
nodejs_binary(
name = name + ".accept",
testonly = True,
data = data,
entry_point = "@angular//tools/ts-api-guardian:bin/ts-api-guardian",
tags = kwargs.pop("tags", []) + ["api_guard"],
templated_args = args + ["--autoDiscoverEntrypoints", "--outDir", goldenDir, "--rootDir", "$(rlocation %s)" % actualDir],
**kwargs
)

View File

@ -1,279 +0,0 @@
/**
* @license
* Copyright Google LLC 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
import * as chalk from 'chalk';
import * as minimist from 'minimist';
import * as path from 'path';
import {discoverAllEntrypoints, generateGoldenFile, SerializationOptions, verifyAgainstGoldenFile} from './main';
/** Name of the CLI */
const CMD = 'ts-api-guardian';
/** Name of the Bazel workspace that runs the CLI. */
const bazelWorkspaceName = process.env.BAZEL_WORKSPACE;
/**
* Path to the Bazel workspace directory. Only set if the CLI is run with `bazel run`.
* https://docs.bazel.build/versions/master/user-manual.html#run.
*/
const bazelWorkspaceDirectory = process.env.BUILD_WORKSPACE_DIRECTORY;
/**
* Regular expression that matches Bazel manifest paths that start with the
* current Bazel workspace, followed by a path delimiter.
*/
const bazelWorkspaceManifestPathRegex =
bazelWorkspaceName ? new RegExp(`^${bazelWorkspaceName}[/\\\\]`) : null;
export function startCli() {
const {argv, mode, errors} = parseArguments(process.argv.slice(2));
const options: SerializationOptions = {
stripExportPattern: [].concat(argv['stripExportPattern']),
allowModuleIdentifiers: [].concat(argv['allowModuleIdentifiers']),
};
// Since the API guardian can be also used by other projects, we should not set up the default
// Angular project tag rules unless specified explicitly through a given option.
if (argv['useAngularTagRules']) {
options.exportTags = {
requireAtLeastOne: ['publicApi', 'codeGenApi'],
banned: ['experimental'],
toCopy: ['deprecated', 'codeGenApi']
};
options.memberTags = {
requireAtLeastOne: [],
banned: ['experimental', 'publicApi', 'codeGenApi'],
toCopy: ['deprecated']
};
options.paramTags = {
requireAtLeastOne: [],
banned: ['experimental', 'publicApi', 'codeGenApi'],
toCopy: ['deprecated']
};
}
// In autoDiscoverEntrypoints mode we set the inputed files as the discovered entrypoints
// for the rootDir
let entrypoints: string[];
if (argv['autoDiscoverEntrypoints']) {
entrypoints = discoverAllEntrypoints(argv['rootDir']);
} else {
entrypoints = argv._.slice();
}
for (const error of errors) {
console.warn(error);
}
if (mode === 'help') {
printUsageAndExit(!!errors.length);
} else {
const targets = resolveFileNamePairs(argv, mode, entrypoints);
if (mode === 'out') {
for (const {entrypoint, goldenFile} of targets) {
generateGoldenFile(entrypoint, goldenFile, options);
}
} else { // mode === 'verify'
let hasDiff = false;
for (const {entrypoint, goldenFile} of targets) {
const diff = verifyAgainstGoldenFile(entrypoint, goldenFile, options);
if (diff) {
hasDiff = true;
const lines = diff.split('\n');
if (lines.length) {
lines.pop(); // Remove trailing newline
}
for (const line of lines) {
const chalkMap:
{[key: string]: any} = {'-': chalk.red, '+': chalk.green, '@': chalk.cyan};
const chalkFunc = chalkMap[line[0]] || chalk.reset;
console.log(chalkFunc(line));
}
}
}
if (hasDiff) {
const bazelTarget = process.env['BAZEL_TARGET'];
// Under bazel, give instructions how to use bazel run to accept the golden file.
if (bazelTarget) {
console.error('\n\nIf you modify a public API, you must accept the new golden file.');
console.error('\n\nTo do so, execute the following Bazel target:');
console.error(` yarn bazel run ${bazelTarget.replace(/_bin$/, '')}.accept`);
if (process.env['TEST_WORKSPACE'] === 'angular') {
console.error('\n\nFor more information, see');
console.error(
'\n https://github.com/angular/angular/blob/master/docs/PUBLIC_API.md#golden-files');
}
}
process.exit(1);
}
}
}
}
export function parseArguments(input: string[]):
{argv: minimist.ParsedArgs, mode: string, errors: string[]} {
let help = false;
const errors: string[] = [];
const argv = minimist(input, {
string: [
'out', 'outDir', 'verify', 'verifyDir', 'rootDir', 'stripExportPattern',
'allowModuleIdentifiers'
],
boolean: [
'help', 'useAngularTagRules', 'autoDiscoverEntrypoints',
// Options used by chalk automagically
'color', 'no-color'
],
alias: {'outFile': 'out', 'verifyFile': 'verify'},
unknown: (option: string) => {
if (option[0] === '-') {
errors.push(`Unknown option: ${option}`);
help = true;
return false; // do not add to argv._
} else {
return true; // add to argv._
}
}
});
help = help || argv['help'];
if (help) {
return {argv, mode: 'help', errors};
}
let modes: string[] = [];
if (argv['out']) {
modes.push('out');
}
if (argv['outDir']) {
modes.push('out');
}
if (argv['verify']) {
modes.push('verify');
}
if (argv['verifyDir']) {
modes.push('verify');
}
if (argv['autoDiscoverEntrypoints']) {
if (!argv['rootDir']) {
errors.push(`--rootDir must be provided with --autoDiscoverEntrypoints.`);
modes = ['help'];
}
if (!argv['outDir'] && !argv['verifyDir']) {
errors.push(`--outDir or --verifyDir must be used with --autoDiscoverEntrypoints.`);
modes = ['help'];
}
} else {
if (!argv._.length) {
errors.push('No input file specified.');
modes = ['help'];
} else if (modes.length !== 1) {
errors.push('Specify either --out[Dir] or --verify[Dir]');
modes = ['help'];
} else if (argv._.length > 1 && !argv['outDir'] && !argv['verifyDir']) {
errors.push(`More than one input specified. Use --${modes[0]}Dir instead.`);
modes = ['help'];
}
}
return {argv, mode: modes[0], errors};
}
function printUsageAndExit(error = false) {
const print = error ? console.warn.bind(console) : console.log.bind(console);
print(`Usage: ${CMD} [options] <file ...>
${CMD} --out <output file> <entrypoint .d.ts file>
${CMD} --outDir <output dir> [--rootDir .] <entrypoint .d.ts files>
${CMD} --verify <golden file> <entrypoint .d.ts file>
${CMD} --verifyDir <golden file dir> [--rootDir .] <entrypoint .d.ts files>
Options:
--help Show this usage message
--out <file> Write golden output to file
--outDir <dir> Write golden file structure to directory
--verify <file> Read golden input from file
--verifyDir <dir> Read golden file structure from directory
--rootDir <dir> Specify the root directory of input files
--useAngularTagRules <boolean> Whether the Angular specific tag rules should be used.
--stripExportPattern <regexp> Do not output exports matching the pattern
--allowModuleIdentifiers <identifier>
Allow identifier for "* as foo" imports
--autoDiscoverEntrypoints Automatically find all entrypoints .d.ts files in the rootDir`);
process.exit(error ? 1 : 0);
}
/**
* Resolves a given path in the file system. If `ts-api-guardian` runs with Bazel, file paths
* are resolved through runfiles. Additionally in Bazel, this method handles the case where
* manifest file paths are not existing, but need to resolve to the Bazel workspace directory.
* This happens commonly when goldens are approved, but the golden file does not exist yet.
*/
function resolveFilePath(fileName: string): string {
// If an absolute path is specified, the path is already resolved.
if (path.isAbsolute(fileName)) {
return fileName;
}
// Outside of Bazel, file paths are resolved based on the current working directory.
if (!bazelWorkspaceName) {
return path.resolve(fileName);
}
// In Bazel, we first try to resolve the file through the runfiles. We do this by calling
// the `require.resolve` function that is patched by the Bazel NodeJS rules. Note that we
// need to catch errors because files inside tree artifacts cannot be resolved through
// runfile manifests. Hence, we need to have alternative resolution logic when resolving
// file paths. Additionally, it could happen that manifest paths which aren't part of the
// runfiles are specified (i.e. golden is approved but does not exist in the workspace yet).
try {
return require.resolve(fileName);
} catch {
}
// This handles cases where file paths cannot be resolved through runfiles. This happens
// commonly when goldens are approved while the golden does not exist in the workspace yet.
// In those cases, we want to build up a relative path based on the manifest path, and join
// it with the absolute bazel workspace directory (which is only set in `bazel run`).
// e.g. `angular/goldens/<..>/common` should become `{workspace_dir}/goldens/<...>/common`.
if (bazelWorkspaceManifestPathRegex !== null && bazelWorkspaceDirectory &&
bazelWorkspaceManifestPathRegex.test(fileName)) {
return path.join(bazelWorkspaceDirectory, fileName.substr(bazelWorkspaceName.length + 1));
}
throw Error(`Could not resolve file path in runfiles: ${fileName}`);
}
function resolveFileNamePairs(argv: minimist.ParsedArgs, mode: string, entrypoints: string[]):
{entrypoint: string, goldenFile: string}[] {
if (argv[mode]) {
return [{
entrypoint: resolveFilePath(entrypoints[0]),
goldenFile: resolveFilePath(argv[mode]),
}];
} else { // argv[mode + 'Dir']
let rootDir = argv['rootDir'] || '.';
const goldenDir = argv[mode + 'Dir'];
return entrypoints.map((fileName: string) => {
return {
entrypoint: resolveFilePath(fileName),
goldenFile: resolveFilePath(path.join(goldenDir, path.relative(rootDir, fileName))),
};
});
}
}

View File

@ -1,98 +0,0 @@
/**
* @license
* Copyright Google LLC 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';
import {publicApi, SerializationOptions} from './serializer';
export {publicApi, SerializationOptions} from './serializer';
export function generateGoldenFile(
entrypoint: string, outFile: string, options: SerializationOptions = {}): void {
const output = publicApi(entrypoint, options);
ensureDirectory(path.dirname(outFile));
fs.writeFileSync(outFile, output);
}
export function verifyAgainstGoldenFile(
entrypoint: string, goldenFile: string, options: SerializationOptions = {}): string {
const actual = publicApi(entrypoint, options);
const expected = fs.existsSync(goldenFile) ? fs.readFileSync(goldenFile).toString() : '';
if (actual === expected) {
return '';
} else {
// The patch should not show absolute paths, as these are pretty long and obfuscated
// the printed golden diff. Additionally, path separators in the patch should be forward
// slashes for consistency and to enable easier integration testing.
const displayFileName = path.relative(process.cwd(), goldenFile).replace(/\\/g, '/');
const patch = createPatch(displayFileName, expected, actual, 'Golden file', 'Generated API');
// Remove the header of the patch
const start = patch.indexOf('\n', patch.indexOf('\n') + 1) + 1;
return patch.substring(start);
}
}
function ensureDirectory(dir: string) {
if (!fs.existsSync(dir)) {
ensureDirectory(path.dirname(dir));
fs.mkdirSync(dir);
}
}
/**
* Determine if the provided path is a directory.
*/
function isDirectory(dirPath: string) {
try {
return fs.lstatSync(dirPath).isDirectory();
} catch {
return false;
}
}
/**
* Gets an array of paths to the typings files for each of the recursively discovered
* package.json
* files from the directory provided.
*/
export function discoverAllEntrypoints(dirPath: string) {
// Determine all of the package.json files
const packageJsons: string[] = [];
const entryPoints: string[] = [];
const findPackageJsonsInDir = (nextPath: string) => {
for (const file of fs.readdirSync(nextPath)) {
const fullPath = path.join(nextPath, file);
if (isDirectory(fullPath)) {
findPackageJsonsInDir(fullPath);
} else {
if (file === 'package.json') {
packageJsons.push(fullPath);
}
}
}
};
findPackageJsonsInDir(dirPath);
// Get all typings file locations from package.json files
for (const packageJson of packageJsons) {
const packageJsonObj =
JSON.parse(fs.readFileSync(packageJson, {encoding: 'utf8'})) as {typings: string};
const typings = packageJsonObj.typings;
if (typings) {
entryPoints.push(path.join(path.dirname(packageJson), typings));
}
}
return entryPoints;
}

View File

@ -1,448 +0,0 @@
/**
* @license
* Copyright Google LLC 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';
const baseTsOptions: ts.CompilerOptions = {
// We don't want symbols from external modules to be resolved, so we use the
// classic algorithm.
moduleResolution: ts.ModuleResolutionKind.Classic
};
export interface JsDocTagOptions {
/**
* An array of names of jsdoc tags, one of which must exist. If no tags are provided, there are no
* required tags.
*/
requireAtLeastOne?: string[];
/**
* An array of names of jsdoc tags that must not exist.
*/
banned?: string[];
/**
* An array of names of jsdoc tags that will be copied to the serialized code.
*/
toCopy?: string[];
}
export interface SerializationOptions {
/**
* Removes all exports matching the regular expression.
*/
stripExportPattern?: RegExp|RegExp[];
/**
* Allows these identifiers as modules in the output. For example,
* ```
* import * as angular from './angularjs';
*
* export class Foo extends angular.Bar {}
* ```
* will produce `export class Foo extends angular.Bar {}` and requires explicitly allowing
* `angular` as a module identifier.
*/
allowModuleIdentifiers?: string[];
/** The jsdoc tag options for top level exports */
exportTags?: JsDocTagOptions;
/** The jsdoc tag options for properties/methods/etc of exports */
memberTags?: JsDocTagOptions;
/** The jsdoc tag options for parameters of members/functions */
paramTags?: JsDocTagOptions;
}
export type DiagnosticSeverity = 'warn'|'error'|'none';
export function publicApi(fileName: string, options: SerializationOptions = {}): string {
return publicApiInternal(ts.createCompilerHost(baseTsOptions), fileName, baseTsOptions, options);
}
export function publicApiInternal(
host: ts.CompilerHost, fileName: string, tsOptions: ts.CompilerOptions,
options: SerializationOptions = {}): string {
// Since the entry point will be compared with the source files from the TypeScript program,
// the path needs to be normalized with forward slashes in order to work within Windows.
const entrypoint = path.normalize(fileName).replace(/\\/g, '/');
// Setup default tag options
options = {
...options,
exportTags: applyDefaultTagOptions(options.exportTags),
memberTags: applyDefaultTagOptions(options.memberTags),
paramTags: applyDefaultTagOptions(options.paramTags)
};
if (!entrypoint.match(/\.d\.ts$/)) {
throw new Error(`Source file "${fileName}" is not a declaration file`);
}
const program = ts.createProgram([entrypoint], tsOptions, host);
return new ResolvedDeclarationEmitter(program, entrypoint, options).emit();
}
interface Diagnostic {
type?: DiagnosticSeverity;
message: string;
}
class ResolvedDeclarationEmitter {
private program: ts.Program;
private fileName: string;
private typeChecker: ts.TypeChecker;
private options: SerializationOptions;
private diagnostics: Diagnostic[];
constructor(program: ts.Program, fileName: string, options: SerializationOptions) {
this.program = program;
this.fileName = fileName;
this.options = options;
this.diagnostics = [];
this.typeChecker = this.program.getTypeChecker();
}
emit(): string {
const sourceFile = this.program.getSourceFiles().find(sf => sf.fileName === this.fileName);
if (!sourceFile) {
throw new Error(`Source file "${this.fileName}" not found`);
}
let output: string[] = [];
const resolvedSymbols = this.getResolvedSymbols(sourceFile);
// Sort all symbols so that the output is more deterministic
resolvedSymbols.sort(symbolCompareFunction);
for (const symbol of resolvedSymbols) {
if (this.isExportPatternStripped(symbol.name)) {
continue;
}
const typeDecl = symbol.declarations && symbol.declarations[0];
const valDecl = symbol.valueDeclaration;
if (!typeDecl && !valDecl) {
this.diagnostics.push({
type: 'warn',
message: `${sourceFile.fileName}: error: No declaration found for symbol "${symbol.name}"`
});
continue;
}
typeDecl && this.emitDeclaration(symbol, typeDecl, output);
if (valDecl && typeDecl.kind === ts.SyntaxKind.InterfaceDeclaration) {
// Only generate value declarations in case of interfaces.
valDecl && this.emitDeclaration(symbol, valDecl, output);
}
}
if (this.diagnostics.length) {
const message = this.diagnostics.map(d => d.message).join('\n');
console.warn(message);
if (this.diagnostics.some(d => d.type === 'error')) {
throw new Error(message);
}
}
return output.join('');
}
emitDeclaration(symbol: ts.Symbol, decl: ts.Node, output: string[]) {
// The declaration node may not be a complete statement, e.g. for var/const
// symbols. We need to find the complete export statement by traversing
// upwards.
while (!hasModifier(decl, ts.SyntaxKind.ExportKeyword) && decl.parent) {
decl = decl.parent;
}
if (hasModifier(decl, ts.SyntaxKind.ExportKeyword)) {
// Make an empty line between two exports
if (output.length) {
output.push('\n');
}
const jsdocComment = this.processJsDocTags(decl, this.options.exportTags);
if (jsdocComment) {
output.push(jsdocComment + '\n');
}
output.push(stripEmptyLines(this.emitNode(decl)) + '\n');
} else {
// This may happen for symbols re-exported from external modules.
this.diagnostics.push({
type: 'warn',
message: createErrorMessage(decl, `No export declaration found for symbol "${symbol.name}"`)
});
}
}
private isExportPatternStripped(symbolName: string): boolean {
return [].concat(this.options.stripExportPattern).some(p => !!(p && symbolName.match(p)));
}
private getResolvedSymbols(sourceFile: ts.SourceFile): ts.Symbol[] {
const ms = (<any>sourceFile).symbol;
const rawSymbols = ms ? (this.typeChecker.getExportsOfModule(ms) || []) : [];
return rawSymbols.map(s => {
if (s.flags & ts.SymbolFlags.Alias) {
const resolvedSymbol = this.typeChecker.getAliasedSymbol(s);
// This will happen, e.g. for symbols re-exported from external modules.
if (!resolvedSymbol.valueDeclaration && !resolvedSymbol.declarations) {
return s;
}
if (resolvedSymbol.name !== s.name) {
if (this.isExportPatternStripped(s.name)) {
return s;
}
throw new Error(
`Symbol "${resolvedSymbol.name}" was aliased as "${s.name}". ` +
`Aliases are not supported.`);
}
return resolvedSymbol;
} else {
return s;
}
});
}
emitNode(node: ts.Node) {
if (hasModifier(node, ts.SyntaxKind.PrivateKeyword)) {
return '';
}
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 && 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);
if (!isAllowed) {
this.diagnostics.push({
type: 'error',
message: createErrorMessage(
firstQualifier,
`Module identifier "${firstQualifier.text}" is not allowed. Remove it ` +
`from source or allow it via --allowModuleIdentifiers.`)
});
}
}
let children: ts.Node[] = [];
if (ts.isFunctionDeclaration(node)) {
// Used ts.isFunctionDeclaration instead of node.kind because this is a type guard
const symbol = this.typeChecker.getSymbolAtLocation(node.name);
symbol.declarations.forEach(x => children = children.concat(x.getChildren()));
} else {
children = node.getChildren();
}
const sourceText = node.getSourceFile().text;
if (children.length) {
// Sort declarations under a class or an interface
if (node.kind === ts.SyntaxKind.SyntaxList) {
switch (node.parent && node.parent.kind) {
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.InterfaceDeclaration: {
// There can be multiple SyntaxLists under a class or an interface,
// since SyntaxList is just an arbitrary data structure generated
// by Node#getChildren(). We need to check that we are sorting the
// right list.
if (children.every(node => node.kind in memberDeclarationOrder)) {
children = children.slice();
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());
});
}
break;
}
}
}
let output: string = children.filter(x => x.kind !== ts.SyntaxKind.JSDocComment)
.map(n => this.emitNode(n))
.join('');
// Print stability annotation for fields and parmeters
if (ts.isParameter(node) || node.kind in memberDeclarationOrder) {
const tagOptions = ts.isParameter(node) ? this.options.paramTags : this.options.memberTags;
const jsdocComment = this.processJsDocTags(node, tagOptions);
if (jsdocComment) {
// Add the annotation after the leading whitespace
output = output.replace(/^(\r?\n\s*)/, `$1${jsdocComment} `);
}
}
return output;
} else {
const ranges = ts.getLeadingCommentRanges(sourceText, node.pos);
let tail = node.pos;
for (const range of ranges || []) {
if (range.end > tail) {
tail = range.end;
}
}
return sourceText.substring(tail, node.end);
}
}
private processJsDocTags(node: ts.Node, tagOptions: JsDocTagOptions) {
const jsDocTags = getJsDocTags(node);
const requireAtLeastOne = tagOptions.requireAtLeastOne;
const isMissingAnyRequiredTag = requireAtLeastOne != null && requireAtLeastOne.length > 0 &&
jsDocTags.every(tag => requireAtLeastOne.indexOf(tag) === -1);
if (isMissingAnyRequiredTag) {
this.diagnostics.push({
type: 'error',
message: createErrorMessage(
node,
'Required jsdoc tags - One of the tags: ' +
requireAtLeastOne.map(tag => `"@${tag}"`).join(', ') +
` - must exist on ${getName(node)}.`)
});
}
const bannedTagsFound =
tagOptions.banned.filter(bannedTag => jsDocTags.some(tag => tag === bannedTag));
if (bannedTagsFound.length) {
this.diagnostics.push({
type: 'error',
message: createErrorMessage(
node,
'Banned jsdoc tags - ' + bannedTagsFound.map(tag => `"@${tag}"`).join(', ') +
` - were found on ${getName(node)}.`)
});
}
const tagsToCopy =
jsDocTags.filter(tag => tagOptions.toCopy.some(tagToCopy => tag === tagToCopy));
if (tagsToCopy.length === 1) {
return `/** @${tagsToCopy[0]} */`;
} else if (tagsToCopy.length > 1) {
return '/**\n' + tagsToCopy.map(tag => ` * @${tag}`).join('\n') + ' */\n';
} else {
return '';
}
}
}
const tagRegex = /@(\w+)/g;
function getJsDocTags(node: ts.Node): string[] {
const sourceText = node.getSourceFile().text;
const trivia = sourceText.substr(node.pos, node.getLeadingTriviaWidth());
// We use a hash so that we don't collect duplicate jsdoc tags
// (e.g. if a property has a getter and setter with the same tag).
const jsdocTags: {[key: string]: boolean} = {};
let match: RegExpExecArray;
while (match = tagRegex.exec(trivia)) {
jsdocTags[match[1]] = true;
}
return Object.keys(jsdocTags);
}
function symbolCompareFunction(a: ts.Symbol, b: ts.Symbol) {
return a.name.localeCompare(b.name);
}
function compareFunction<T>(a: T, b: T) {
return a === b ? 0 : a > b ? 1 : -1;
}
const memberDeclarationOrder: {[key: number]: number} = {
[ts.SyntaxKind.PropertySignature]: 0,
[ts.SyntaxKind.PropertyDeclaration]: 0,
[ts.SyntaxKind.GetAccessor]: 0,
[ts.SyntaxKind.SetAccessor]: 0,
[ts.SyntaxKind.CallSignature]: 1,
[ts.SyntaxKind.Constructor]: 2,
[ts.SyntaxKind.ConstructSignature]: 2,
[ts.SyntaxKind.IndexSignature]: 3,
[ts.SyntaxKind.MethodSignature]: 4,
[ts.SyntaxKind.MethodDeclaration]: 4
};
function stripEmptyLines(text: string): string {
return text.split(/\r?\n/).filter(x => !!x.length).join('\n');
}
/**
* Returns the first qualifier if the input node is a dotted expression.
*/
function getFirstQualifier(node: ts.Node): ts.Identifier|null {
switch (node.kind) {
case ts.SyntaxKind.PropertyAccessExpression: {
// For expression position
let lhs = node;
do {
lhs = (<ts.PropertyAccessExpression>lhs).expression;
} while (lhs && lhs.kind !== ts.SyntaxKind.Identifier);
return <ts.Identifier>lhs;
}
case ts.SyntaxKind.TypeReference: {
// For type position
let lhs: ts.Node = (<ts.TypeReferenceNode>node).typeName;
do {
lhs = (<ts.QualifiedName>lhs).left;
} while (lhs && lhs.kind !== ts.SyntaxKind.Identifier);
return <ts.Identifier>lhs;
}
default:
return null;
}
}
function createErrorMessage(node: ts.Node, message: string): string {
const sourceFile = node.getSourceFile();
let position;
if (sourceFile) {
const {line, character} = sourceFile.getLineAndCharacterOfPosition(node.getStart());
position = `${sourceFile.fileName}(${line + 1},${character + 1})`;
} else {
position = '<unknown>';
}
return `${position}: error: ${message}`;
}
function hasModifier(node: ts.Node, modifierKind: ts.SyntaxKind): boolean {
return !!node.modifiers && node.modifiers.some(x => x.kind === modifierKind);
}
function applyDefaultTagOptions(tagOptions: JsDocTagOptions|undefined): JsDocTagOptions {
return {requireAtLeastOne: [], banned: [], toCopy: [], ...tagOptions};
}
function getName(node: any) {
return '`' + (node.name && node.name.text ? node.name.text : node.getText()) + '`';
}

View File

@ -1,53 +0,0 @@
{
"name": "ts-api-guardian",
"version": "0.6.0",
"description": "Guards the API of TypeScript libraries!",
"main": "lib/main.js",
"typings": "lib/main.d.ts",
"bin": {
"ts-api-guardian": "./bin/ts-api-guardian"
},
"directories": {
"test": "test"
},
"peerDependencies": {
"typescript": "~3.9.2"
},
"dependencies": {
"chalk": "^4.0.0",
"diff": "^5.0.0",
"minimist": "^1.2.0"
},
"devDependencies": {
"@types/diff": "^5.0.0",
"@types/jasmine": "^3.0.0",
"@types/minimist": "^1.2.0",
"@types/node": "^10.9.4",
"jasmine": "^3.1.0",
"source-map-support": "^0.5.9",
"typescript": "4.3.4"
},
"keywords": [
"typescript"
],
"contributors": [
"Alan Agius <alan.agius4@gmail.com> (https://github.com/alan-agius4/)",
"Alex Eagle <alexeagle@google.com> (https://angular.io/)",
"Martin Probst <martinprobst@google.com> (https://angular.io/)",
"Victor Savkin <vsavkin@google.com> (https://victorsavkin.com)",
"Igor Minar <iminar@google.com> (https://angular.io/)"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/angular/angular/issues"
},
"homepage": "https://github.com/angular/angular/tools/ts-api-guardian",
"repository": {
"type": "git",
"url": "https://github.com/angular/angular.git",
"directory": "tools/ts-api-guardian"
},
"publishConfig": {
"registry": "https://wombat-dressing-room.appspot.com"
}
}

View File

@ -1,20 +0,0 @@
/**
* @license
* Copyright Google LLC 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
*/
const path = require('path');
const {runfiles} = require('@bazel/runfiles');
// Change directories to the path of the ts-api-guardian source tree. We need to resolve an actual
// path of a tree because we want to determine the path to the directory that includes all
// test fixture runfiles. On Windows this is usually the original non-sandboxed disk location,
// otherwise this just refers to the runfile directory with all the proper symlinked files.
// NB: we resolve `test/fixtures/empty.ts` and then step up 3 folders so to ensure we resolve to the
// root of the source tree and not the output tree on Windows where there are no runfiles.
// TODO: remove the whole bootstrap file once the tests are Bazel and Windows compatible.
process.chdir(path.resolve(
runfiles.resolve('angular/tools/ts-api-guardian/test/fixtures/empty.ts'), '../../..'));

View File

@ -1,148 +0,0 @@
/**
* @license
* Copyright Google LLC 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 child_process from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import {assertFileEqual} from './helpers';
const BINARY_PATH = require.resolve('../ts-api-guardian/bin/ts-api-guardian');
describe('cli: e2e test', () => {
const outDir = path.join(process.env['TEST_TMPDIR'], 'tmp');
beforeEach(() => {
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir);
}
});
afterEach(() => {
fs.rmdirSync(outDir, {recursive: true});
});
it('should print usage without any argument', () => {
const {stderr} = execute([]);
expect(stderr).toMatch(/Usage/);
});
it('should show help message with --help', () => {
const {stdout} = execute(['--help']);
expect(stdout).toMatch(/Usage/);
});
it('should generate golden file with --out', () => {
const simpleFile = path.join(outDir, 'simple.d.ts');
const {status, stderr} = execute(['--out', simpleFile, 'test/fixtures/simple.d.ts']);
expect(status).toBe(0, stderr);
assertFileEqual(simpleFile, 'test/fixtures/simple_expected.d.ts');
});
it('should verify golden file with --verify and exit cleanly on no difference', () => {
const {stdout, status} =
execute(['--verify', 'test/fixtures/simple_expected.d.ts', 'test/fixtures/simple.d.ts']);
expect(stdout).toBe('');
expect(status).toBe(0);
});
it('should verify golden file with --verify and exit with error on difference', () => {
const {stdout, status} = execute(
['--verify', 'test/fixtures/verify_expected.d.ts', 'test/fixtures/verify_entrypoint.d.ts']);
expect(stdout).toBe(fs.readFileSync('test/fixtures/verify.patch').toString());
expect(status).toBe(1);
});
it('should generate multiple golden files with --outDir and --rootDir', () => {
const {status} = execute([
'--outDir', outDir, '--rootDir', 'test/fixtures', 'test/fixtures/simple.d.ts',
'test/fixtures/sorting.d.ts'
]);
expect(status).toBe(0);
assertFileEqual(path.join(outDir, 'simple.d.ts'), 'test/fixtures/simple_expected.d.ts');
assertFileEqual(path.join(outDir, 'sorting.d.ts'), 'test/fixtures/sorting_expected.d.ts');
});
it('should verify multiple golden files with --verifyDir and --rootDir', () => {
copyFile('test/fixtures/simple_expected.d.ts', path.join(outDir, 'simple.d.ts'));
copyFile('test/fixtures/sorting_expected.d.ts', path.join(outDir, 'sorting.d.ts'));
const {stdout, status} = execute([
'--verifyDir', outDir, '--rootDir', 'test/fixtures', 'test/fixtures/simple.d.ts',
'test/fixtures/sorting.d.ts'
]);
expect(stdout).toBe('');
expect(status).toBe(0);
});
it('should generate respecting --stripExportPattern', () => {
const {status} = execute([
'--out', path.join(outDir, 'underscored.d.ts'), '--stripExportPattern', '^__.*',
'test/fixtures/underscored.d.ts'
]);
expect(status).toBe(0);
assertFileEqual(
path.join(outDir, 'underscored.d.ts'), 'test/fixtures/underscored_expected.d.ts');
});
it('should not throw for aliased stripped exports', () => {
const {status} = execute([
'--out', path.join(outDir, 'stripped_alias.d.ts'), '--stripExportPattern', '^__.*',
'test/fixtures/stripped_alias.d.ts'
]);
expect(status).toBe(0);
assertFileEqual(
path.join(outDir, 'stripped_alias.d.ts'), 'test/fixtures/stripped_alias_expected.d.ts');
});
it('should verify respecting --stripExportPattern', () => {
const {stdout, status} = execute([
'--verify', 'test/fixtures/underscored_expected.d.ts', 'test/fixtures/underscored.d.ts',
'--stripExportPattern', '^__.*'
]);
expect(stdout).toBe('');
expect(status).toBe(0);
});
it('should respect --allowModuleIdentifiers', () => {
const {stdout, status} = execute([
'--verify', 'test/fixtures/module_identifier_expected.d.ts', '--allowModuleIdentifiers',
'foo', 'test/fixtures/module_identifier.d.ts'
]);
expect(stdout).toBe('');
expect(status).toBe(0);
});
});
function copyFile(sourceFile: string, targetFile: string) {
fs.writeFileSync(targetFile, fs.readFileSync(sourceFile));
}
function execute(args: string[]): {stdout: string, stderr: string, status: number} {
// We need to determine the directory that includes the `ts-api-guardian` npm_package that
// will be used to spawn the CLI binary. This is a workaround because technically we shouldn't
// spawn a child process that doesn't have the custom NodeJS module resolution for Bazel.
const nodePath = [
path.join(require.resolve('npm/node_modules/chalk/package.json'), '../../'),
path.join(require.resolve('../lib/cli.js'), '../../'),
].join(process.platform === 'win32' ? ';' : ':');
const output = child_process.spawnSync(process.execPath, [BINARY_PATH, ...args], {
env: {
'NODE_PATH': nodePath,
}
});
expect(output.error).toBeFalsy(`Child process failed or timed out: ${output.error}`);
expect(output.signal).toBeFalsy(`Child process killed by signal ${output.signal}`);
return {
stdout: output.stdout.toString(),
stderr: output.stderr.toString(),
status: output.status
};
}

View File

@ -1,81 +0,0 @@
/**
* @license
* Copyright Google LLC 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 {parseArguments} from '../lib/cli';
describe('cli: parseArguments', () => {
it('should show usage with error when supplied with no arguments', () => {
const {mode, errors} = parseArguments([]);
expect(mode).toBe('help');
expect(errors).toEqual(['No input file specified.']);
});
it('should show usage without error when supplied with --help', () => {
const {mode, errors} = parseArguments(['--help']);
expect(mode).toBe('help');
expect(errors).toEqual([]);
});
it('should show usage with error when supplied with none of --out/verify[Dir]', () => {
const {mode, errors} = parseArguments(['input.d.ts']);
expect(mode).toBe('help');
expect(errors).toEqual(['Specify either --out[Dir] or --verify[Dir]']);
});
it('should show usage with error when supplied with both of --out/verify[Dir]', () => {
const {mode, errors} =
parseArguments(['--out', 'out.d.ts', '--verifyDir', 'golden.d.ts', 'input.d.ts']);
expect(mode).toBe('help');
expect(errors).toEqual(['Specify either --out[Dir] or --verify[Dir]']);
});
it('should show usage with error when supplied without input file', () => {
const {mode, errors} = parseArguments(['--out', 'output.d.ts']);
expect(mode).toBe('help');
expect(errors).toEqual(['No input file specified.']);
});
it('should show usage with error when supplied without input file', () => {
const {mode, errors} = parseArguments(['--out', 'output.d.ts', 'first.d.ts', 'second.d.ts']);
expect(mode).toBe('help');
expect(errors).toEqual(['More than one input specified. Use --outDir instead.']);
});
it('should use out mode when supplied with --out', () => {
const {argv, mode, errors} = parseArguments(['--out', 'out.d.ts', 'input.d.ts']);
expect(argv['out']).toBe('out.d.ts');
expect(argv._).toEqual(['input.d.ts']);
expect(mode).toBe('out');
expect(errors).toEqual([]);
});
it('should use verify mode when supplied with --verify', () => {
const {argv, mode, errors} = parseArguments(['--verify', 'out.d.ts', 'input.d.ts']);
expect(argv['verify']).toBe('out.d.ts');
expect(argv._).toEqual(['input.d.ts']);
expect(mode).toBe('verify');
expect(errors).toEqual([]);
});
it('should show usage with error when supplied with --autoDiscoverEntrypoints without --baseDir',
() => {
const {mode, errors} =
parseArguments(['--autoDiscoverEntrypoints', '--outDir', 'something']);
expect(mode).toBe('help');
expect(errors).toEqual(['--rootDir must be provided with --autoDiscoverEntrypoints.']);
});
it('should show usage with error when supplied with --autoDiscoverEntrypoints without --outDir/verifyDir',
() => {
const {mode, errors} =
parseArguments(['--autoDiscoverEntrypoints', '--rootDir', 'something']);
expect(mode).toBe('help');
expect(errors).toEqual(
['--outDir or --verifyDir must be used with --autoDiscoverEntrypoints.']);
});
});

View File

@ -1,14 +0,0 @@
export declare class A {
field: string;
method(a: string): number;
}
export interface B {
field: A;
}
export declare class C {
private privateProp;
propWithDefault: number;
protected protectedProp: number;
someProp: string;
constructor(someProp: string, propWithDefault: number, privateProp: any, protectedProp: number);
}

View File

@ -1,15 +0,0 @@
export declare class A {
field: string;
method(a: string): number;
}
export interface B {
field: A;
}
export declare class C {
propWithDefault: number;
protected protectedProp: number;
someProp: string;
constructor(someProp: string, propWithDefault: number, privateProp: any, protectedProp: number);
}

View File

@ -1,7 +0,0 @@
/**
* @license
* Copyright Google LLC 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
*/

View File

@ -1,8 +0,0 @@
export declare enum Foo {
Alpha = 0,
Beta = 1,
}
export interface Bar {
field: Foo.Alpha,
}

View File

@ -1,8 +0,0 @@
export interface Bar {
field: Foo.Alpha,
}
export declare enum Foo {
Alpha = 0,
Beta = 1,
}

View File

@ -1,4 +0,0 @@
export interface TypeOnly { field: string; }
export interface TypeAndValue { field: string; }
export const TypeAndValue: Function;

View File

@ -1,5 +0,0 @@
export interface TypeAndValue { field: string; }
export const TypeAndValue: Function;
export interface TypeOnly { field: string; }

View File

@ -1,3 +0,0 @@
export declare type SimpleChanges<T = any> = {
[P in keyof T]?: any;
};

View File

@ -1,3 +0,0 @@
export declare type SimpleChanges<T = any> = {
[P in keyof T]?: any;
};

View File

@ -1,5 +0,0 @@
import * as foo from './somewhere';
/** @publicApi */
export declare class A extends foo.Bar {
}

View File

@ -1,2 +0,0 @@
export declare class A extends foo.Bar {
}

View File

@ -1 +0,0 @@
export { A, B } from './simple';

View File

@ -1 +0,0 @@
export { A as Apple } from './classes_and_interfaces';

View File

@ -1 +0,0 @@
export { A } from './classes_and_interfaces';

View File

@ -1,4 +0,0 @@
export declare class A {
field: string;
method(a: string): number;
}

View File

@ -1,3 +0,0 @@
export declare const A: string;
export declare var B: string;

View File

@ -1,5 +0,0 @@
/**
* We want to ensure that external modules are not resolved. Typescript happens
* to be conveniently available in our environment.
*/
export { CompilerHost } from 'typescript';

View File

@ -1 +0,0 @@
export * from './simple';

View File

@ -1,3 +0,0 @@
export declare const A: string;
export declare var B: string;

View File

@ -1,4 +0,0 @@
/** @publicApi */
export declare const A: string;
/** @publicApi */
export declare var B: string;

View File

@ -1,3 +0,0 @@
export declare const A: string;
export declare var B: string;

View File

@ -1,17 +0,0 @@
/** @publicApi */
export declare type E = string;
/** @publicApi */
export interface D {
e: number;
}
/** @publicApi */
export declare var e: C;
/** @publicApi */
export declare class C {
e: number;
d: string;
}
/** @publicApi */
export declare function b(): boolean;
/** @publicApi */
export declare const a: string;

View File

@ -1,16 +0,0 @@
export declare const a: string;
export declare function b(): boolean;
export declare class C {
d: string;
e: number;
}
export interface D {
e: number;
}
export declare var e: C;
export declare type E = string;

View File

@ -1,4 +0,0 @@
export {original_symbol as __private_symbol} from './stripped_alias_original';
/** @publicApi */
export class B {
}

View File

@ -1,2 +0,0 @@
export class B {
}

View File

@ -1 +0,0 @@
export let original_symbol: number;

View File

@ -1,7 +0,0 @@
export class UsesTypeLiterals {
a: number | undefined;
b: number | null;
c: number | true;
d: number | null | undefined;
e: Array<string | null | undefined>;
}

View File

@ -1,7 +0,0 @@
export class UsesTypeLiterals {
a: number | undefined;
b: number | null;
c: number | true;
d: number | null | undefined;
e: Array<string | null | undefined>;
}

View File

@ -1,4 +0,0 @@
export const __a__: string;
/** @publicApi */
export class B {
}

View File

@ -1,2 +0,0 @@
export class B {
}

View File

@ -1,11 +0,0 @@
--- test/fixtures/verify_expected.d.ts Golden file
+++ test/fixtures/verify_expected.d.ts Generated API
@@ -1,5 +1,6 @@
export interface A {
c: number;
- a(arg: any[]): any;
- b: string;
+ a(arg: any[]): {[name: string]: number};
}
+
+export declare const b: boolean;

View File

@ -1,6 +0,0 @@
/** @publicApi */
export interface A {
c: number;
a(arg: any[]): {[name: string]: number};
}
export { b } from './verify_submodule';

View File

@ -1,5 +0,0 @@
export interface A {
c: number;
a(arg: any[]): any;
b: string;
}

View File

@ -1,2 +0,0 @@
/** @publicApi */
export declare const b: boolean;

View File

@ -1,13 +0,0 @@
/**
* @license
* Copyright Google LLC 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 {readFileSync} from 'fs';
export function assertFileEqual(actualFile: string, expectedFile: string) {
expect(readFileSync(actualFile).toString()).toBe(readFileSync(expectedFile).toString());
}

View File

@ -1,151 +0,0 @@
/**
* @license
* Copyright Google LLC 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 fs from 'fs';
import * as path from 'path';
import * as main from '../lib/main';
import {assertFileEqual} from './helpers';
describe('integration test: public api', () => {
let _warn: any = null;
let warnings: string[] = [];
beforeEach(() => {
_warn = console.warn;
console.warn = (...args: string[]) => warnings.push(args.join(' '));
});
afterEach(() => {
console.warn = _warn;
warnings = [];
_warn = null;
});
it('should handle empty files', () => {
check('test/fixtures/empty.d.ts', 'test/fixtures/empty_expected.d.ts');
});
it('should include symbols', () => {
check('test/fixtures/simple.d.ts', 'test/fixtures/simple_expected.d.ts');
});
it('should include symbols reexported explicitly', () => {
check('test/fixtures/reexported.d.ts', 'test/fixtures/reexported_expected.d.ts');
});
it('should include symbols reexported with *', () => {
check('test/fixtures/reexported_star.d.ts', 'test/fixtures/reexported_star_expected.d.ts');
});
it('should include members of classes and interfaces', () => {
check(
'test/fixtures/classes_and_interfaces.d.ts',
'test/fixtures/classes_and_interfaces_expected.d.ts');
});
it('should include value and type', () => {
check(
'test/fixtures/exports_type_and_value.d.ts',
'test/fixtures/exports_type_and_value_expected.d.ts');
});
it('should include members reexported classes', () => {
check(
'test/fixtures/reexported_classes.d.ts', 'test/fixtures/reexported_classes_expected.d.ts');
});
it('should remove reexported external symbols', () => {
check('test/fixtures/reexported_extern.d.ts', 'test/fixtures/reexported_extern_expected.d.ts');
expect(warnings).toEqual([
'test/fixtures/reexported_extern.d.ts(5,1): error: No export declaration found for symbol "CompilerHost"'
]);
});
it('should support type literals', () => {
check('test/fixtures/type_literals.d.ts', 'test/fixtures/type_literals_expected.d.ts');
});
it('should allow enums as types', () => {
check('test/fixtures/enum_as_type.d.ts', 'test/fixtures/enum_as_type_expected.d.ts');
});
it('should throw on passing a .ts file as an input', () => {
expect(() => main.publicApi('test/fixtures/empty.ts'))
.toThrowError('Source file "test/fixtures/empty.ts" is not a declaration file');
});
it('should respect serialization options', () => {
check(
'test/fixtures/underscored.d.ts', 'test/fixtures/underscored_expected.d.ts',
{stripExportPattern: /^__.*/});
});
});
describe('integration test: generateGoldenFile', () => {
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');
beforeEach(() => {
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir);
}
});
afterEach(() => {
fs.rmdirSync(outDir, {recursive: true});
});
it('should generate a golden file', () => {
main.generateGoldenFile('test/fixtures/reexported_classes.d.ts', outFile);
assertFileEqual(outFile, 'test/fixtures/reexported_classes_expected.d.ts');
});
it('should generate a golden file with any ancestor directory created', () => {
main.generateGoldenFile('test/fixtures/reexported_classes.d.ts', deepOutFile);
assertFileEqual(deepOutFile, 'test/fixtures/reexported_classes_expected.d.ts');
});
it('should respect serialization options', () => {
main.generateGoldenFile(
'test/fixtures/underscored.d.ts', outFile, {stripExportPattern: /^__.*/});
assertFileEqual(outFile, 'test/fixtures/underscored_expected.d.ts');
});
it('should generate a golden file with keyof', () => {
main.generateGoldenFile('test/fixtures/keyof.d.ts', outFile);
assertFileEqual(outFile, 'test/fixtures/keyof_expected.d.ts');
});
});
describe('integration test: verifyAgainstGoldenFile', () => {
it('should check an entrypoint against a golden file on equal', () => {
const diff = main.verifyAgainstGoldenFile(
'test/fixtures/reexported_classes.d.ts', 'test/fixtures/reexported_classes_expected.d.ts');
expect(diff).toBe('');
});
it('should check an entrypoint against a golden file with proper diff message', () => {
const diff = main.verifyAgainstGoldenFile(
'test/fixtures/verify_entrypoint.d.ts', 'test/fixtures/verify_expected.d.ts');
expect(diff).toBe(fs.readFileSync('test/fixtures/verify.patch').toString());
});
it('should respect serialization options', () => {
const diff = main.verifyAgainstGoldenFile(
'test/fixtures/underscored.d.ts', 'test/fixtures/underscored_expected.d.ts',
{stripExportPattern: /^__.*/});
expect(diff).toBe('');
});
});
function check(sourceFile: string, expectedFile: string, options: main.SerializationOptions = {}) {
expect(main.publicApi(sourceFile, options)).toBe(fs.readFileSync(expectedFile).toString());
}

View File

@ -1,655 +0,0 @@
/**
* @license
* Copyright Google LLC 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 ts from 'typescript';
import {publicApiInternal, SerializationOptions} from '../lib/serializer';
const classesAndInterfaces = `
export declare class A {
field: string;
method(a: string): number;
}
export interface B {
field: A;
}
export declare class C {
someProp: string;
propWithDefault: number;
private privateProp;
protected protectedProp: number;
constructor(someProp: string, propWithDefault: number, privateProp: any, protectedProp: number);
}
`;
describe('unit test', () => {
let _warn: any = null;
let warnings: string[] = [];
beforeEach(() => {
_warn = console.warn;
console.warn = (...args: string[]) => warnings.push(args.join(' '));
});
afterEach(() => {
console.warn = _warn;
warnings = [];
_warn = null;
});
it('should ignore private methods', () => {
const input = `
export declare class A {
fa(): void;
protected fb(): void;
private fc();
}
`;
const expected = `
export declare class A {
fa(): void;
protected fb(): void;
}
`;
check({'file.d.ts': input}, expected);
});
it('should support overloads functions', () => {
const input = `
export declare function group(steps: AnimationMetadata[], options?: AnimationOptions | null): AnimationGroupMetadata;
export declare function registerLocaleData(data: any, extraData?: any): void;
export declare function registerLocaleData(data: any, localeId?: string, extraData?: any): void;
`;
const expected = `
export declare function group(steps: AnimationMetadata[], options?: AnimationOptions | null): AnimationGroupMetadata;
export declare function registerLocaleData(data: any, extraData?: any): void;
export declare function registerLocaleData(data: any, localeId?: string, extraData?: any): void;
`;
check({'file.d.ts': input}, expected);
});
it('should ignore private props', () => {
const input = `
export declare class A {
fa: any;
protected fb: any;
private fc;
}
`;
const expected = `
export declare class A {
fa: any;
protected fb: any;
}
`;
check({'file.d.ts': input}, expected);
});
it('should support imports without capturing imports', () => {
const input = `
import {A} from './classes_and_interfaces';
export declare class C {
field: A;
}
`;
const expected = `
export declare class C {
field: A;
}
`;
check({'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input}, expected);
});
it('should throw on aliased reexports', () => {
const input = `
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.');
});
it('should remove reexported external symbols', () => {
const input = `
export { Foo } from 'some-external-module-that-cannot-be-resolved';
`;
const expected = `
`;
check({'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input}, expected);
expect(warnings).toEqual(
['file.d.ts(1,1): error: No export declaration found for symbol "Foo"']);
});
it('should sort exports', () => {
const input = `
export declare type E = string;
export interface D {
e: number;
}
export declare var e: C;
export declare class C {
e: number;
d: string;
}
export declare function b(): boolean;
export declare const a: string;
`;
const expected = `
export declare const a: string;
export declare function b(): boolean;
export declare class C {
d: string;
e: number;
}
export interface D {
e: number;
}
export declare var e: C;
export declare type E = string;
`;
check({'file.d.ts': input}, expected);
});
it('should sort class members', () => {
const input = `
export class A {
f: number;
static foo(): void;
c: string;
static a: boolean;
constructor();
static bar(): void;
}
`;
const expected = `
export class A {
c: string;
f: number;
constructor();
static a: boolean;
static bar(): void;
static foo(): void;
}
`;
check({'file.d.ts': input}, expected);
});
it('should sort interface members', () => {
const input = `
export interface A {
(): void;
[key: string]: any;
c(): void;
a: number;
new (): Object;
}
`;
const expected = `
export interface A {
a: number;
(): void;
new (): Object;
[key: string]: any;
c(): void;
}
`;
check({'file.d.ts': input}, expected);
});
it('should sort class members including readonly', () => {
const input = `
export declare class DebugNode {
private _debugContext;
nativeNode: any;
listeners: any[];
parent: any | null;
constructor(nativeNode: any, parent: DebugNode | null, _debugContext: any);
readonly injector: any;
readonly componentInstance: any;
readonly context: any;
readonly references: {
[key: string]: any;
};
readonly providerTokens: any[];
}
`;
const expected = `
export declare class DebugNode {
readonly componentInstance: any;
readonly context: any;
readonly injector: any;
listeners: any[];
nativeNode: any;
parent: any | null;
readonly providerTokens: any[];
readonly references: {
[key: string]: any;
};
constructor(nativeNode: any, parent: DebugNode | null, _debugContext: any);
}
`;
check({'file.d.ts': input}, expected);
});
it('should sort two call signatures', () => {
const input = `
export interface A {
(b: number): void;
(a: number): void;
}
`;
const expected = `
export interface A {
(a: number): void;
(b: number): void;
}
`;
check({'file.d.ts': input}, expected);
});
it('should sort exports including re-exports', () => {
const submodule = `
export declare var e: C;
export declare class C {
e: number;
d: string;
}
`;
const input = `
export * from './submodule';
export declare type E = string;
export interface D {
e: number;
}
export declare function b(): boolean;
export declare const a: string;
`;
const expected = `
export declare const a: string;
export declare function b(): boolean;
export declare class C {
d: string;
e: number;
}
export interface D {
e: number;
}
export declare var e: C;
export declare type E = string;
`;
check({'submodule.d.ts': submodule, 'file.d.ts': input}, expected);
});
it('should remove module comments', () => {
const input = `
/**
* An amazing module.
* @module
*/
/**
* Foo function.
*/
export declare function foo(): boolean;
export declare const bar: number;
`;
const expected = `
export declare const bar: number;
export declare function foo(): boolean;
`;
check({'file.d.ts': input}, expected);
});
it('should remove class and field comments', () => {
const input = `
/**
* Does something really cool.
*/
export declare class A {
/**
* A very interesting getter.
*/
b: string;
/**
* A very useful field.
*/
name: string;
}
`;
const expected = `
export declare class A {
b: string;
name: string;
}
`;
check({'file.d.ts': input}, expected);
});
it('should skip symbols matching specified pattern', () => {
const input = `
export const __a__: string;
export class B {
}
`;
const expected = `
export class B {
}
`;
check({'file.d.ts': input}, expected, {stripExportPattern: /^__.*/});
});
it('should throw on using module imports in expression position that were not explicitly allowed',
() => {
const input = `
import * as foo from './foo';
export declare class A extends foo.A {
}
`;
checkThrows(
{'file.d.ts': input},
'file.d.ts(2,32): error: Module identifier "foo" is not allowed. ' +
'Remove it from source or allow it via --allowModuleIdentifiers.');
});
it('should throw on using module imports in type position that were not explicitly allowed',
() => {
const input = `
import * as foo from './foo';
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 allow it via --allowModuleIdentifiers.');
});
it('should not throw on using explicitly allowed module imports', () => {
const input = `
import * as foo from './foo';
export declare class A extends foo.A {
}
`;
const expected = `
export declare class A extends foo.A {
}
`;
check({'file.d.ts': input}, expected, {allowModuleIdentifiers: ['foo']});
});
it('should not throw if module imports, that were not explicitly allowed, are not used', () => {
const input = `
import * as foo from './foo';
export declare class A {
}
`;
const expected = `
export declare class A {
}
`;
check({'file.d.ts': input}, expected);
});
it('should copy specified jsdoc tags of exports in docstrings', () => {
const input = `
/**
* @deprecated This is useless now
*/
export declare class A {
}
/**
* @experimental
*/
export declare const b: string;
/**
* @stable
*/
export declare var c: number;
`;
const expected = `
/** @deprecated */
export declare class A {
}
/** @experimental */
export declare const b: string;
export declare var c: number;
`;
check({'file.d.ts': input}, expected, {exportTags: {toCopy: ['deprecated', 'experimental']}});
});
it('should copy specified jsdoc tags of fields in docstrings', () => {
const input = `
/** @otherTag */
export declare class A {
/**
* @stable
*/
value: number;
/**
* @experimental
* @otherTag
*/
constructor();
/**
* @deprecated
*/
foo(): void;
}
`;
const expected = `
export declare class A {
value: number;
/** @experimental */ constructor();
/** @deprecated */ foo(): void;
}
`;
check({'file.d.ts': input}, expected, {memberTags: {toCopy: ['deprecated', 'experimental']}});
});
it('should copy specified jsdoc tags of parameters in docstrings', () => {
const input = `
export declare class A {
foo(str: string, /** @deprecated */ value: number): void;
}
`;
const expected = `
export declare class A {
foo(str: string, /** @deprecated */ value: number): void;
}
`;
check({'file.d.ts': input}, expected, {paramTags: {toCopy: ['deprecated', 'experimental']}});
});
it('should throw on using banned jsdoc tags on exports', () => {
const input = `
/**
* @stable
*/
export declare class A {
value: number;
}
`;
checkThrows(
{'file.d.ts': input},
'file.d.ts(4,1): error: Banned jsdoc tags - "@stable" - were found on `A`.',
{exportTags: {banned: ['stable']}});
});
it('should throw on using banned jsdoc tags on fields', () => {
const input = `
export declare class A {
/**
* @stable
*/
value: number;
}
`;
checkThrows(
{'file.d.ts': input},
'file.d.ts(5,3): error: Banned jsdoc tags - "@stable" - were found on `value`.',
{memberTags: {banned: ['stable']}});
});
it('should throw on using banned jsdoc tags on parameters', () => {
const input = `
export declare class A {
foo(/** @stable */ param: number): void;
}
`;
checkThrows(
{'file.d.ts': input},
'file.d.ts(2,22): error: Banned jsdoc tags - "@stable" - were found on `param`.',
{paramTags: {banned: ['stable']}});
});
it('should throw on missing required jsdoc tags on exports', () => {
const input = `
/** @experimental */
export declare class A {
value: number;
}
`;
checkThrows(
{'file.d.ts': input},
'file.d.ts(2,1): error: Required jsdoc tags - One of the tags: "@stable" - must exist on `A`.',
{exportTags: {requireAtLeastOne: ['stable']}});
});
it('should throw on missing required jsdoc tags on fields', () => {
const input = `
/** @experimental */
export declare class A {
value: number;
}
`;
checkThrows(
{'file.d.ts': input},
'file.d.ts(3,3): error: Required jsdoc tags - One of the tags: "@stable" - must exist on `value`.',
{memberTags: {requireAtLeastOne: ['stable']}});
});
it('should throw on missing required jsdoc tags on parameters', () => {
const input = `
/** @experimental */
export declare class A {
foo(param: number): void;
}
`;
checkThrows(
{'file.d.ts': input},
'file.d.ts(3,7): error: Required jsdoc tags - One of the tags: "@stable" - must exist on `param`.',
{paramTags: {requireAtLeastOne: ['stable']}});
});
it('should require at least one of the requireAtLeastOne tags', () => {
const input = `
/** @experimental */
export declare class A {
foo(param: number): void;
}
`;
checkThrows(
{'file.d.ts': input},
'file.d.ts(3,7): error: Required jsdoc tags - One of the tags: "@stable", "@foo", "@bar" - must exist on `param`.',
{paramTags: {requireAtLeastOne: ['stable', 'foo', 'bar']}});
});
it('should allow with one of the requireAtLeastOne tags found', () => {
const input = `
/**
* @foo
* @bar
* @stable
*/
export declare class A {
}
/**
* @foo
*/
export declare const b: string;
/**
* @bar
*/
export declare var c: number;
/**
* @stable
*/
export declare function d(): void;
`;
const expected = `
export declare class A {
}
export declare const b: string;
export declare var c: number;
export declare function d(): void;
`;
check(
{'file.d.ts': input}, expected,
{exportTags: {requireAtLeastOne: ['stable', 'foo', 'bar']}});
});
});
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);
},
writeFile: (name, text, writeByteOrderMark) => {},
fileExists: (filename) => !!files[filename],
readFile: (filename) => stripExtraIndentation(files[filename]),
getDefaultLibFileName: () => 'lib.ts',
useCaseSensitiveFileNames: () => true,
getCanonicalFileName: (filename) => filename,
getCurrentDirectory: () => './',
getNewLine: () => '\n',
getDirectories: () => []
};
}
function check(
files: {[name: string]: string}, expected: string, options: SerializationOptions = {}) {
const actual = publicApiInternal(getMockHost(files), 'file.d.ts', {}, options);
expect(actual.trim()).toBe(stripExtraIndentation(expected).trim());
}
function checkThrows(
files: {[name: string]: string}, error: string, options: SerializationOptions = {}) {
expect(() => publicApiInternal(getMockHost(files), 'file.d.ts', {}, options)).toThrowError(error);
}
function stripExtraIndentation(text: string) {
let lines = text.split('\n');
// 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;
// Ignore empty line
return line.length ? Math.min(min, indent) : min;
}, text.length);
return lines.map(line => line.substr(commonIndent)).join('\n') + '\n';
}

View File

@ -26,7 +26,6 @@
"exclude": [
"testing",
"node_modules",
"ts-api-guardian",
"typings-test",
"public_api_guard",
"docs"

View File

@ -116,7 +116,6 @@
// Ignore test files
"./packages/compiler-cli/test/compliance/test_cases/**/*",
"./packages/localize/**/test_files/**/*",
"./tools/ts-api-guardian/test/fixtures/**/*",
"./tools/public_api_guard/**/*.d.ts",
"./modules/benchmarks_external/**/*",
// Ignore zone.js directory