thymikee / jest-preset-angular

Jest configuration preset for Angular projects.
https://thymikee.github.io/jest-preset-angular/
MIT License
883 stars 306 forks source link

[Bug]: jest-preset-angular does not work well with monorepos with mixed hoisted dependencies #1721

Closed imolorhe closed 2 years ago

imolorhe commented 2 years ago

Version

12.2.0

Steps to reproduce

Setup a monorepo with an angular project with several dependencies, some dependencies are hoisted to the root of the monorepo and others are not.

When you run jest with this preset, you will be getting errors like:

Unexpected value 'TranslateModule' imported by the module 'DynamicTestModule'. Please add an @NgModule annotation

TypeError: Cannot read properties of undefined (reading 'Tag')

Type AgGridModule does not have 'ɵmod' property


Some tests will pass and others will fail.

As it turns out, jest-preset-angular uses this code to call ngcc to compile the dependencies, but it only accounts for the node_modules directory where the ngcc was found in https://github.com/thymikee/jest-preset-angular/blob/main/src/utils/ngcc-jest-processor.ts#L17-L19

This has the unintended effect that depending on how the hoisting happened (moving the ngcc to the root node modules or the angular project node modules), some dependencies in the other node_modules directory would not get compiled.

Took a bit of digging through the code to identify this as the source of the problem.

I fixed it in my case by copying ngcc-jest-processor and modifying it to run the compiler on both node_modules directories.

It would be great if this issue could be solved directly in this project as well.

Expected behavior

I expect all the tests to pass

Actual behavior

Some tests pass, and others fail

Additional context

For my use case, I fixed it by copying the ngcc-jest-processor into my project and modifying it to call ngcc twice (one per node modules directory) and that resolved the issue for me.

/**
 * Mainly copied from https://github.com/angular/angular-cli/blob/master/packages/ngtools/webpack/src/ngcc_processor.ts
 * and adjusted to work with Jest
 */
// const { spawnSync } = require('child_process');
// const path = require('path');
import { spawnSync } from 'child_process';
import path from 'path';

const ANGULAR_COMPILER_CLI_PKG_NAME = `@angular${path.sep}compiler-cli`;
let ngccPath = '';

try {
  ngccPath = require.resolve('@angular/compiler-cli/ngcc/main-ngcc.js');
} catch {
  const compilerCliNgccPath = require.resolve('@angular/compiler-cli/ngcc');
  ngccPath = path.resolve(
    compilerCliNgccPath.substring(0, compilerCliNgccPath.lastIndexOf(path.sep)),
    'main-ngcc.js'
  );
}
function findNodeModulesDirectory() {
  return ngccPath.substring(0, ngccPath.indexOf(ANGULAR_COMPILER_CLI_PKG_NAME));
}

const nodeModuleDirPath = findNodeModulesDirectory();

const runNgccJestProcessorForNodeModule = (tsconfigPath, nodeModuleDir) => {
  if (nodeModuleDir) {
    process.stdout.write(`\nngcc-jest-processor..: running ngcc for ${nodeModuleDir}\n`);

    const ngccBaseArgs = [
      ngccPath,
      '--source' /** basePath */,
      nodeModuleDir,
      '--properties' /** propertiesToConsider */,
      /**
       * There are various properties: fesm2015, fesm5, es2015, esm2015, esm5, main, module, browser to choose from.
       * Normally, Jest requires `umd`. If running Jest in ESM mode, Jest will require both `umd` + `esm2015`.
       */
      ...['es2015', 'main'],
      '--first-only' /** compileAllFormats */,
      'false', // make sure that `ngcc` runs on subfolders as well
      '--async',
    ];
    if (tsconfigPath) {
      ngccBaseArgs.push(...['--tsconfig', tsconfigPath]);
    }
    // We spawn instead of using the API because:
    // - NGCC Async uses clustering which is problematic when used via the API which means
    // that we cannot setup multiple cluster masters with different options.
    // - We will not be able to have concurrent builds otherwise Ex: App-Shell,
    // as NGCC will create a lock file for both builds and it will cause builds to fails.
    const { status, error } = spawnSync(process.execPath, ngccBaseArgs, {
      stdio: ['inherit', process.stderr, process.stderr],
    });
    if (status !== 0) {
      const errorMessage = error?.message ?? '';

      throw new Error(
        `${errorMessage} NGCC failed ${errorMessage ? ', see above' : ''}.`
      );
    }
  } else {
    console.log(
      `Warning: Could not locate '@angular/compiler-cli' to run 'ngcc' automatically.` +
        `Please make sure you are running 'ngcc-jest-processor.js' from root level of your project.` +
        `'ngcc' must be run before running Jest`
    );
  }
};
export const runNgccJestProcessor = (
  tsconfigPath
) => {
  runNgccJestProcessorForNodeModule(tsconfigPath, path.resolve(__dirname, 'node_modules'));
  runNgccJestProcessorForNodeModule(tsconfigPath, path.resolve(__dirname, '../../node_modules'));
};

Environment

System:
    OS: macOS 12.2.1
    CPU: (8) arm64 Apple M1 Pro
  Binaries:
    Node: 16.15.1 - ~/.nvm/versions/node/v16.15.1/bin/node
    Yarn: 1.22.10 - /usr/local/bin/yarn
    npm: 8.11.0 - ~/.nvm/versions/node/v16.15.1/bin/npm
ahnpnl commented 2 years ago

Ngcc is intended to run at root of a project only. If you are using Jest projects option, you need to run ngcc over there

imolorhe commented 2 years ago

If I understand it correctly, I need to run it with the tsconfig.spec.json which would compile for the tests, which is different from the ngcc run in my postbuild script. I don't use Jest projects option either but that doesn't seem to be related, as the issue is about compiling the node modules dependencies before running the tests

ahnpnl commented 2 years ago

ngcc is optional thing, it shouldn't block you from anything. The intention of ngcc-jest-processor is to make it in 1 file which is easy to use and it's entirely optional.

imolorhe commented 2 years ago

Maybe I'm not explaining it properly. I need ngcc to run, and it has always run as a postbuild script and when running my angular tests thanks to jest-preset-angular. However, I recently decided to use yarn workspaces for my repo which I wasn't using previously. This change made some node modules to get hoisted to the root of the monorepo.

One of the things that jest-preset-angular brings is the convenience to configure all the things needed to properly run angular tests with jest, which I believe is why it also offers to run ngcc for your tests by default. However this was lost when I used yarn workspaces since it only runs ngcc in one of the node modules.

(I should note that I have no issues with running or building the angular project itself, and it is just the tests that have the issue.)

Again I don't use jest projects so I don't have a jest config at the root of the monorepo. However if I had one, the global-setup from jest-preset-angular would still not work since it tries to resolve the node modules based on the location of the ngcc script, which means in both the root of the repo and the package, it would resolve to the same node modules directory still.

Let me know if this makes it clearer.

ahnpnl commented 2 years ago

I get it now, one thing I want to clear up is: by default this preset doesn’t provide ngcc to run but you have to opt in, please check our preset https://github.com/thymikee/jest-preset-angular/blob/main/src/presets/index.ts

Our ngcc script is to stimulate what angular cli does, but if it doesn’t fit your need, you shouldn’t use this preset’s ngcc script but should use the one from Angular itself. I think that would help you to move forward. There is an ngcc bin file which is shipped with angular cli.

Please also take a look at our example https://github.com/thymikee/jest-preset-angular/tree/main/examples/example-app-yarn-workspace which might help.

ahnpnl commented 2 years ago

Regarding to fix the ngcc script, feel free to submit your PR :)