Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vitest-angular): introduce application bundle-based Vitest builder #1443

Merged
merged 9 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions libs/card/src/lib/card/card.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ describe('CardComponent', () => {
let fixture: ComponentFixture<CardComponent>;
let component: CardComponent;

beforeEach(() =>
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CardComponent],
})
);
});
});

beforeEach(() => {
fixture = TestBed.createComponent(CardComponent);
Expand Down
17 changes: 17 additions & 0 deletions libs/card/src/lib/card2/__snapshots__/card.component.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`CardComponent > should create the app 1`] = `
<lib-card>
card-works

<button
color="primary"
mat-button=""
>
Render card2
</button>
<!--bindings={
"ng-reflect-ng-if": "false"
}-->
</lib-card>
`;
Empty file.
3 changes: 3 additions & 0 deletions libs/card/src/lib/card2/card.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{ title }}
<button mat-button color="primary" (click)="render = true">Render card2</button>
<mat-card *ngIf="render"> Some card </mat-card>
54 changes: 54 additions & 0 deletions libs/card/src/lib/card2/card.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';
import { MatCardHarness } from '@angular/material/card/testing';

import { CardComponent } from './card.component';

describe('CardComponent', () => {
let loader: HarnessLoader;
let fixture: ComponentFixture<CardComponent>;
let component: CardComponent;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [CardComponent],
});
});

beforeEach(() => {
fixture = TestBed.createComponent(CardComponent);
component = fixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(fixture);
fixture.detectChanges();
});

it('should show a card once we click on the button', async () => {
const button = await loader.getHarness(MatButtonHarness);

const card = await loader.getHarnessOrNull(MatCardHarness);
expect(card).toBeNull();

await button.click();

const cardAfterClick = await loader.getHarnessOrNull(MatCardHarness);
expect(cardAfterClick).not.toBeNull();
});

it('should create the app', () => {
expect(component).toBeTruthy();
});

it(`should have as title 'vitetest'`, () => {
expect(component.title).toEqual('card-works');
});

it('should create the app', () => {
expect(fixture).toMatchSnapshot();
});

it.skip('should skip this test', () => {
expect(false).toBeTruthy();
});
});
15 changes: 15 additions & 0 deletions libs/card/src/lib/card2/card.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';

@Component({
selector: 'lib-card',
standalone: true,
imports: [CommonModule, MatCardModule],
templateUrl: './card.component.html',
styleUrls: ['./card.component.css'],
})
export class CardComponent {
title = 'card-works';
render = false;
}
6 changes: 2 additions & 4 deletions libs/card/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
{
"compilerOptions": {
"target": "es2022",
"useDefineForClassFields": false,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true
},
"files": [],
"include": [],
Expand Down
5 changes: 4 additions & 1 deletion libs/card/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
"types": [],
"esModuleInterop": true,
"isolatedModules": true,
"moduleResolution": "bundler"
},
"exclude": ["src/**/*.spec.ts", "vite.config.ts"],
"include": ["src/**/*.ts"]
Expand Down
3 changes: 1 addition & 2 deletions libs/card/tsconfig.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node", "vitest/globals"],
"target": "es2016"
"types": ["node", "vitest/globals"]
},
"files": ["src/test-setup.ts"],
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
Expand Down
9 changes: 7 additions & 2 deletions packages/vitest-angular/builders.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
{
"builders": {
"test": {
"implementation": "./src/lib/vitest.impl",
"implementation": "./src/lib/builders/test/vitest.impl",
"schema": "./src/lib/schema.json",
"description": "Test with Vitest"
"description": "Run tests with Vitest"
},
"build-test": {
"implementation": "./src/lib/builders/build/vitest.impl",
"schema": "./src/lib/builders/build/schema.json",
"description": "Bundle and run tests with Vitest using the Application Builder"
}
}
}
5 changes: 3 additions & 2 deletions packages/vitest-angular/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import vitestBuilder from './lib/vitest.impl';
import vitestBuilder from './lib/builders/test/vitest.impl';
import vitestApplicationBuilder from './lib/builders/build/vitest.impl';

export { vitestBuilder };
export { vitestBuilder, vitestApplicationBuilder };
29 changes: 29 additions & 0 deletions packages/vitest-angular/src/lib/builders/build/devkit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export async function getBuildApplicationFunction() {
const { VERSION } = await (Function(
'return import("@angular/compiler-cli")'
)() as Promise<{ VERSION: { major: string; minor: string } }>);

const angularVersion = Number(VERSION.major);
const angularMinor = Number(VERSION.minor);
let buildApplicationInternal: Function;

if (angularVersion < 16 || (angularVersion === 16 && angularMinor <= 2)) {
throw new Error(
'This builder is not supported with versions earlier than Angular v16.2'
);
} else if (angularVersion >= 16 && angularVersion < 18) {
const {
buildApplicationInternal: buildApplicationInternalFn,
} = require('@angular-devkit/build-angular/src/builders/application');

buildApplicationInternal = buildApplicationInternalFn;
} else {
const {
buildApplicationInternal: buildApplicationInternalFn,
} = require('@angular/build/private');

buildApplicationInternal = buildApplicationInternalFn;
}

return { buildApplicationInternal, angularVersion };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

// import assert from 'node:assert';
import { dirname, join, relative, resolve } from 'node:path';

import { AngularMemoryOutputFiles } from '../utils';

interface AngularMemoryPluginOptions {
workspaceRoot?: string;
angularVersion: number;
outputFiles: AngularMemoryOutputFiles;
external?: string[];
}

export async function createAngularMemoryPlugin(
options: AngularMemoryPluginOptions
) {
const { normalizePath } = await (Function(
'return import("vite")'
)() as Promise<typeof import('vite')>);
const { outputFiles, external } = options;
let config;
let projectRoot: string;
const workspaceRoot = options?.workspaceRoot || process.cwd();

return {
name: 'vite:angular-memory',
// Ensures plugin hooks run before built-in Vite hooks
enforce: 'pre',
config(userConfig: any) {
config = userConfig;
projectRoot = resolve(workspaceRoot, config.root || '.');
},
async resolveId(source: string, importer: string) {
// Prevent vite from resolving an explicit external dependency (`externalDependencies` option)
if (external?.includes(source)) {
// This is still not ideal since Vite will still transform the import specifier to
// `/@id/${source}` but is currently closer to a raw external than a resolved file path.
return source;
}

if (importer) {
if (
source[0] === '.' &&
normalizePath(importer).startsWith(projectRoot)
) {
// Remove query if present
const [importerFile] = importer.split('?', 1);
source =
'/' + join(dirname(relative(projectRoot, importerFile)), source);
}
}

const [file] = source.split('?', 1);
const fileSplits = file.split('/');

if (outputFiles.has(fileSplits[fileSplits.length - 1])) {
return fileSplits[fileSplits.length - 1];
}

if (outputFiles.has(file)) {
return join(projectRoot, source);
}
return;
},
load(id: string) {
const [file] = id.split('?', 1);
const relativeFile =
options.angularVersion < 19
? normalizePath(relative(projectRoot, file))
.replace(/^.*\//, '')
.replace('.ts', '.js')
: 'spec-' +
normalizePath(relative(projectRoot, file))
.replace('.ts', '.js')
.replace(/^[./]+/, '_')
.replace(/\//g, '-');

const codeContents =
outputFiles.get(relativeFile)?.contents ||
outputFiles.get(id)?.contents;
if (codeContents === undefined) {
return undefined;
}

const code = Buffer.from(codeContents).toString('utf-8');
const mapContents = outputFiles.get(relativeFile + '.map')?.contents;

return {
// Remove source map URL comments from the code if a sourcemap is present.
// Vite will inline and add an additional sourcemap URL for the sourcemap.
code: mapContents
? code.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '')
: code,
map: mapContents && Buffer.from(mapContents).toString('utf-8'),
};
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export async function esbuildDownlevelPlugin() {
const { transformWithEsbuild } = await (Function(
'return import("vite")'
)() as Promise<typeof import('vite')>);
return {
name: 'analogs-vitest-esbuild-downlevel-plugin',
async transform(_code: string, id: string) {
if (_code.includes('async (')) {
const { code, map } = await transformWithEsbuild(_code, id, {
loader: 'js',
format: 'esm',
target: 'es2016',
sourcemap: true,
sourcefile: id,
});

return {
code,
map,
};
}

return undefined;
},
};
}
9 changes: 9 additions & 0 deletions packages/vitest-angular/src/lib/builders/build/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface VitestSchema {
mode?: string;
setupFile: string;
configFile?: string;
include: string[];
exclude?: string[];
watch?: boolean;
tsConfig: string;
}
47 changes: 47 additions & 0 deletions packages/vitest-angular/src/lib/builders/build/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Vitest schema for Test Facade.",
"description": "Vitest target options",
"type": "object",
"properties": {
"configFile": {
"type": "string",
"description": "The path to the local vitest config",
"x-completion-type": "file",
"x-completion-glob": "@vitest.config@(.js|.ts|.mts)",
"aliases": ["config"]
},
"include": {
"type": "array",
"items": {
"type": "string"
},
"default": ["src/**/*.spec.ts"],
"description": "Globs of files to include, relative to project root."
},
"exclude": {
"type": "array",
"items": {
"type": "string"
},
"default": ["node_modules", "dist", ".idea", ".git", ".cache"],
"description": "Globs of files to exclude, relative to the project root."
},
"setupFile": {
"type": "string",
"description": "The path to the setup file.",
"default": "src/test-setup.ts"
},
"tsConfig": {
"type": "string",
"description": "The relative path to the TypeScript configuration file for running tests.",
"default": "tsconfig.spec.json"
},
"watch": {
"type": "boolean",
"default": false,
"description": "Run the tests in watch mode"
}
},
"additionalProperties": true
}
4 changes: 4 additions & 0 deletions packages/vitest-angular/src/lib/builders/build/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type AngularMemoryOutputFiles = Map<
string,
{ contents: Uint8Array; hash: string; servable: boolean }
>;
Loading