Skip to content

Commit b48c0c8

Browse files
committed
refactor: add experimental jest builder
For now this just runs ESBuild-er to build test code, Jest is not actually invoked yet. This uses `glob` to find test files matching the given pattern. I went out of my way to limit `glob` functionality as much as possible in case we change the implementation later. (cherry picked from commit 7fe6570)
1 parent 1803efb commit b48c0c8

File tree

10 files changed

+420
-1
lines changed

10 files changed

+420
-1
lines changed

packages/angular/cli/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ CLI_SCHEMA_DATA = [
8585
"//packages/angular_devkit/build_angular:src/builders/browser-esbuild/schema.json",
8686
"//packages/angular_devkit/build_angular:src/builders/dev-server/schema.json",
8787
"//packages/angular_devkit/build_angular:src/builders/extract-i18n/schema.json",
88+
"//packages/angular_devkit/build_angular:src/builders/jest/schema.json",
8889
"//packages/angular_devkit/build_angular:src/builders/karma/schema.json",
8990
"//packages/angular_devkit/build_angular:src/builders/ng-packagr/schema.json",
9091
"//packages/angular_devkit/build_angular:src/builders/protractor/schema.json",

packages/angular/cli/lib/config/workspace-schema.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@
360360
"@angular-devkit/build-angular:dev-server",
361361
"@angular-devkit/build-angular:extract-i18n",
362362
"@angular-devkit/build-angular:karma",
363+
"@angular-devkit/build-angular:jest",
363364
"@angular-devkit/build-angular:protractor",
364365
"@angular-devkit/build-angular:server",
365366
"@angular-devkit/build-angular:ng-packagr"
@@ -516,6 +517,28 @@
516517
}
517518
}
518519
},
520+
{
521+
"type": "object",
522+
"additionalProperties": false,
523+
"properties": {
524+
"builder": {
525+
"const": "@angular-devkit/build-angular:jest"
526+
},
527+
"defaultConfiguration": {
528+
"type": "string",
529+
"description": "A default named configuration to use when a target configuration is not provided."
530+
},
531+
"options": {
532+
"$ref": "../../../../angular_devkit/build_angular/src/builders/jest/schema.json"
533+
},
534+
"configurations": {
535+
"type": "object",
536+
"additionalProperties": {
537+
"$ref": "../../../../angular_devkit/build_angular/src/builders/jest/schema.json"
538+
}
539+
}
540+
}
541+
},
519542
{
520543
"type": "object",
521544
"additionalProperties": false,

packages/angular_devkit/build_angular/BUILD.bazel

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ ts_json_schema(
3838
src = "src/builders/extract-i18n/schema.json",
3939
)
4040

41+
ts_json_schema(
42+
name = "jest_schema",
43+
src = "src/builders/jest/schema.json",
44+
)
45+
4146
ts_json_schema(
4247
name = "karma_schema",
4348
src = "src/builders/karma/schema.json",
@@ -79,6 +84,7 @@ ts_library(
7984
"//packages/angular_devkit/build_angular:src/builders/browser/schema.ts",
8085
"//packages/angular_devkit/build_angular:src/builders/dev-server/schema.ts",
8186
"//packages/angular_devkit/build_angular:src/builders/extract-i18n/schema.ts",
87+
"//packages/angular_devkit/build_angular:src/builders/jest/schema.ts",
8288
"//packages/angular_devkit/build_angular:src/builders/karma/schema.ts",
8389
"//packages/angular_devkit/build_angular:src/builders/ng-packagr/schema.ts",
8490
"//packages/angular_devkit/build_angular:src/builders/protractor/schema.ts",
@@ -267,7 +273,7 @@ ts_library(
267273
"src/**/tests/*.ts",
268274
],
269275
exclude = [
270-
"src/testing/**/*_spec.ts",
276+
"src/**/*_spec.ts",
271277
],
272278
),
273279
data = glob(["test/**/*"]),
@@ -347,6 +353,12 @@ LARGE_SPECS = {
347353
"@npm//buffer",
348354
],
349355
},
356+
"jest": {
357+
"extra_deps": [
358+
"@npm//glob",
359+
"@npm//@types/glob",
360+
],
361+
},
350362
}
351363

352364
[

packages/angular_devkit/build_angular/builders.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
"schema": "./src/builders/extract-i18n/schema.json",
2727
"description": "Extract i18n strings from a browser application."
2828
},
29+
"jest": {
30+
"implementation": "./src/builders/jest",
31+
"schema": "./src/builders/jest/schema.json",
32+
"description": "Run unit tests using Jest."
33+
},
2934
"karma": {
3035
"implementation": "./src/builders/karma",
3136
"schema": "./src/builders/karma/schema.json",
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
10+
import { buildEsbuildBrowserInternal } from '../browser-esbuild';
11+
import { BrowserEsbuildOptions } from '../browser-esbuild/options';
12+
import { OutputHashing } from '../browser-esbuild/schema';
13+
import { normalizeOptions } from './options';
14+
import { Schema as JestBuilderSchema } from './schema';
15+
import { findTestFiles } from './test-files';
16+
17+
/** Main execution function for the Jest builder. */
18+
export default createBuilder(
19+
async (schema: JestBuilderSchema, context: BuilderContext): Promise<BuilderOutput> => {
20+
context.logger.warn(
21+
'NOTE: The Jest builder is currently EXPERIMENTAL and not ready for production use.',
22+
);
23+
24+
const options = normalizeOptions(schema);
25+
const testFiles = await findTestFiles(options, context.workspaceRoot);
26+
const testOut = 'dist/test-out'; // TODO(dgp1130): Hide in temp directory.
27+
28+
// Build all the test files.
29+
return await build(context, {
30+
entryPoints: testFiles,
31+
tsConfig: options.tsConfig,
32+
polyfills: options.polyfills,
33+
outputPath: testOut,
34+
aot: false,
35+
index: null,
36+
outputHashing: OutputHashing.None,
37+
outExtension: 'mjs', // Force native ESM.
38+
commonChunk: false,
39+
optimization: false,
40+
buildOptimizer: false,
41+
sourceMap: {
42+
scripts: true,
43+
styles: false,
44+
vendor: false,
45+
},
46+
});
47+
},
48+
);
49+
50+
async function build(
51+
context: BuilderContext,
52+
options: BrowserEsbuildOptions,
53+
): Promise<BuilderOutput> {
54+
try {
55+
for await (const _ of buildEsbuildBrowserInternal(options, context)) {
56+
// Nothing to do for each event, just wait for the whole build.
57+
}
58+
59+
return { success: true };
60+
} catch (err) {
61+
return {
62+
success: false,
63+
error: (err as Error).message,
64+
};
65+
}
66+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { Schema as JestBuilderSchema } from './schema';
10+
11+
/**
12+
* Options supported for the Jest builder. The schema is an approximate
13+
* representation of the options type, but this is a more precise version.
14+
*/
15+
export type JestBuilderOptions = JestBuilderSchema & {
16+
include: string[];
17+
exclude: string[];
18+
};
19+
20+
/**
21+
* Normalizes input options validated by the schema to a more precise and useful
22+
* options type in {@link JestBuilderOptions}.
23+
*/
24+
export function normalizeOptions(schema: JestBuilderSchema): JestBuilderOptions {
25+
return {
26+
// Options with default values can't actually be null, even if the types say so.
27+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
28+
include: schema.include!,
29+
exclude: schema.exclude!,
30+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
31+
32+
...schema,
33+
};
34+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"title": "Jest browser schema for Build Facade.",
4+
"description": "Jest target options",
5+
"type": "object",
6+
"properties": {
7+
"include": {
8+
"type": "array",
9+
"items": {
10+
"type": "string"
11+
},
12+
"default": ["**/*.spec.ts"],
13+
"description": "Globs of files to include, relative to project root. \nThere are 2 special cases:\n - when a path to directory is provided, all spec files ending \".spec.@(ts|tsx)\" will be included\n - when a path to a file is provided, and a matching spec file exists it will be included instead."
14+
},
15+
"exclude": {
16+
"type": "array",
17+
"items": {
18+
"type": "string"
19+
},
20+
"default": [],
21+
"description": "Globs of files to exclude, relative to the project root."
22+
},
23+
"tsConfig": {
24+
"type": "string",
25+
"description": "The name of the TypeScript configuration file."
26+
},
27+
"polyfills": {
28+
"description": "Polyfills to be included in the build.",
29+
"oneOf": [
30+
{
31+
"type": "array",
32+
"description": "A list of polyfills to include in the build. Can be a full path for a file, relative to the current workspace or module specifier. Example: 'zone.js'.",
33+
"items": {
34+
"type": "string",
35+
"uniqueItems": true
36+
},
37+
"default": []
38+
},
39+
{
40+
"type": "string",
41+
"description": "The full path for the polyfills file, relative to the current workspace or a module specifier. Example: 'zone.js'."
42+
}
43+
]
44+
}
45+
},
46+
"additionalProperties": false,
47+
"required": ["tsConfig"]
48+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { IOptions as GlobOptions, glob as globCb } from 'glob';
10+
import { promisify } from 'util';
11+
import { JestBuilderOptions } from './options';
12+
13+
const globAsync = promisify(globCb);
14+
15+
/**
16+
* Finds all test files in the project.
17+
*
18+
* @param options The builder options describing where to find tests.
19+
* @param workspaceRoot The path to the root directory of the workspace.
20+
* @param glob A promisified implementation of the `glob` module. Only intended for
21+
* testing purposes.
22+
* @returns A set of all test files in the project.
23+
*/
24+
export async function findTestFiles(
25+
options: JestBuilderOptions,
26+
workspaceRoot: string,
27+
glob: typeof globAsync = globAsync,
28+
): Promise<Set<string>> {
29+
const globOptions: GlobOptions = {
30+
cwd: workspaceRoot,
31+
ignore: ['node_modules/**'].concat(options.exclude),
32+
strict: true, // Fail on an "unusual error" when reading the file system.
33+
nobrace: true, // Do not expand `a{b,c}` to `ab,ac`.
34+
noext: true, // Disable "extglob" patterns.
35+
nodir: true, // Match only files, don't care about directories.
36+
};
37+
38+
const included = await Promise.all(options.include.map((pattern) => glob(pattern, globOptions)));
39+
40+
// Flatten and deduplicate any files found in multiple include patterns.
41+
return new Set(included.flat());
42+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { JestBuilderOptions } from '../options';
10+
11+
/** Default options to use for most tests. */
12+
export const BASE_OPTIONS = Object.freeze<JestBuilderOptions>({
13+
include: ['**/*.spec.ts'],
14+
exclude: [],
15+
tsConfig: 'tsconfig.spec.json',
16+
});

0 commit comments

Comments
 (0)