Skip to content

Commit f0142aa

Browse files
devversionzarend
authored andcommitted
build: replace gulp setup with bundling script for legacy remote browser tests
Replaces the legacy gulp setup with a simple script that runs the Angular compiler and ESBuild with the linker. This is simpler than switching the Gulp legacy output to properly deal with the FW packages using partial compilation output together with SystemJS. It has been a long goal removing SystemJS and the gulp tooling anyway, so this is a good chance for simplifying this code.. removing the need for all these additional tsconfig files; and additional configuration of the legacy gulp tooling when wiring up new packages.
1 parent a60e650 commit f0142aa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+266
-1157
lines changed

gulpfile.js

Lines changed: 0 additions & 24 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
"e2e": "bazel test //src/... --build_tag_filters=e2e --test_tag_filters=e2e --build_tests_only",
2727
"deploy-dev-app": "node ./scripts/deploy-dev-app.js",
2828
"breaking-changes": "ts-node --project scripts/tsconfig.json scripts/breaking-changes.ts",
29-
"gulp": "gulp",
3029
"check-entry-point-setup": "node ./scripts/check-entry-point-setup.js",
3130
"check-package-externals": "ts-node --project scripts/tsconfig.json scripts/check-package-externals.ts",
3231
"format": "yarn ng-dev format changed",
@@ -64,7 +63,6 @@
6463
"material-components-web": "13.0.0-canary.860ad06a1.0",
6564
"rxjs": "^6.5.3",
6665
"rxjs-tslint-rules": "^4.33.1",
67-
"systemjs": "0.19.43",
6866
"tslib": "^2.3.0",
6967
"zone.js": "~0.11.3"
7068
},
@@ -150,7 +148,6 @@
150148
"@types/browser-sync": "^2.26.1",
151149
"@types/fs-extra": "^9.0.5",
152150
"@types/glob": "^7.1.3",
153-
"@types/gulp": "4.0.8",
154151
"@types/inquirer": "^7.3.1",
155152
"@types/jasmine": "^3.6.0",
156153
"@types/luxon": "^1.27.0",
@@ -176,12 +173,10 @@
176173
"dgeni": "^0.4.11",
177174
"dgeni-packages": "^0.28.4",
178175
"diff": "^5.0.0",
176+
"esbuild": "^0.13.3",
179177
"firebase-tools": "^9.2.1",
180178
"fs-extra": "^9.0.1",
181179
"glob": "^7.1.2",
182-
"gulp": "^4.0.2",
183-
"gulp-cli": "^2.3.0",
184-
"gulp-dart-sass": "^1.0.2",
185180
"highlight.js": "^10.7.0",
186181
"husky": "^7.0.1",
187182
"inquirer": "^8.0.0",

scripts/circleci/run-browserstack-tests.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ export BROWSER_STACK_ACCESS_KEY=`echo ${BROWSER_STACK_ACCESS_KEY} | rev`
1717
# by the Karma configuration script.
1818
export TEST_PLATFORM="browserstack"
1919

20-
# Run the unit tests on Browserstack with Karma.
21-
yarn gulp ci:test
20+
# Build the legacy tests
21+
node ./scripts/create-legacy-tests-bundle.mjs
22+
23+
# Run Karma
24+
yarn karma start ./test/karma.conf.js --single-run
2225

2326
# Wait for all sub processes to terminate properly.
2427
wait

scripts/circleci/run-saucelabs-tests.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ export SAUCE_ACCESS_KEY=`echo ${SAUCE_ACCESS_KEY} | rev`
1818
# by the Karma configuration script.
1919
export TEST_PLATFORM="saucelabs"
2020

21-
# Run the unit tests on Saucelabs with Karma.
22-
yarn gulp ci:test
21+
# Build the legacy tests
22+
node ./scripts/create-legacy-tests-bundle.mjs
23+
24+
# Run Karma
25+
yarn karma start ./test/karma.conf.js --single-run
2326

2427
# Kill the Saucelabs tunnel. This is necessary in order to avoid rate-limit
2528
# errors that cause the unit tests to be flaky.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#!/usr/bin/env node
2+
3+
import {ConsoleLogger, LogLevel, NodeJSFileSystem} from '@angular/compiler-cli';
4+
import {createEs2015LinkerPlugin} from '@angular/compiler-cli/linker/babel';
5+
import {transformAsync} from '@babel/core';
6+
import child_process from 'child_process';
7+
import esbuild from 'esbuild';
8+
import fs from 'fs';
9+
import glob from 'glob';
10+
import {dirname, join, relative} from 'path';
11+
import sass from 'sass';
12+
import url from 'url';
13+
14+
const containingDir = dirname(url.fileURLToPath(import.meta.url));
15+
const projectDir = join(containingDir, '../');
16+
const legacyTsconfigPath = join(projectDir, 'src/tsconfig-legacy.json');
17+
18+
const distDir = join(projectDir, 'dist/');
19+
const nodeModulesDir = join(projectDir, 'node_modules/');
20+
const outFile = join(distDir, 'legacy-test-bundle.spec.js');
21+
const ngcBinFile = join(nodeModulesDir, '@angular/compiler-cli/bundles/src/bin/ngc.js');
22+
const legacyOutputDir = join(distDir, 'legacy-test-out');
23+
24+
25+
/**
26+
* This script builds the whole library in `angular/components` together with its
27+
* spec files into a single IIFE bundle.
28+
*
29+
* The bundle can then be used in the legacy Saucelabs or Browserstack tests. Bundling
30+
* helps with running the Angular linker on framework packages, and also avoids unnecessary
31+
* complexity with maintaining module resolution at runtime through e.g. SystemJS.
32+
*/
33+
async function main() {
34+
// Wait for all Sass compilations to finish.
35+
await compileSassFiles();
36+
37+
// Build the project with Ngtsc so that external resources are inlined.
38+
await compileProjectWithNgtsc();
39+
40+
const specEntryPointFile = await createEntryPointSpecFile();
41+
const esbuildLinkerPlugin = await createLinkerEsbuildPlugin();
42+
const esbuildResolvePlugin = await createResolveEsbuildPlugin();
43+
44+
const result = await esbuild.build({
45+
bundle: true,
46+
sourceRoot: projectDir,
47+
platform: 'browser',
48+
format: 'iife',
49+
outfile: outFile,
50+
plugins: [esbuildResolvePlugin, esbuildLinkerPlugin],
51+
stdin: {contents: specEntryPointFile, resolveDir: projectDir},
52+
});
53+
54+
if (result.errors.length) {
55+
throw Error('Could not build legacy test bundle. See errors above.');
56+
}
57+
}
58+
59+
/**
60+
* Compiles all non-partial Sass files in the project and writes them next
61+
* to their source files. The files are written into the source root as
62+
* this simplifies the resolution within the standalone Angular compiler.
63+
*
64+
* Given that the legacy tests should only run on CI, it is acceptable to
65+
* write to the checked-in source tree. The files remain untracked unless
66+
* explicitly added.
67+
*/
68+
async function compileSassFiles() {
69+
const sassFiles = glob.sync('src/**/!(_*|theme).scss', {cwd: projectDir, absolute: true});
70+
const sassTasks = [];
71+
72+
for (const file of sassFiles) {
73+
const outRelativePath = relative(projectDir, file).replace(/\.scss$/, '.css');
74+
const outPath = join(projectDir, outRelativePath);
75+
const task = renderSassFileAsync(file).then(async (content) => {
76+
console.info('Compiled, now writing:', outRelativePath);
77+
await fs.promises.mkdir(dirname(outPath), {recursive: true});
78+
await fs.promises.writeFile(outPath, content)
79+
});
80+
81+
sassTasks.push(task);
82+
}
83+
84+
// Wait for all Sass compilations to finish.
85+
await Promise.all(sassTasks);
86+
}
87+
88+
/**
89+
* Compiles the project using the Angular compiler in order to produce JS output of
90+
* the packages and tests. This step is important in order to full-compile all
91+
* exported components of the library (inlining external stylesheets or templates).
92+
*/
93+
async function compileProjectWithNgtsc() {
94+
// Build the project with Ngtsc so that external resources are inlined.
95+
const ngcProcess = child_process.spawnSync(
96+
'node', [ngcBinFile, '--project', legacyTsconfigPath], {shell: true, stdio: 'inherit'});
97+
98+
if (ngcProcess.error || ngcProcess.status !== 0) {
99+
throw Error('Unable to compile tests and library. See error above.');
100+
}
101+
}
102+
103+
/**
104+
* Queries for all spec files in the built output and creates a single
105+
* ESM entry-point file which imports all the spec files.
106+
*
107+
* This spec file can then be used as entry-point for ESBuild in order
108+
* to bundle all specs in an IIFE file.
109+
*/
110+
async function createEntryPointSpecFile() {
111+
const testFiles = glob.sync('**/*.spec.js', {absolute: true, cwd: legacyOutputDir});
112+
113+
let specEntryPointFile = `import './test/angular-test-init-spec.ts';`;
114+
let i = 0;
115+
const testNamespaces = [];
116+
117+
for (const file of testFiles) {
118+
const relativePath = relative(projectDir, file);
119+
const specifier = `./${relativePath.replace(/\\/g, '/')}`;
120+
const testNamespace = `__${i++}`;
121+
122+
testNamespaces.push(testNamespace);
123+
specEntryPointFile += `import * as ${testNamespace} from '${specifier}';\n`;
124+
}
125+
126+
for (const namespaceId of testNamespaces) {
127+
// We generate a side-effect invocation that references the test import. This
128+
// is necessary to trick `ESBuild` in preserving the imports. Unfortunately the
129+
// test files would be dead-code eliminated otherwise because the specs are part
130+
// of folders with `package.json` files setting the `"sideEffects: false"` field.
131+
specEntryPointFile += `new Function('x', 'return x')(${namespaceId});\n`;
132+
}
133+
134+
return specEntryPointFile;
135+
}
136+
137+
/** Helper function to render a Sass file asynchronously using promises. */
138+
async function renderSassFileAsync(inputFile) {
139+
return new Promise((resolve, reject) => {
140+
sass.render(
141+
{file: inputFile, includePaths: [nodeModulesDir]},
142+
(err, result) => err ? reject(err) : resolve(result.css));
143+
});
144+
}
145+
146+
/**
147+
* Creates an ESBuild plugin which maps `@angular/<..>` module names to their
148+
* locally-built output (for the packages which are built as part of this repo).
149+
*/
150+
async function createResolveEsbuildPlugin() {
151+
return {
152+
name: 'ng-resolve-esbuild', setup: (build) => {
153+
build.onResolve({filter: /@angular\//}, async (args) => {
154+
const pkgName = args.path.substr('@angular/'.length);
155+
let resolvedPath = join(legacyOutputDir, pkgName)
156+
let stats = await statGraceful(resolvedPath);
157+
158+
// If the resolved path points to a directory, resolve the contained `index.js` file
159+
if (stats && stats.isDirectory()) {
160+
resolvedPath = join(resolvedPath, 'index.js');
161+
stats = await statGraceful(resolvedPath);
162+
}
163+
// If the resolved path does not exist, check with an explicit JS extension.
164+
else if (stats === null) {
165+
resolvedPath += '.js';
166+
stats = await statGraceful(resolvedPath);
167+
}
168+
169+
return stats !== null ? {path: resolvedPath} : undefined;
170+
});
171+
}
172+
}
173+
}
174+
175+
/** Creates an ESBuild plugin that runs the Angular linker on framework packages. */
176+
async function createLinkerEsbuildPlugin() {
177+
const linkerBabelPlugin = createEs2015LinkerPlugin({
178+
fileSystem: new NodeJSFileSystem(),
179+
logger: new ConsoleLogger(LogLevel.warn),
180+
// We enable JIT mode as unit tests also will rely on the linked ESM files.
181+
linkerJitMode: true,
182+
});
183+
184+
return {
185+
name: 'ng-linker-esbuild',
186+
setup: (build) => {
187+
build.onLoad({filter: /fesm2020/}, async (args) => {
188+
const filePath = args.path;
189+
const content = await fs.promises.readFile(filePath, 'utf8');
190+
const {code} = await transformAsync(content, {
191+
filename: filePath,
192+
filenameRelative: filePath,
193+
plugins: [linkerBabelPlugin],
194+
sourceMaps: 'inline',
195+
});
196+
return {contents: code};
197+
});
198+
},
199+
};
200+
}
201+
202+
/**
203+
* Retrieves the `fs.Stats` results for the given path gracefully.
204+
* If the file does not exist, returns `null`.
205+
*/
206+
async function statGraceful(path) {
207+
try {
208+
return await fs.promises.stat(path);
209+
} catch {
210+
return null;
211+
}
212+
}
213+
214+
main().catch(e => {
215+
console.error(e);
216+
process.exitCode = 1;
217+
});

src/cdk-experimental/tsconfig-tests.json

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/cdk/tsconfig-tests.json

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/components-examples/material/datepicker/tsconfig-build.json

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)