Skip to content

Commit

Permalink
feat(compiler): use replace-resources transformer from Angular (#708)
Browse files Browse the repository at this point in the history
BREAKING CHANGE
- `isolatedModules: true` will use `inline-files` and `strip-styles` transformers as default transformers.
- `isolatedModules: false` will use `replace-resources` transformer from `@ngtools/webpack` (besides the existing `downlevel-ctor` transformer). This will make `jest-preset-angular` become closer to what Angular CLI does with Karma + Jasmine.
  • Loading branch information
ahnpnl committed Jan 6, 2021
1 parent 68237fe commit 1b20c19
Show file tree
Hide file tree
Showing 20 changed files with 453 additions and 69 deletions.
5 changes: 1 addition & 4 deletions e2e/__tests__/calc/calc.component.ts
@@ -1,7 +1,5 @@
import { Component, OnInit, Input } from '@angular/core';
import { Observable } from 'rxjs';

const image = require('e2e/test-app-v10/src/assets/its_something.png');
import * as image from 'e2e/test-app-v10/src/assets/its_something.png';

@Component({
selector: 'app-calc',
Expand All @@ -23,7 +21,6 @@ export class CalcComponent implements OnInit {
@Input() hasAClass = false;
prop1: number;
image: string;
observable$: Observable<string>;

constructor() {
this.init();
Expand Down
2 changes: 1 addition & 1 deletion e2e/__tests__/forward-ref/forward-ref.spec.ts
@@ -1,6 +1,6 @@
import { forwardRef, Inject, Injector } from '@angular/core';

const shouldSkipTest = process.env.NG_VERSION === 'v9' || process.env.SKIP_TEST === 'true';
const shouldSkipTest = process.env.NG_VERSION === 'v9' || process.env.ISOLATED_MODULES === 'true';
const skipTest = shouldSkipTest ? test.skip : test

if (shouldSkipTest) {
Expand Down
Expand Up @@ -3,6 +3,7 @@ import { Component } from '@angular/core';
@Component({
selector: 'app-simple-with-styles',
templateUrl: './simple-with-styles.component.html',
styleUrls: ['./simple-with-styles.scss'],
// we have to setup styles this way, since simple styles/styleUrs properties will be removed (jest does not unit test styles)
styles: [`
.some-class { color: red }
Expand Down
3 changes: 3 additions & 0 deletions e2e/__tests__/simple-with-styles/simple-with-styles.scss
@@ -0,0 +1,3 @@
h1 {
font-size: 1.6rem;
}
1 change: 1 addition & 0 deletions e2e/test-app-v10/src/typings.d.ts
Expand Up @@ -3,3 +3,4 @@ declare var module: NodeModule;
interface NodeModule {
id: string;
}
declare module '*.png';
1 change: 1 addition & 0 deletions e2e/test-app-v11/src/typings.d.ts
Expand Up @@ -3,3 +3,4 @@ declare var module: NodeModule;
interface NodeModule {
id: string;
}
declare module '*.png';
1 change: 1 addition & 0 deletions e2e/test-app-v9/src/typings.d.ts
Expand Up @@ -3,3 +3,4 @@ declare var module: NodeModule;
interface NodeModule {
id: string;
}
declare module '*.png';
17 changes: 12 additions & 5 deletions scripts/e2e.js
Expand Up @@ -25,15 +25,18 @@ const executeTest = (projectRealPath) => {
logger.log();

logger.log('setting NG_VERSION environment variable');
logger.log();
const projectName = projectRealPath.match(/([^\\/]*)\/*$/)[1];
process.env.NG_VERSION = projectName.substring(projectName.lastIndexOf('-') + 1);

// then we install it in the repo
logger.log('ensuring all dependencies of target project are installed');
logger.log();

execa.sync('yarn', ['install'], { cwd: projectRealPath });

logger.log('cleaning old assets in target project');
logger.log();

const testCasesDest = join(projectRealPath, 'src', '__tests__');
const presetDir = join(projectRealPath, 'node_modules', 'jest-preset-angular');
Expand All @@ -42,6 +45,7 @@ const executeTest = (projectRealPath) => {
mkdirSync(presetDir);

logger.log('copying distributed assets to target project');
logger.log();

copySync(join(cwd, 'jest-preset.js'), `${presetDir}/jest-preset.js`);
copySync(join(cwd, 'ngcc-jest-processor.js'), `${presetDir}/ngcc-jest-processor.js`);
Expand All @@ -50,6 +54,7 @@ const executeTest = (projectRealPath) => {
copySync(join(cwd, 'build'), `${presetDir}/build`);

logger.log('copying test cases to target project');
logger.log();

copySync(join(cwd, 'e2e', '__tests__'), testCasesDest);

Expand All @@ -66,7 +71,7 @@ const executeTest = (projectRealPath) => {
// cmdESMIso.push(...jestArgs);
}

logger.log('starting non isolatedModules tests');
logger.log('STARTING NONE ISOLATED MODULES TESTS');
logger.log();
logger.log('starting the CJS tests using:', ...cmdCjsUnIso);
logger.log();
Expand All @@ -77,11 +82,13 @@ const executeTest = (projectRealPath) => {
env: process.env,
});

logger.log('starting isolatedModules tests');
logger.log();
logger.log('setting SKIP_TEST environment variable for isolatedModules true');
process.env.SKIP_TEST = 'true';
logger.log('STARTING ISOLATED MODULES TESTS');
logger.log();
logger.log('setting ISOLATED_MODULES environment variable for isolatedModules true');
process.env.ISOLATED_MODULES = 'true';

logger.log();
logger.log('starting the CommonJS tests using:', ...cmdCjsIso);
logger.log();

Expand All @@ -104,7 +111,7 @@ const executeTest = (projectRealPath) => {

execa.sync('rimraf', [testCasesDest]);
delete process.env.NG_VERSION;
delete process.env.SKIP_TEST;
delete process.env.ISOLATED_MODULES;
};

const cwd = process.cwd();
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/__helpers__/test-helpers.ts
@@ -0,0 +1,12 @@
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
export const jestCfgStub = {
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
testRegex: ['(/__tests__/.*|(\\\\.|/)(test|spec))\\\\.[jt]sx?$'],
globals: {
'ts-jest': {
diagnostics: {
pretty: false,
},
},
},
} as any; // eslint-disable-line @typescript-eslint/no-explicit-any
1 change: 1 addition & 0 deletions src/__tests__/__mocks__/app.component.html
@@ -0,0 +1 @@
<h1>App works</h1>
3 changes: 3 additions & 0 deletions src/__tests__/__mocks__/app.component.scss
@@ -0,0 +1,3 @@
h1 {
font-size: 1.6rem;
}
9 changes: 8 additions & 1 deletion src/__tests__/__mocks__/app.component.ts
Expand Up @@ -3,7 +3,14 @@ import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
styleUrls: ['./app.component.scss', './foo.component.css'],
styles: [
`
h1 {
font-size: 1.6rem;
}
`,
],
})
export class AppComponent {
title = 'test-app-v10';
Expand Down
46 changes: 46 additions & 0 deletions src/__tests__/__snapshots__/replace-resources.spec.ts.snap
@@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Replace resources transformer should use inline-files + strip-styles for isolatedModules true 1`] = `
"\\"use strict\\";
Object.defineProperty(exports, \\"__esModule\\", { value: true });
exports.AppComponent = void 0;
const tslib_1 = require(\\"tslib\\");
const core_1 = require(\\"@angular/core\\");
let AppComponent = class AppComponent {
constructor() {
this.title = 'test-app-v10';
}
};
AppComponent = tslib_1.__decorate([
core_1.Component({
selector: 'app-root',
template: require('./app.component.html'),
styleUrls: [],
styles: [],
})
], AppComponent);
exports.AppComponent = AppComponent;
//# "
`;

exports[`Replace resources transformer should use replaceResources transformer from @angular/compiler-cli for isolatedModules false 1`] = `
"\\"use strict\\";
Object.defineProperty(exports, \\"__esModule\\", { value: true });
exports.AppComponent = void 0;
const tslib_1 = require(\\"tslib\\");
const core_1 = require(\\"@angular/core\\");
let AppComponent = class AppComponent {
constructor() {
this.title = 'test-app-v10';
}
};
AppComponent = tslib_1.__decorate([
core_1.Component({
selector: 'app-root',
template: require(\\"./app.component.html\\"),
styles: []
})
], AppComponent);
exports.AppComponent = AppComponent;
//# "
`;
52 changes: 52 additions & 0 deletions src/__tests__/replace-resources.spec.ts
@@ -0,0 +1,52 @@
import { readFileSync } from 'fs';
import { join } from 'path';
import { SOURCE_MAPPING_PREFIX } from 'ts-jest/dist/compiler/compiler-utils';

import { NgJestConfig } from '../config/ng-jest-config';
import { jestCfgStub } from './__helpers__/test-helpers';
import { NgJestCompiler } from '../compiler/ng-jest-compiler';

describe('Replace resources transformer', () => {
const fileName = join(__dirname, '__mocks__', 'app.component.ts');
const fileContent = readFileSync(fileName, 'utf-8');

test('should use replaceResources transformer from @angular/compiler-cli for isolatedModules false', () => {
const ngJestConfig = new NgJestConfig({
...jestCfgStub,
globals: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
'ts-jest': {
...jestCfgStub.globals['ts-jest'],
isolatedModules: false,
},
},
});
const compiler = new NgJestCompiler(ngJestConfig, new Map());

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const emittedResult = compiler.getCompiledOutput(fileName, fileContent, false)!;

// Source map is different based on file location which can fail on CI, so we only compare snapshot for js
expect(emittedResult.substring(0, emittedResult.indexOf(SOURCE_MAPPING_PREFIX))).toMatchSnapshot();
});

test('should use inline-files + strip-styles for isolatedModules true', () => {
const ngJestConfig = new NgJestConfig({
...jestCfgStub,
globals: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
'ts-jest': {
...jestCfgStub.globals['ts-jest'],
isolatedModules: true,
},
},
});
const compiler = new NgJestCompiler(ngJestConfig, new Map());

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const emittedResult = compiler.getCompiledOutput(fileName, fileContent, false)!;

// Source map is different based on file location which can fail on CI, so we only compare snapshot for js
expect(emittedResult.substring(0, emittedResult.indexOf(SOURCE_MAPPING_PREFIX))).toMatchSnapshot();
});
});
31 changes: 17 additions & 14 deletions src/compiler/ng-jest-compiler.ts
Expand Up @@ -9,6 +9,7 @@ import { factory as downlevelCtor } from '../transformers/downlevel-ctor';
import { factory as inlineFiles } from '../transformers/inline-files';
import { factory as stripStyles } from '../transformers/strip-styles';
import { NgJestCompilerHost } from './compiler-host';
import { replaceResources } from '../transformers/replace-resources';

export class NgJestCompiler implements CompilerInstance {
private _compilerOptions!: CompilerOptions;
Expand Down Expand Up @@ -36,15 +37,9 @@ export class NgJestCompiler implements CompilerInstance {

getCompiledOutput(fileName: string, fileContent: string, supportsStaticESM: boolean): string {
const customTransformers = this.ngJestConfig.customTransformers;
const transformers = {
...customTransformers,
before: [
// hoisting from `ts-jest` or other before transformers
...(customTransformers.before as ts.TransformerFactory<ts.SourceFile>[]),
inlineFiles(this.ngJestConfig),
stripStyles(this.ngJestConfig),
],
};
const isAppPath = (fileName: string) => !fileName.endsWith('.ngfactory.ts') && !fileName.endsWith('.ngstyle.ts');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const getTypeChecker = () => this._program!.getTypeChecker();
if (this._program) {
const allDiagnostics = [];
if (!this._rootNames.includes(fileName)) {
Expand All @@ -60,17 +55,17 @@ export class NgJestCompiler implements CompilerInstance {

const sourceFile = this._program.getSourceFile(fileName);
const emitResult = this._program.emit(sourceFile, undefined, undefined, undefined, {
...transformers,
...customTransformers,
before: [
// hoisting from `ts-jest` or other before transformers
...transformers.before,
...(customTransformers.before as ts.TransformerFactory<ts.SourceFile>[]),
/**
* Downlevel constructor parameters for DI support. This is required to support forwardRef in ES2015 due to
* TDZ issues. This wrapper is needed here due to the program not being available until after
* the transformers are created. Also because program can be updated so we can't push this transformer in
* _createCompilerHost
*/
downlevelCtor(this._program),
replaceResources(isAppPath, getTypeChecker),
],
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand Down Expand Up @@ -110,7 +105,15 @@ export class NgJestCompiler implements CompilerInstance {

const result: ts.TranspileOutput = this._ts.transpileModule(fileContent, {
fileName,
transformers,
transformers: {
...customTransformers,
before: [
// hoisting from `ts-jest` or other before transformers
...(customTransformers.before as ts.TransformerFactory<ts.SourceFile>[]),
inlineFiles(this.ngJestConfig),
stripStyles(this.ngJestConfig),
],
},
compilerOptions: {
...this._compilerOptions,
module: moduleKind,
Expand All @@ -127,7 +130,7 @@ export class NgJestCompiler implements CompilerInstance {
}
}

private _setupOptions({ parsedTsConfig }: NgJestConfig) {
private _setupOptions({ parsedTsConfig }: NgJestConfig): void {
this._logger.debug({ parsedTsConfig }, '_setupOptions: initializing compiler config');

this._compilerOptions = { ...parsedTsConfig.options };
Expand Down
12 changes: 12 additions & 0 deletions src/constants.ts
@@ -0,0 +1,12 @@
/** Angular component decorator templateUrl property name */
export const TEMPLATE_URL = 'templateUrl';
/** Angular component decorator styleUrls property name */
export const STYLE_URLS = 'styleUrls';
/** Angular component decorator styles property name */
export const STYLES = 'styles';
/** Angular component decorator template property name */
export const TEMPLATE = 'template';
/** Node require function name */
export const REQUIRE = 'require';
/** Angular component decorator name */
export const COMPONENT = 'Component';
28 changes: 17 additions & 11 deletions src/transformers/inline-files.ts
Expand Up @@ -31,22 +31,16 @@ import type {
Visitor,
PropertyAssignment,
LiteralLikeNode,
StringLiteral,
} from 'typescript';
import { getCreateStringLiteral, ConfigSet } from './transform-utils';
import type { ConfigSet } from 'ts-jest/dist/config/config-set';

import { TEMPLATE_URL, STYLE_URLS, REQUIRE, TEMPLATE } from '../constants';

// replace original ts-jest ConfigSet with this simple interface, as it would require
// jest-preset-angular to add several babel devDependencies to get the other types
// inside the ConfigSet right

/** Angular component decorator TemplateUrl property name */
const TEMPLATE_URL = 'templateUrl';
/** Angular component decorator StyleUrls property name */
const STYLE_URLS = 'styleUrls';
/** Angular component decorator Template property name */
const TEMPLATE = 'template';
/** Node require function name */
const REQUIRE = 'require';

/**
* Property names anywhere in an angular project to transform
*/
Expand Down Expand Up @@ -74,8 +68,20 @@ export function factory(cs: ConfigSet): (ctx: TransformationContext) => Transfor
* Our compiler (typescript, or a module with typescript-like interface)
*/
const ts = cs.compilerModule;
function getCreateStringLiteral(): typeof ts.createStringLiteral {
if (ts.createStringLiteral && typeof ts.createStringLiteral === 'function') {
return ts.createStringLiteral;
}

const createStringLiteral = getCreateStringLiteral(ts);
return function createStringLiteral(text: string) {
const node = <StringLiteral>ts.createNode(ts.SyntaxKind.StringLiteral, -1, -1);
node.text = text;
node.flags |= ts.NodeFlags.Synthesized;

return node;
};
}
const createStringLiteral = getCreateStringLiteral();

/**
* Traverses the AST down to the relevant assignments anywhere in the file
Expand Down

0 comments on commit 1b20c19

Please sign in to comment.