Skip to content

Commit 452c6d3

Browse files
devversionmmalerba
authored andcommitted
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 #12877
1 parent 4acff50 commit 452c6d3

File tree

2 files changed

+60
-4
lines changed

2 files changed

+60
-4
lines changed

tools/release/release-output/check-packages.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ import {bold, red, yellow} from 'chalk';
22
import {existsSync} from 'fs';
33
import {sync as glob} from 'glob';
44
import {join} from 'path';
5-
import {checkMaterialPackage, checkReleaseBundle} from './output-validations';
5+
import {
6+
checkMaterialPackage,
7+
checkReleaseBundle,
8+
checkTypeDefinitionFile
9+
} from './output-validations';
610

711
/** Glob that matches all JavaScript bundle files within a release package. */
812
const releaseBundlesGlob = '+(esm5|esm2015|bundles)/*.js';
913

14+
/** Glob that matches all TypeScript definition files within a release package. */
15+
const releaseTypeDefinitionsGlob = '**/*.d.ts';
16+
1017
/**
1118
* Type that describes a map of package failures. The keys are failure messages and
1219
* their value is an array of specifically affected files.
@@ -21,19 +28,27 @@ type PackageFailures = Map<string, string[]>;
2128
*/
2229
export function checkReleasePackage(releasesPath: string, packageName: string): boolean {
2330
const packagePath = join(releasesPath, packageName);
24-
const bundlePaths = glob(releaseBundlesGlob, {cwd: packagePath, absolute: true});
2531
const failures = new Map() as PackageFailures;
2632
const addFailure = (message, filePath?) => {
2733
failures.set(message, (failures.get(message) || []).concat(filePath));
2834
};
2935

36+
const bundlePaths = glob(releaseBundlesGlob, {cwd: packagePath, absolute: true});
37+
const typeDefinitions = glob(releaseTypeDefinitionsGlob, {cwd: packagePath, absolute: true});
38+
3039
// We want to walk through each bundle within the current package and run
3140
// release validations that ensure that the bundles are not invalid.
3241
bundlePaths.forEach(bundlePath => {
3342
checkReleaseBundle(bundlePath)
3443
.forEach(message => addFailure(message, bundlePath));
3544
});
3645

46+
// Run output validations for all TypeScript definition files within the release output.
47+
typeDefinitions.forEach(filePath => {
48+
checkTypeDefinitionFile(filePath)
49+
.forEach(message => addFailure(message, filePath));
50+
});
51+
3752
// Special release validation checks for the "material" release package.
3853
if (packageName === 'material') {
3954
checkMaterialPackage(join(releasesPath, packageName))

tools/release/release-output/output-validations.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {existsSync, readFileSync} from 'fs';
22
import {sync as glob} from 'glob';
3-
import {join} from 'path';
3+
import {dirname, isAbsolute, join} from 'path';
4+
import * as ts from 'typescript';
45

56
/** RegExp that matches Angular component inline styles that contain a sourcemap reference. */
67
const inlineStylesSourcemapRegex = /styles: ?\[["'].*sourceMappingURL=.*["']/;
@@ -14,7 +15,7 @@ const externalReferencesRegex = /(templateUrl|styleUrls): *["'[]/;
1415
*/
1516
export function checkReleaseBundle(bundlePath: string): string[] {
1617
const bundleContent = readFileSync(bundlePath, 'utf8');
17-
let failures: string[] = [];
18+
const failures: string[] = [];
1819

1920
if (inlineStylesSourcemapRegex.exec(bundleContent) !== null) {
2021
failures.push('Found sourcemap references in component styles.');
@@ -27,6 +28,46 @@ export function checkReleaseBundle(bundlePath: string): string[] {
2728
return failures;
2829
}
2930

31+
/**
32+
* Checks the specified TypeScript definition file by ensuring it does not contain invalid
33+
* dynamic import statements. There can be invalid type imports paths because we compose the
34+
* release package by moving things in a desired output structure. See Angular package format
35+
* specification and https://github.com/angular/material2/pull/12876
36+
*/
37+
export function checkTypeDefinitionFile(filePath: string): string[] {
38+
const baseDir = dirname(filePath);
39+
const fileContent = readFileSync(filePath, 'utf8');
40+
const failures = [];
41+
42+
const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true);
43+
const nodeQueue = [...sourceFile.getChildren()];
44+
45+
while (nodeQueue.length) {
46+
const node = nodeQueue.shift()!;
47+
48+
// Check all dynamic type imports and ensure that the import path is valid within the release
49+
// output. Note that we don't want to enforce that there are no dynamic type imports because
50+
// type inference is heavily used within the schematics and is useful in some situations.
51+
if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument) &&
52+
ts.isStringLiteral(node.argument.literal)) {
53+
const importPath = node.argument.literal.text;
54+
55+
// In case the type import path starts with a dot, we know that this is a relative path
56+
// and can ensure that the target path exists. Note that we cannot completely rely on
57+
// "isAbsolute" because dynamic imports can also import from modules (e.g. "my-npm-module")
58+
if (importPath.startsWith('.') && !existsSync(join(baseDir, `${importPath}.d.ts`))) {
59+
failures.push('Found relative type imports which do not exist.');
60+
} else if (isAbsolute(importPath)) {
61+
failures.push('Found absolute type imports in definition file.');
62+
}
63+
}
64+
65+
nodeQueue.push(...node.getChildren());
66+
}
67+
68+
return failures;
69+
}
70+
3071
/**
3172
* Checks the Angular Material release package and ensures that prebuilt themes
3273
* and the theming bundle are built properly.

0 commit comments

Comments
 (0)