11import path from 'canonical-path' ;
22import { spawn } from 'cross-spawn' ;
33import fs from 'fs-extra' ;
4- import { sync as globbySync } from 'globby' ;
4+ import { globbySync } from 'globby' ;
5+ import jsonc from 'cjson' ;
56import os from 'os' ;
67import shelljs from 'shelljs' ;
78import treeKill from 'tree-kill' ;
@@ -20,27 +21,42 @@ process.env.CHROMEDRIVER_BIN = path.resolve(process.env.CHROMEDRIVER_BIN);
2021const { argv} = yargs ( hideBin ( process . argv ) ) ;
2122
2223const EXAMPLE_PATH = path . resolve ( argv . _ [ 0 ] ) ;
23- const NODE_MODULES_PATH = path . resolve ( argv . _ [ 1 ] ) ;
2424const NODE = process . execPath ;
25- const VENDORED_YARN = path . resolve ( argv . _ [ 2 ] ) ;
25+ const VENDORED_YARN = path . resolve ( argv . _ [ 1 ] ) ;
26+ const EXAMPLE_DEPS_WORKSPACE_NAME = argv . _ [ 2 ] ;
27+ const LOCAL_PACKAGES = argv . _ . slice ( 3 ) . reduce ( ( pkgs , pkgNameAndPath ) => {
28+ const [ pkgName , pkgPath ] = pkgNameAndPath . split ( '#' ) ;
29+ pkgs [ pkgName ] = path . resolve ( pkgPath ) ;
30+ return pkgs ;
31+ } , { } ) ;
32+
2633const SJS_SPEC_FILENAME = 'e2e-spec.ts' ;
2734const CLI_SPEC_FILENAME = 'e2e/src/app.e2e-spec.ts' ;
2835const EXAMPLE_CONFIG_FILENAME = 'example-config.json' ;
2936const MAX_NO_OUTPUT_TIMEOUT = 1000 * 60 * 5 ; // 5 minutes
3037
3138/**
32- * Run Protractor End-to-End Tests for Doc Samples
39+ * Run Protractor End-to-End Tests for a Docs Example
40+ *
41+ * Usage: node run-example-e2e.mjs <examplePath> <yarnPath> <exampleDepsWorkspaceName> [localPackage...]
42+ *
43+ * Args:
44+ * examplePath: path to the example
45+ * yarnPath: path to a vendored version of yarn
46+ * exampleDepsWorkspaceName: name of bazel workspace containing example node_omodules
47+ * localPackages: a vararg of local packages to substitute in place npm deps, in the form @package/name#pathToPackage.
3348 *
3449 * Flags
3550 * --retry to retry failed tests (useful for overcoming flakes)
3651 * e.g. --retry 3 // To try each test up to 3 times.
3752 */
38- async function runE2e ( examplePath , nodeModulesPath ) {
53+
54+ async function runE2e ( examplePath ) {
3955 const exampleName = path . basename ( examplePath ) ;
4056 const maxAttempts = argv . retry || 1 ;
4157 try {
4258 examplePath = createCopyOfExampleForTest ( exampleName , examplePath ) ;
43- symlinkNodeModules ( examplePath , nodeModulesPath ) ;
59+ await constructNodeModules ( examplePath ) ;
4460
4561 let testFn ;
4662 if ( isSystemJsTest ( examplePath ) ) {
@@ -165,8 +181,74 @@ function runProtractorAoT(exampleName, appDir) {
165181 return runProtractorSystemJS ( exampleName , promise , appDir , aotRunSpawnInfo ) ;
166182}
167183
168- function symlinkNodeModules ( examplePath , nodeModulesPath ) {
169- fs . ensureSymlinkSync ( nodeModulesPath , path . join ( examplePath , 'node_modules' ) , 'dir' ) ;
184+ async function constructNodeModules ( examplePath ) {
185+ const linkedNodeModules = path . resolve ( examplePath , 'node_modules' ) ;
186+ const exampleDepsNodeModules = path . resolve ( '..' , EXAMPLE_DEPS_WORKSPACE_NAME , 'node_modules' ) ;
187+ fs . ensureDirSync ( linkedNodeModules ) ;
188+
189+ await Promise . all ( [
190+ linkExampleDeps ( exampleDepsNodeModules , linkedNodeModules ) ,
191+ linkLocalDeps ( exampleDepsNodeModules , linkedNodeModules )
192+ ] ) ;
193+
194+ fs . copySync ( path . join ( exampleDepsNodeModules , '.bin' ) , path . join ( linkedNodeModules , '.bin' ) ) ;
195+ pointBinSymlinksToLocalPackages ( linkedNodeModules ) ;
196+ }
197+
198+ // The .bin folder is copied over from the original yarn_install repository, so the
199+ // bin symlinks point there. When we link local packages in place of their npm equivalent,
200+ // we need to alter those symlinks to point into the local package.
201+ function pointBinSymlinksToLocalPackages ( linkedNodeModules ) {
202+ if ( os . platform ( ) === 'win32' ) {
203+ // Bins on Windows are not symlinks; they are scripts that will invoke the bin
204+ // relative to their location. The relative path will already point to the symlinked
205+ // local package, so no further action is required.
206+ return ;
207+ }
208+ const allNodeModuleBins = globbySync ( [ '**' ] , { cwd : path . join ( linkedNodeModules , '.bin' ) , onlyFiles : true } ) ;
209+ allNodeModuleBins . forEach ( bin => {
210+ const symlinkTarget = fs . readlinkSync ( path . join ( linkedNodeModules , '.bin' , bin ) ) ;
211+ for ( const pkgName of Object . keys ( LOCAL_PACKAGES ) ) {
212+ const binMightBeInLocalPackage = symlinkTarget . includes ( path . join ( EXAMPLE_DEPS_WORKSPACE_NAME , 'node_modules' , pkgName ) + path . sep ) ;
213+ if ( binMightBeInLocalPackage ) {
214+ const pathToBinWithinPackage = symlinkTarget . substring ( symlinkTarget . indexOf ( pkgName ) + pkgName . length + path . sep . length ) ;
215+ const binExistsInLocalPackage = fs . existsSync ( path . join ( linkedNodeModules , pkgName , pathToBinWithinPackage ) ) ;
216+ if ( binExistsInLocalPackage ) {
217+ // Replace the copied bin symlink with one that points to the symlinked local package.
218+ fs . rmSync ( path . join ( linkedNodeModules , '.bin' , bin ) ) ;
219+ fs . ensureSymlinkSync ( path . join ( '..' , pkgName , pathToBinWithinPackage ) , path . join ( linkedNodeModules , '.bin' , bin ) ) ;
220+ }
221+ break ;
222+ }
223+ }
224+ } ) ;
225+ }
226+
227+ function linkExampleDeps ( exampleDepsNodeModules , linkedNodeModules ) {
228+ const exampleDepsPackages = getPackageNamesFromNodeModules ( exampleDepsNodeModules ) ;
229+
230+ return Promise . all ( exampleDepsPackages
231+ . filter ( pkgName => ! ( pkgName in LOCAL_PACKAGES ) )
232+ . map ( pkgName => fs . ensureSymlink ( path . join ( exampleDepsNodeModules , pkgName ) , path . join ( linkedNodeModules , pkgName ) , 'dir' ) )
233+ ) ;
234+ }
235+
236+ async function linkLocalDeps ( exampleDepsNodeModules , linkedNodeModules ) {
237+ const hasNpmDepForPkg = await Promise . all ( Object . keys ( LOCAL_PACKAGES ) . map ( pkgName => fs . pathExists ( path . join ( exampleDepsNodeModules , pkgName ) ) ) ) ;
238+
239+ return Promise . all ( Object . keys ( LOCAL_PACKAGES ) . filter ( ( pkgName , i ) => hasNpmDepForPkg [ i ] )
240+ . map ( pkgName => fs . ensureSymlinkSync ( LOCAL_PACKAGES [ pkgName ] , path . join ( linkedNodeModules , pkgName ) , 'dir' ) ) ) ;
241+ }
242+
243+ function getPackageNamesFromNodeModules ( nodeModulesPath ) {
244+ return globbySync ( [
245+ '@*/*' ,
246+ '!@*$' , // Exclude a namespace folder itself
247+ '(?!@)*' ,
248+ '!.bin' ,
249+ '!.yarn-integrity' ,
250+ '!_*'
251+ ] , { cwd : nodeModulesPath , onlyDirectories : true , dot : true } ) ;
170252}
171253
172254// Start the example in appDir; then run protractor with the specified
@@ -188,6 +270,16 @@ function runE2eTestsCLI(exampleName, appDir) {
188270 }
189271 }
190272
273+ // When local packages are symlinked in, node has trouble resolving some peer deps. Setting
274+ // preserveSymlinks: true in angular.json fixes this. This isn't required without local
275+ // packages because in the worst case we would leak into the original Bazel repository
276+ // and it would still find a node_modules folder for resolution.
277+ if ( Object . keys ( LOCAL_PACKAGES ) . length > 0 ) {
278+ const angularJson = jsonc . load ( path . join ( appDir , 'angular.json' ) , { encoding : 'utf-8' } ) ;
279+ angularJson . projects [ 'angular.io-example' ] . architect . build . options . preserveSymlinks = true ;
280+ fs . writeFileSync ( path . join ( appDir , 'angular.json' ) , JSON . stringify ( angularJson ) ) ;
281+ }
282+
191283 // `--no-webdriver-update` is needed to preserve the ChromeDriver version already installed.
192284 const testCommands = config . tests || [ {
193285 cmd : NODE ,
@@ -306,4 +398,5 @@ function adjustChromeBinPathForWindows() {
306398 return process . env . CHROME_BIN ;
307399}
308400
309- runE2e ( EXAMPLE_PATH , NODE_MODULES_PATH ) ;
401+ runE2e ( EXAMPLE_PATH ) ;
402+
0 commit comments