Skip to content

Commit d489436

Browse files
feat(qwik-nx): build executor should run type checking (#143)
* feat(qwik-nx): build executor should run type checking * test: build type check e2e
1 parent c598bf0 commit d489436

File tree

8 files changed

+271
-26
lines changed

8 files changed

+271
-26
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`appGenerator e2e Basic behavior with app generator should run type checking before the build step: stderr 1`] = `""`;
4+
5+
exports[`appGenerator e2e Basic behavior with app generator should run type checking before the build step: stdout 1`] = `
6+
"
7+
> nx run PROJECT_NAME:build
8+
9+
>  NX  Running type check for the \\"PROJECT_NAME\\"..
10+
npx tsc --incremental --noEmit --pretty -p REPLACED_PATH/tsconfig.app.json
11+
apps/PROJECT_NAME/src/routes/index.tsx:8:15 - error TS2322: Type 'string' is not assignable to type 'number'.
12+
8 a = 'not-a-number';
13+
   ~
14+
libs/LIB_PROJECT_NAME/src/lib/LIB_PROJECT_NAME.tsx:7:22 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
15+
7 b.push(1);
16+
   ~
17+
Found 2 errors in 2 files.
18+
Errors Files
19+
1 apps/PROJECT_NAME/src/routes/index.tsx:8
20+
1 libs/LIB_PROJECT_NAME/src/lib/LIB_PROJECT_NAME.tsx:7
21+
22+
23+
24+
> NX Running target build for project PROJECT_NAME failed
25+
26+
Failed tasks:
27+
28+
- PROJECT_NAME:build
29+
30+
Hint: run the command with --verbose for more details.
31+
32+
"
33+
`;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`presetGenerator e2e Basic behavior with preset generator should run type checking before the build step: stderr 1`] = `""`;
4+
5+
exports[`presetGenerator e2e Basic behavior with preset generator should run type checking before the build step: stdout 1`] = `
6+
"
7+
> nx run PROJECT_NAME:build
8+
9+
>  NX  Running type check for the \\"PROJECT_NAME\\"..
10+
npx tsc --incremental --noEmit --pretty -p REPLACED_PATH/tsconfig.app.json
11+
apps/PROJECT_NAME/src/routes/index.tsx:8:15 - error TS2322: Type 'string' is not assignable to type 'number'.
12+
8 a = 'not-a-number';
13+
   ~
14+
libs/LIB_PROJECT_NAME/src/lib/LIB_PROJECT_NAME.tsx:7:22 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
15+
7 b.push(1);
16+
   ~
17+
Found 2 errors in 2 files.
18+
Errors Files
19+
1 apps/PROJECT_NAME/src/routes/index.tsx:8
20+
1 libs/LIB_PROJECT_NAME/src/lib/LIB_PROJECT_NAME.tsx:7
21+
22+
23+
24+
> NX Running target build for project PROJECT_NAME failed
25+
26+
Failed tasks:
27+
28+
- PROJECT_NAME:build
29+
30+
Hint: run the command with --verbose for more details.
31+
32+
"
33+
`;

e2e/qwik-nx-e2e/tests/application-basic-behavior.suite.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ensureNxProject,
66
runNxCommandAsync,
77
uniq,
8+
updateFile,
89
} from '@nx/plugin/testing';
910

1011
import {
@@ -15,12 +16,17 @@ import {
1516
killPorts,
1617
DEFAULT_E2E_TIMEOUT,
1718
} from '@qwikifiers/e2e/utils';
19+
import { names } from '@nx/devkit';
1820

1921
export function testApplicationBasicBehavior(generator: 'app' | 'preset') {
2022
const devServerPort = 4212;
2123
const previewServerPort = 4232;
2224
describe(`Basic behavior with ${generator} generator`, () => {
2325
let project: string;
26+
let libProject: string;
27+
let rootRoutePath: string;
28+
let libComponentName: string;
29+
let libComponentPath: string;
2430

2531
beforeAll(async () => {
2632
await killPorts(devServerPort, previewServerPort);
@@ -29,16 +35,30 @@ export function testApplicationBasicBehavior(generator: 'app' | 'preset') {
2935

3036
beforeAll(async () => {
3137
project = uniq('qwik-nx');
38+
libProject = uniq('qwik-nx');
39+
rootRoutePath = `apps/${project}/src/routes/index.tsx`;
40+
libComponentPath = `libs/${libProject}/src/lib/${libProject}.tsx`;
41+
libComponentName = names(libProject).className;
3242

3343
const projectNameParam =
3444
generator === 'preset' ? `--qwikAppName=${project}` : project;
3545

3646
await runNxCommandAsync(
3747
`generate qwik-nx:${generator} ${projectNameParam} --no-interactive`
3848
);
49+
await runNxCommandAsync(
50+
`generate qwik-nx:library ${libProject} --no-interactive`
51+
);
3952
await runNxCommandAsync(
4053
`generate qwik-nx:component my-test-component --project=${project} --unitTestRunner=vitest --no-interactive`
4154
);
55+
updateFile(rootRoutePath, (content) => {
56+
content =
57+
`import { ${libComponentName} } from '@proj/${libProject}';\n` +
58+
content;
59+
content = content.replace('<h1>', `<${libComponentName}/>\n<h1>`);
60+
return content;
61+
});
4262
}, DEFAULT_E2E_TIMEOUT);
4363

4464
afterAll(async () => {
@@ -53,6 +73,9 @@ export function testApplicationBasicBehavior(generator: 'app' | 'preset') {
5373
'should create qwik-nx',
5474
async () => {
5575
const result = await runNxCommandAsync(`build ${project}`);
76+
expect(result.stdout).toContain(
77+
`Running type check for the "${project}"..`
78+
);
5679
expect(
5780
stripConsoleColors(result.stdout.replace(/\n|\s/g, ''))
5881
).toContain(
@@ -77,6 +100,53 @@ export function testApplicationBasicBehavior(generator: 'app' | 'preset') {
77100
DEFAULT_E2E_TIMEOUT
78101
);
79102

103+
it(
104+
'should run type checking before the build step',
105+
async () => {
106+
let originalContent!: string;
107+
let originalLibContent!: string;
108+
updateFile(rootRoutePath, (content) => {
109+
originalContent = content;
110+
return content.replace(
111+
'export default component$(() => {',
112+
`export default component$(() => {
113+
let a = 0;
114+
a = 'not-a-number';
115+
`
116+
);
117+
});
118+
updateFile(libComponentPath, (content) => {
119+
originalLibContent = content;
120+
return content.replace(
121+
'component$(() => {',
122+
`component$(() => {
123+
let b: string[] = ['some-string'];
124+
b.push(1);
125+
`
126+
);
127+
});
128+
129+
const result = await runNxCommandAsync(`build ${project} --verbose`, {
130+
silenceError: true,
131+
});
132+
133+
// restore original contents
134+
updateFile(rootRoutePath, originalContent);
135+
updateFile(libComponentPath, originalLibContent);
136+
137+
function replaceDynamicContent(output: string) {
138+
return output
139+
.replaceAll(project, 'PROJECT_NAME')
140+
.replaceAll(libProject, 'LIB_PROJECT_NAME')
141+
.replace(/-p .+/, '-p REPLACED_PATH/tsconfig.app.json');
142+
}
143+
144+
expect(replaceDynamicContent(result.stderr)).toMatchSnapshot('stderr');
145+
expect(replaceDynamicContent(result.stdout)).toMatchSnapshot('stdout');
146+
},
147+
DEFAULT_E2E_TIMEOUT
148+
);
149+
80150
it(
81151
'unit tests should pass in the created app',
82152
async () => {

e2e/qwik-nx-e2e/tsconfig.spec.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"extends": "./tsconfig.json",
33
"compilerOptions": {
44
"outDir": "../../dist/out-tsc",
5+
"lib": ["es2022", "dom"],
56
"module": "commonjs",
67
"types": ["jest", "node"]
78
},

packages/qwik-nx/src/executors/build/executor.spec.ts

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import { BuildExecutorSchema } from './schema';
22
import executor from './executor';
3-
import { ExecutorContext, runExecutor, Target } from '@nx/devkit';
3+
import {
4+
ExecutorContext,
5+
ProjectsConfigurations,
6+
runExecutor,
7+
Target,
8+
} from '@nx/devkit';
9+
import { resolve } from 'path';
410

511
// eslint-disable-next-line @typescript-eslint/no-var-requires
612
const devkit: { runExecutor: typeof runExecutor } = require('@nx/devkit');
13+
// eslint-disable-next-line @typescript-eslint/no-var-requires
14+
const fs = require('fs');
715

816
enum MockFailTargets {
917
NoSuccess = 'mock-no-success',
@@ -12,6 +20,15 @@ enum MockFailTargets {
1220

1321
describe('Build Executor', () => {
1422
let runExecutorPayloads: Target[] = [];
23+
const defaultContext = {
24+
root: '/root',
25+
projectName: 'my-app',
26+
targetName: 'build',
27+
configurationName: 'production',
28+
projectsConfigurations: {
29+
projects: { 'my-app': { projectType: 'application', root: '/root' } },
30+
} as any as ProjectsConfigurations,
31+
} as ExecutorContext;
1532

1633
jest.spyOn(devkit, 'runExecutor').mockImplementation((target: Target) => {
1734
if (target.target === MockFailTargets.NoSuccess) {
@@ -44,17 +61,11 @@ describe('Build Executor', () => {
4461
});
4562

4663
it('should execute targets sequentially', async () => {
47-
const context = {
48-
root: '/root',
49-
projectName: 'my-app',
50-
targetName: 'build',
51-
configurationName: 'production',
52-
} as ExecutorContext;
53-
5464
const options: BuildExecutorSchema = {
5565
runSequence: ['my-app:target1:development', 'my-app:target2'],
66+
skipTypeCheck: true,
5667
};
57-
const iterable = executor(options, context);
68+
const iterable = executor(options, defaultContext);
5869
let result = await iterable.next();
5970
expect(result.value?.success).toEqual(true);
6071
expect(runExecutorPayloads).toEqual([
@@ -66,13 +77,6 @@ describe('Build Executor', () => {
6677
});
6778

6879
it('should stop execution if executor returned "success: false"', async () => {
69-
const context = {
70-
root: '/root',
71-
projectName: 'my-app',
72-
targetName: 'build',
73-
configurationName: 'production',
74-
} as ExecutorContext;
75-
7680
const target = MockFailTargets.NoSuccess;
7781

7882
const options: BuildExecutorSchema = {
@@ -81,8 +85,9 @@ describe('Build Executor', () => {
8185
`my-app:${target}`,
8286
'my-app:target2',
8387
],
88+
skipTypeCheck: true,
8489
};
85-
const iterable = executor(options, context);
90+
const iterable = executor(options, defaultContext);
8691
let result = await iterable.next();
8792
expect(result.value?.success).toEqual(false);
8893
expect(runExecutorPayloads).toEqual([
@@ -94,13 +99,6 @@ describe('Build Executor', () => {
9499
});
95100

96101
it('should stop execution if unhandled error occurs', async () => {
97-
const context = {
98-
root: '/root',
99-
projectName: 'my-app',
100-
targetName: 'build',
101-
configurationName: 'production',
102-
} as ExecutorContext;
103-
104102
const target = MockFailTargets.Error;
105103

106104
const options: BuildExecutorSchema = {
@@ -109,8 +107,9 @@ describe('Build Executor', () => {
109107
`my-app:${target}`,
110108
'my-app:target2',
111109
],
110+
skipTypeCheck: true,
112111
};
113-
const iterable = executor(options, context);
112+
const iterable = executor(options, defaultContext);
114113
let result = await iterable.next();
115114
expect(result.value?.success).toEqual(false);
116115
expect(runExecutorPayloads).toEqual([
@@ -120,4 +119,33 @@ describe('Build Executor', () => {
120119
result = await iterable.next();
121120
expect(result.done).toEqual(true);
122121
});
122+
123+
describe('type check', () => {
124+
const defaultOptionsForTypeCheck: BuildExecutorSchema = {
125+
runSequence: ['my-app:target1:development', 'my-app:target2'],
126+
};
127+
128+
it('should throw an error if tsconfig is not found at default location', async () => {
129+
jest.spyOn(fs, 'existsSync').mockReturnValue(false);
130+
const iterable = executor(defaultOptionsForTypeCheck, defaultContext);
131+
await expect(iterable.next()).rejects.toThrow(
132+
`Could not find tsconfig at "${resolve(
133+
'/root/tsconfig.app.json'
134+
)}". If project's tsconfig file name is not standard, provide the path to it as "tsConfig" executor option.`
135+
);
136+
});
137+
it('should throw an error if tsconfig is not found at specified location', async () => {
138+
jest.spyOn(fs, 'existsSync').mockReturnValue(false);
139+
const iterable = executor(
140+
{
141+
...defaultOptionsForTypeCheck,
142+
tsConfig: '/root/tsconfig.other.json',
143+
},
144+
defaultContext
145+
);
146+
await expect(iterable.next()).rejects.toThrow(
147+
`Could not find tsconfig at "${resolve('/root/tsconfig.other.json')}".`
148+
);
149+
});
150+
});
123151
});

0 commit comments

Comments
 (0)