build: ensure that there are no invalid dynamic imports (#14619)

**Note**: In a follow-up I want to wire up the release output validations as part of a CircleCI job. It just makes sense to keep all of these release output validations in the same place.

References https://github.com/angular/material2/issues/12877
This commit is contained in:
Paul Gschwendtner
2019-01-03 00:49:15 +01:00
committed by mmalerba
parent 4acff50ee7
commit 452c6d391a
2 changed files with 60 additions and 4 deletions
+17 -2
View File
@@ -2,11 +2,18 @@ import {bold, red, yellow} from 'chalk';
import {existsSync} from 'fs';
import {sync as glob} from 'glob';
import {join} from 'path';
import {checkMaterialPackage, checkReleaseBundle} from './output-validations';
import {
checkMaterialPackage,
checkReleaseBundle,
checkTypeDefinitionFile
} from './output-validations';
/** Glob that matches all JavaScript bundle files within a release package. */
const releaseBundlesGlob = '+(esm5|esm2015|bundles)/*.js';
/** Glob that matches all TypeScript definition files within a release package. */
const releaseTypeDefinitionsGlob = '**/*.d.ts';
/**
* Type that describes a map of package failures. The keys are failure messages and
* their value is an array of specifically affected files.
@@ -21,12 +28,14 @@ type PackageFailures = Map<string, string[]>;
*/
export function checkReleasePackage(releasesPath: string, packageName: string): boolean {
const packagePath = join(releasesPath, packageName);
const bundlePaths = glob(releaseBundlesGlob, {cwd: packagePath, absolute: true});
const failures = new Map() as PackageFailures;
const addFailure = (message, filePath?) => {
failures.set(message, (failures.get(message) || []).concat(filePath));
};
const bundlePaths = glob(releaseBundlesGlob, {cwd: packagePath, absolute: true});
const typeDefinitions = glob(releaseTypeDefinitionsGlob, {cwd: packagePath, absolute: true});
// We want to walk through each bundle within the current package and run
// release validations that ensure that the bundles are not invalid.
bundlePaths.forEach(bundlePath => {
@@ -34,6 +43,12 @@ export function checkReleasePackage(releasesPath: string, packageName: string):
.forEach(message => addFailure(message, bundlePath));
});
// Run output validations for all TypeScript definition files within the release output.
typeDefinitions.forEach(filePath => {
checkTypeDefinitionFile(filePath)
.forEach(message => addFailure(message, filePath));
});
// Special release validation checks for the "material" release package.
if (packageName === 'material') {
checkMaterialPackage(join(releasesPath, packageName))
@@ -1,6 +1,7 @@
import {existsSync, readFileSync} from 'fs';
import {sync as glob} from 'glob';
import {join} from 'path';
import {dirname, isAbsolute, join} from 'path';
import * as ts from 'typescript';
/** RegExp that matches Angular component inline styles that contain a sourcemap reference. */
const inlineStylesSourcemapRegex = /styles: ?\[["'].*sourceMappingURL=.*["']/;
@@ -14,7 +15,7 @@ const externalReferencesRegex = /(templateUrl|styleUrls): *["'[]/;
*/
export function checkReleaseBundle(bundlePath: string): string[] {
const bundleContent = readFileSync(bundlePath, 'utf8');
let failures: string[] = [];
const failures: string[] = [];
if (inlineStylesSourcemapRegex.exec(bundleContent) !== null) {
failures.push('Found sourcemap references in component styles.');
@@ -27,6 +28,46 @@ export function checkReleaseBundle(bundlePath: string): string[] {
return failures;
}
/**
* Checks the specified TypeScript definition file by ensuring it does not contain invalid
* dynamic import statements. There can be invalid type imports paths because we compose the
* release package by moving things in a desired output structure. See Angular package format
* specification and https://github.com/angular/material2/pull/12876
*/
export function checkTypeDefinitionFile(filePath: string): string[] {
const baseDir = dirname(filePath);
const fileContent = readFileSync(filePath, 'utf8');
const failures = [];
const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true);
const nodeQueue = [...sourceFile.getChildren()];
while (nodeQueue.length) {
const node = nodeQueue.shift()!;
// Check all dynamic type imports and ensure that the import path is valid within the release
// output. Note that we don't want to enforce that there are no dynamic type imports because
// type inference is heavily used within the schematics and is useful in some situations.
if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument) &&
ts.isStringLiteral(node.argument.literal)) {
const importPath = node.argument.literal.text;
// In case the type import path starts with a dot, we know that this is a relative path
// and can ensure that the target path exists. Note that we cannot completely rely on
// "isAbsolute" because dynamic imports can also import from modules (e.g. "my-npm-module")
if (importPath.startsWith('.') && !existsSync(join(baseDir, `${importPath}.d.ts`))) {
failures.push('Found relative type imports which do not exist.');
} else if (isAbsolute(importPath)) {
failures.push('Found absolute type imports in definition file.');
}
}
nodeQueue.push(...node.getChildren());
}
return failures;
}
/**
* Checks the Angular Material release package and ensures that prebuilt themes
* and the theming bundle are built properly.