Skip to content

Commit a8cff1e

Browse files
devversionjelbourn
authored andcommitted
build: add lint rule to avoid relative cross entry-point imports/exports (#17741)
Adds a new lint rule to avoid relative cross entry-point imports and exports. If a public entry-point imports from another entry-point it should use the public module name in order to comply with the Angular Package format, and to avoid unnecessary code inlining when creating flat entry-point bundles. Restructures the cdk testing fake-event code into `cdk/testing/testbed` since that is where the fake event code is primarily used. This makes the new lint rule happy, and also improves the public API of `cdk/testing` since interfaces like `ElementDimensions` are part of the `TestElement` class and should therefore be exported.
1 parent e02bb82 commit a8cff1e

20 files changed

+134
-33
lines changed

src/cdk/testing/private/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ ts_library(
1010
),
1111
module_name = "@angular/cdk/testing/private",
1212
deps = [
13-
"//src/cdk/testing",
13+
"//src/cdk/testing/testbed",
1414
"@npm//@angular/core",
1515
"@npm//@types/jasmine",
1616
],

src/cdk/testing/private/public-api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
export * from '../fake-events'; // Re-exported for convenince.
109
export * from './expect-async-error';
1110
export * from './wrapped-error-message';
1211
export * from './mock-ng-zone';
1312
export * from './text-dedent';
13+
14+
// Re-exported for convenience.
15+
export * from '../testbed/fake-events';

src/cdk/testing/protractor/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ ts_library(
1111
module_name = "@angular/cdk/testing/protractor",
1212
deps = [
1313
"//src/cdk/testing",
14-
"//src/cdk/testing/private",
1514
"@npm//protractor",
1615
],
1716
)

src/cdk/testing/protractor/protractor-element.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ElementDimensions, ModifierKeys, TestElement, TestKey} from '@angular/cdk/testing';
910
import {browser, ElementFinder, Key} from 'protractor';
10-
import {ElementDimensions} from '../element-dimensions';
11-
import {TestElement, TestKey} from '../test-element';
12-
import {ModifierKeys} from '../fake-events';
1311

1412
/** Maps the `TestKey` constants to Protractor's `Key` constants. */
1513
const keyMap = {

src/cdk/testing/protractor/protractor-harness-environment.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {HarnessEnvironment} from '@angular/cdk/testing';
9+
import {HarnessEnvironment, HarnessLoader, TestElement} from '@angular/cdk/testing';
1010
import {by, element as protractorElement, ElementFinder} from 'protractor';
11-
import {HarnessLoader} from '../component-harness';
12-
import {TestElement} from '../test-element';
1311
import {ProtractorElement} from './protractor-element';
1412

1513
/** A `HarnessEnvironment` implementation for Protractor. */

src/cdk/testing/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
export * from './component-harness';
1010
export * from './harness-environment';
1111
export * from './test-element';
12+
export * from './element-dimensions';

src/cdk/testing/test-element.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77
*/
88

99
import {ElementDimensions} from './element-dimensions';
10-
import {ModifierKeys} from './fake-events';
10+
11+
/** Modifier keys that may be held while typing. */
12+
export interface ModifierKeys {
13+
control?: boolean;
14+
alt?: boolean;
15+
shift?: boolean;
16+
meta?: boolean;
17+
}
1118

1219
/** An enum of non-text keys that can be used with the `sendKeys` method. */
1320
// NOTE: This is a separate enum from `@angular/cdk/keycodes` because we don't necessarily want to

src/cdk/testing/testbed/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ ts_library(
1212
deps = [
1313
"//src/cdk/keycodes",
1414
"//src/cdk/testing",
15-
"//src/cdk/testing/private",
1615
"@npm//@angular/core",
1716
"@npm//rxjs",
1817
],

src/cdk/testing/fake-events/dispatch-events.ts renamed to src/cdk/testing/testbed/fake-events/dispatch-events.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ModifierKeys} from '@angular/cdk/testing';
910
import {
1011
createFakeEvent,
1112
createKeyboardEvent,
1213
createMouseEvent,
1314
createTouchEvent,
14-
ModifierKeys
1515
} from './event-objects';
1616

1717
/**

src/cdk/testing/fake-events/event-objects.ts renamed to src/cdk/testing/testbed/fake-events/event-objects.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
/** Modifier keys that may be held while typing. */
10-
export interface ModifierKeys {
11-
control?: boolean;
12-
alt?: boolean;
13-
shift?: boolean;
14-
meta?: boolean;
15-
}
9+
import {ModifierKeys} from '@angular/cdk/testing';
1610

1711
/**
1812
* Creates a browser MouseEvent with the specified options.

src/cdk/testing/fake-events/index.ts renamed to src/cdk/testing/testbed/fake-events/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
// These are private APIs that are used both by the public APIs inside of this package, as well as
10-
// in the unit tests of other packages, hence why we need to re-export them through here.
9+
// These are private APIs that are used both by the public APIs inside of this package, as well
10+
// as in unit tests of other entry-points, hence why we need to re-export them through here.
1111
export * from './dispatch-events';
1212
export * from './event-objects';
1313
export * from './element-focus';

src/cdk/testing/fake-events/type-in-element.ts renamed to src/cdk/testing/testbed/fake-events/type-in-element.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ModifierKeys} from '@angular/cdk/testing';
910
import {dispatchFakeEvent, dispatchKeyboardEvent} from './dispatch-events';
1011
import {triggerFocus} from './element-focus';
11-
import {ModifierKeys} from './event-objects';
1212

1313
/**
1414
* Checks whether the given Element is a text input element.

src/cdk/testing/testbed/testbed-harness-environment.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {HarnessEnvironment} from '@angular/cdk/testing';
9+
import {
10+
ComponentHarness,
11+
ComponentHarnessConstructor,
12+
HarnessEnvironment,
13+
HarnessLoader,
14+
TestElement
15+
} from '@angular/cdk/testing';
1016
import {ComponentFixture, flush} from '@angular/core/testing';
1117
import {Observable} from 'rxjs';
1218
import {takeWhile} from 'rxjs/operators';
13-
import {ComponentHarness, ComponentHarnessConstructor, HarnessLoader} from '../component-harness';
14-
import {TestElement} from '../test-element';
1519
import {TaskState, TaskStateZoneInterceptor} from './task-state-zone-interceptor';
1620
import {UnitTestElement} from './unit-test-element';
1721

src/cdk/testing/testbed/unit-test-element.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,15 @@
77
*/
88

99
import * as keyCodes from '@angular/cdk/keycodes';
10-
import {TestElement, TestKey} from '../test-element';
11-
import {ElementDimensions} from '../element-dimensions';
10+
import {ElementDimensions, ModifierKeys, TestElement, TestKey} from '@angular/cdk/testing';
1211
import {
13-
ModifierKeys,
12+
clearElement,
1413
dispatchMouseEvent,
14+
isTextInput,
1515
triggerBlur,
1616
triggerFocus,
17-
isTextInput,
18-
clearElement,
1917
typeInElement,
20-
} from '../fake-events';
18+
} from './fake-events';
2119

2220
/** Maps `TestKey` constants to the `keyCode` and `key` values used by native browser events. */
2321
const keyMap = {

src/material-experimental/form-field/testing/form-field-harness.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import {
1313
HarnessQuery,
1414
TestElement
1515
} from '@angular/cdk/testing';
16+
import {
17+
MatFormFieldControlHarness
18+
} from '@angular/material-experimental/form-field/testing/control';
1619
import {MatInputHarness} from '@angular/material-experimental/input/testing';
1720
import {MatSelectHarness} from '@angular/material-experimental/select/testing';
18-
import {MatFormFieldControlHarness} from './control';
1921
import {FormFieldHarnessFilters} from './form-field-harness-filters';
2022

2123
// TODO(devversion): support datepicker harness once developed (COMP-203).

test/karma-system-config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ System.config({
207207
'rxjs/operators': {main: 'index'},
208208

209209
// Needed for relative imports inside the testing package to work.
210-
'dist/packages/cdk/testing/fake-events': {main: 'index'},
210+
'dist/packages/cdk/testing/testbed/fake-events': {main: 'index'},
211211

212212
// Set the default extension for the root package, because otherwise the tests can't
213213
// be built within the production mode. Due to missing file extensions.

tools/public_api_guard/cdk/testing.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ export interface ComponentHarnessConstructor<T extends ComponentHarness> {
2626
new (locatorFactory: LocatorFactory): T;
2727
}
2828

29+
export interface ElementDimensions {
30+
height: number;
31+
left: number;
32+
top: number;
33+
width: number;
34+
}
35+
2936
export declare abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFactory {
3037
protected rawRootElement: E;
3138
rootElement: TestElement;
@@ -90,6 +97,13 @@ export declare type LocatorFnResult<T extends (HarnessQuery<any> | string)[]> =
9097
} ? C : T[I] extends string ? TestElement : never;
9198
}[number];
9299

100+
export interface ModifierKeys {
101+
alt?: boolean;
102+
control?: boolean;
103+
meta?: boolean;
104+
shift?: boolean;
105+
}
106+
93107
export interface TestElement {
94108
blur(): Promise<void>;
95109
clear(): Promise<void>;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {existsSync} from 'fs';
2+
import * as minimatch from 'minimatch';
3+
import {dirname, join, normalize, relative, resolve} from 'path';
4+
import * as Lint from 'tslint';
5+
import * as ts from 'typescript';
6+
7+
const BUILD_BAZEL_FILE = 'BUILD.bazel';
8+
9+
/**
10+
* Rule that enforces that imports or exports with relative paths do not resolve to
11+
* source files outside of the current Bazel package. This enforcement is necessary
12+
* because relative cross entry-point imports/exports can cause code being inlined
13+
* unintentionally and could break module resolution since the folder structure
14+
* changes in the Angular Package release output.
15+
*/
16+
export class Rule extends Lint.Rules.TypedRule {
17+
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
18+
return this.applyWithFunction(sourceFile, checkSourceFile, this.getOptions().ruleArguments);
19+
}
20+
}
21+
22+
/**
23+
* Rule walker function that checks the source file for imports/exports
24+
* with relative cross entry-point references.
25+
*/
26+
function checkSourceFile(ctx: Lint.WalkContext<string[]>) {
27+
const filePath = ctx.sourceFile.fileName;
28+
const relativeFilePath = relative(process.cwd(), filePath);
29+
30+
if (!ctx.options.every(o => minimatch(relativeFilePath, o))) {
31+
return;
32+
}
33+
34+
const visitNode = (node: ts.Node) => {
35+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
36+
if (!node.moduleSpecifier || !ts.isStringLiteralLike(node.moduleSpecifier) ||
37+
!node.moduleSpecifier.text.startsWith('.')) {
38+
return;
39+
}
40+
41+
const modulePath = node.moduleSpecifier.text;
42+
const basePath = dirname(filePath);
43+
const currentPackage = findClosestBazelPackage(basePath);
44+
const resolvedPackage = findClosestBazelPackage(resolve(basePath, modulePath));
45+
46+
if (currentPackage && resolvedPackage &&
47+
normalize(currentPackage) !== normalize(resolvedPackage)) {
48+
const humanizedType = ts.isImportDeclaration(node) ? 'Import' : 'Export';
49+
ctx.addFailureAtNode(
50+
node,
51+
`${humanizedType} resolves to a different Bazel build package through a relative ` +
52+
`path. This is not allowed and can be fixed by using the actual module import.`);
53+
}
54+
return;
55+
}
56+
ts.forEachChild(node, visitNode);
57+
};
58+
59+
ts.forEachChild(ctx.sourceFile, visitNode);
60+
}
61+
62+
/** Finds the closest Bazel build package for the given path. */
63+
function findClosestBazelPackage(startPath: string): string|null {
64+
let currentPath = startPath;
65+
while (!hasBuildFile(currentPath)) {
66+
const parentPath = dirname(currentPath);
67+
if (parentPath === currentPath) {
68+
return null;
69+
}
70+
currentPath = parentPath;
71+
}
72+
return currentPath;
73+
}
74+
75+
/** Checks whether the given directory has a Bazel build file. */
76+
function hasBuildFile(dirPath: string): boolean {
77+
return existsSync(join(dirPath, BUILD_BAZEL_FILE));
78+
}

tslint.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@
160160
true,
161161
"src/!(a11y-demo|e2e-app|components-examples|universal-app)/**/!(*.spec).ts"
162162
],
163+
"no-cross-entry-point-relative-imports": [
164+
true,
165+
"src/!(a11y-demo|e2e-app|components-examples|universal-app|dev-app)/**/!(*.spec).ts",
166+
"!src/+(cdk|material)/schematics/**/*",
167+
"!src/cdk/testing/+(private|tests)/**/*",
168+
"!src/google-maps/testing/**/*"
169+
],
163170
"file-name-casing": [true, {
164171
// Exclude custom lint rule files since they have to always be camel-cased and end
165172
// with "Rule".

0 commit comments

Comments
 (0)