chore(core): refactor explicit-project-dependencies unit tests (#10317)

This commit is contained in:
James Henry 2022-05-18 13:02:59 +04:00 committed by GitHub
parent f25e94a5dc
commit a89c360f46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,223 +1,656 @@
import { vol } from 'memfs';
import type { Workspace } from '../../config/workspace-json-project-json';
import { defaultFileHasher } from '../../hasher/file-hasher';
import { createProjectFileMap } from '../file-map-utils';
import { ProjectGraphBuilder } from '../project-graph-builder';
import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies';
jest.mock('fs', () => require('memfs').fs);
jest.mock('nx/src/utils/app-root', () => ({
workspaceRoot: '/root',
}));
import { vol } from 'memfs';
import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies';
import { defaultFileHasher } from '../../hasher/file-hasher';
import {
ProjectGraphProcessorContext,
ProjectGraphProjectNode,
} from '../../config/project-graph';
import { ProjectGraphBuilder } from '../project-graph-builder';
// projectName => tsconfig import path
const dependencyProjectNamesToImportPaths = {
proj2: '@proj/my-second-proj',
proj3a: '@proj/project-3',
proj4ab: '@proj/proj4ab',
};
describe('explicit project dependencies', () => {
let ctx: ProjectGraphProcessorContext;
let projects: Record<string, ProjectGraphProjectNode>;
let fsJson;
beforeEach(() => {
const workspaceJson = {
projects: {
proj: {
root: 'libs/proj',
},
proj2: {
root: 'libs/proj2',
},
proj3a: {
root: 'libs/proj3a',
},
proj123: {
root: 'libs/proj123',
},
proj1234: {
root: 'libs/proj1234',
},
'proj1234-child': {
root: 'libs/proj1234-child',
},
},
};
const nxJson = {
npmScope: 'proj',
};
const tsConfig = {
compilerOptions: {
baseUrl: '.',
paths: {
'@proj/proj': ['libs/proj/index.ts'],
'@proj/my-second-proj': ['libs/proj2/index.ts'],
'@proj/project-3': ['libs/proj3a/index.ts'],
'@proj/proj123': ['libs/proj123/index.ts'],
'@proj/proj1234': ['libs/proj1234/index.ts'],
'@proj/proj1234-child': ['libs/proj1234-child/index.ts'],
'@proj/proj4ab': ['libs/proj4ab/index.ts'],
},
},
};
fsJson = {
'./package.json': `{
"name": "test",
"dependencies": [],
"devDependencies": []
}`,
'./workspace.json': JSON.stringify(workspaceJson),
'./nx.json': JSON.stringify(nxJson),
'./tsconfig.base.json': JSON.stringify(tsConfig),
'./libs/proj/index.ts': `import {a} from '@proj/my-second-proj';
import('@proj/project-3');
const a = { loadChildren: '@proj/proj4ab#a' };
`,
'./libs/proj2/index.ts': `export const a = 2;`,
'./libs/proj3a/index.ts': `export const a = 3;`,
'./libs/proj4ab/index.ts': `export const a = 4;`,
'./libs/proj123/index.ts': 'export const a = 5',
'./libs/proj1234/index.ts': `export const a = 6
import { a } from '@proj/proj1234-child'
`,
'./libs/proj1234-child/index.ts': 'export const a = 7',
'./libs/proj1234/a.b.ts': `// nx-ignore-next-line
import('@proj/proj2')
/* nx-ignore-next-line */
import {a} from '@proj/proj3a
`,
'./libs/proj1234/b.c.ts': `// nx-ignore-next-line
require('@proj/proj4ab#a')
// nx-ignore-next-line
import('@proj/proj2')
/* nx-ignore-next-line */
import {a} from '@proj/proj3a
const a = {
// nx-ignore-next-line
loadChildren: '@proj/3a'
}
const b = {
// nx-ignore-next-line
loadChildren: '@proj/3a',
children: [{
// nx-ignore-next-line
loadChildren: '@proj/proj2,
// nx-ignore-next-line
loadChildren: '@proj/proj3a'
}]
}
`,
};
vol.fromJSON(fsJson, '/root');
vol.reset();
});
defaultFileHasher.init();
describe('static imports, dynamic imports, and commonjs requires', () => {
it('should build explicit dependencies for static imports, and top-level dynamic imports and commonjs requires', () => {
const sourceProjectName = 'proj';
const { ctx, builder } = createVirtualWorkspace({
sourceProjectName,
sourceProjectFiles: [
{
path: 'libs/proj/index.ts',
content: `
import {a} from '@proj/my-second-proj';
await import('@proj/project-3');
require('@proj/proj4ab');
import * as npmPackage from 'npm-package';
`,
},
],
});
ctx = {
const res = buildExplicitTypeScriptDependencies(
ctx.workspace,
builder.graph,
ctx.filesToProcess
);
expect(res).toEqual([
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj2',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj3a',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj4ab',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'npm:npm-package',
},
]);
});
it('should build explicit dependencies for static exports', () => {
const sourceProjectName = 'proj';
const { ctx, builder } = createVirtualWorkspace({
sourceProjectName,
sourceProjectFiles: [
{
path: 'libs/proj/index.ts',
content: `
export {a} from '@proj/my-second-proj';
export * as project3 from '@proj/project-3';
export * from '@proj/proj4ab';
`,
},
],
});
const res = buildExplicitTypeScriptDependencies(
ctx.workspace,
builder.graph,
ctx.filesToProcess
);
expect(res).toEqual([
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj2',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj3a',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj4ab',
},
]);
});
it(`should build explicit dependencies for TypeScript's import/export require syntax, and side-effectful import`, () => {
const sourceProjectName = 'proj';
const { ctx, builder } = createVirtualWorkspace({
sourceProjectName,
sourceProjectFiles: [
{
path: 'libs/proj/index.ts',
content: `
import i = require("@proj/my-second-proj");
export import i = require("@proj/project-3");
import '@proj/proj4ab';
`,
},
],
});
const res = buildExplicitTypeScriptDependencies(
ctx.workspace,
builder.graph,
ctx.filesToProcess
);
expect(res).toEqual([
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj2',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj3a',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj4ab',
},
]);
});
it('should build explicit dependencies for nested dynamic imports and commonjs requires', () => {
const sourceProjectName = 'proj';
const { ctx, builder } = createVirtualWorkspace({
sourceProjectName,
sourceProjectFiles: [
{
path: 'libs/proj/nested-dynamic-import.ts',
content: `
async function nestedInAFunction() {
await import('@proj/project-3');
}
`,
},
{
path: 'libs/proj/nested-require.ts',
content: `
function nestedInAFunction() {
require('@proj/proj4ab');
}
`,
},
{
path: 'libs/proj/component.tsx',
content: `
export function App() {
import('@proj/my-second-proj')
return (
<GlobalStateProvider>
<Shell></Shell>
</GlobalStateProvider>
);
}
`,
},
],
});
const res = buildExplicitTypeScriptDependencies(
ctx.workspace,
builder.graph,
ctx.filesToProcess
);
expect(res).toEqual([
{
sourceProjectName,
sourceProjectFile: 'libs/proj/component.tsx',
targetProjectName: 'proj2',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/nested-dynamic-import.ts',
targetProjectName: 'proj3a',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/nested-require.ts',
targetProjectName: 'proj4ab',
},
]);
});
it('should build explicit dependencies when relative paths are used', () => {
const sourceProjectName = 'proj';
const { ctx, builder } = createVirtualWorkspace({
sourceProjectName,
sourceProjectFiles: [
{
path: 'libs/proj/absolute-path.ts',
content: `
import('../../libs/proj3a/index.ts');
`,
},
{
path: 'libs/proj/relative-path.ts',
content: `
import('../proj4ab/index.ts');
`,
},
],
});
const res = buildExplicitTypeScriptDependencies(
ctx.workspace,
builder.graph,
ctx.filesToProcess
);
expect(res).toEqual([
{
sourceProjectName,
sourceProjectFile: 'libs/proj/absolute-path.ts',
targetProjectName: 'proj3a',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/relative-path.ts',
targetProjectName: 'proj4ab',
},
]);
});
it('should not build explicit dependencies when nx-ignore-next-line comments are present', () => {
const sourceProjectName = 'proj';
const { ctx, builder } = createVirtualWorkspace({
sourceProjectName,
sourceProjectFiles: [
{
path: 'libs/proj/static-import-1.ts',
content: `
// nx-ignore-next-line
import {a} from '@proj/my-second-proj';
`,
},
{
path: 'libs/proj/static-import-2.ts',
content: `
/* nx-ignore-next-line */
import {a} from '@proj/my-second-proj';
`,
},
{
path: 'libs/proj/dynamic-import-1.ts',
content: `
// nx-ignore-next-line
await import('@proj/project-3');
`,
},
{
path: 'libs/proj/dynamic-import-2.ts',
content: `
/* nx-ignore-next-line */
await import('@proj/project-3');
`,
},
{
path: 'libs/proj/dynamic-import-3.ts',
content: `
async function nestedInAFunction() {
/* nx-ignore-next-line */
await import('@proj/project-3');
}
`,
},
{
path: 'libs/proj/require-1.ts',
content: `
// nx-ignore-next-line
require('@proj/proj4ab');
`,
},
{
path: 'libs/proj/require-2.ts',
content: `
/* nx-ignore-next-line */
require('@proj/proj4ab');
`,
},
{
path: 'libs/proj/require-3.ts',
content: `
function nestedInAFunction() {
/* nx-ignore-next-line */
require('@proj/proj4ab');
}
`,
},
{
path: 'libs/proj/comments-with-excess-whitespace.ts',
content: `
/*
nx-ignore-next-line
*/
require('@proj/proj4ab');
// nx-ignore-next-line
import('@proj/proj4ab');
/*
nx-ignore-next-line */
import { foo } from '@proj/proj4ab';
`,
},
],
});
const res = buildExplicitTypeScriptDependencies(
ctx.workspace,
builder.graph,
ctx.filesToProcess
);
expect(res).toEqual([]);
});
it('should not build explicit dependencies for stringified or templatized import/require statements', () => {
const sourceProjectName = 'proj';
const { ctx, builder } = createVirtualWorkspace({
sourceProjectName,
sourceProjectFiles: [
{
path: 'libs/proj/stringified-imports-and-require.ts',
content: `
"import {a} from '@proj/my-second-proj';";
'await import("@proj/my-second-proj");';
\`require('@proj/my-second-proj');\`
`,
},
/**
* TODO: Refactor usage of Scanner to fix these.
*
* The use of a template literal before a templatized import/require
* currently causes Nx to interpret the import/require as if they were not templatized and were declarared directly
* in the source code.
*/
// Also reported here: https://github.com/nrwl/nx/issues/8938
// {
// path: 'libs/proj/file-1.ts',
// content: `
// const npmScope = 'myorg';
// console.log(\`@\${npmScope}\`);
// console.log(\`import {foo} from '@proj/my-second-proj'\`)
// `,
// },
// {
// path: 'libs/proj/file-2.ts',
// content: `
// const v = \`\${val}
// \${val}
// \${val} \${val}
// \${val} \${val}
// \`;
// tree.write('/path/to/file.ts', \`import something from "@proj/project-3";\`);
// `,
// },
// {
// path: 'libs/proj/file-3.ts',
// content: `
// \`\${Tree}\`;
// \`\`;
// \`import { A } from '@proj/my-second-proj'\`;
// \`import { B, C, D } from '@proj/project-3'\`;
// \`require('@proj/proj4ab')\`;
// `,
// },
// {
// path: 'libs/proj/file-4.ts',
// // Ensure unterminated template literal does not break project graph creation
// content: `
// \`\${Tree\`;
// \`\`;
// \`import { A } from '@proj/my-second-proj'\`;
// \`import { B, C, D } from '@proj/project-3'\`;
// \`require('@proj/proj4ab')\`;
// `,
// },
],
});
const res = buildExplicitTypeScriptDependencies(
ctx.workspace,
builder.graph,
ctx.filesToProcess
);
expect(res).toEqual([]);
});
});
/**
* In version 8, Angular deprecated the loadChildren string syntax in favor of using dynamic imports, but it is still
* fully supported by the framework:
*
* https://angular.io/guide/deprecations#loadchildren-string-syntax
*/
describe('legacy Angular loadChildren string syntax', () => {
it('should build explicit dependencies for legacy Angular loadChildren string syntax', () => {
const sourceProjectName = 'proj';
const { ctx, builder } = createVirtualWorkspace({
sourceProjectName,
sourceProjectFiles: [
{
path: 'libs/proj/file-1.ts',
content: `
const a = { loadChildren: '@proj/proj4ab#a' };
`,
},
{
path: 'libs/proj/file-2.ts',
content: `
const routes: Routes = [{
path: 'lazy',
loadChildren: '@proj/project-3#LazyModule',
}];
`,
},
/**
* TODO: This case, where a no subsitution template literal is used, is not working
*/
// {
// path: 'libs/proj/no-substitution-template-literal.ts',
// content: `
// const a = {
// loadChildren: \`@proj/my-second-proj\`
// };
// `,
// },
],
});
const res = buildExplicitTypeScriptDependencies(
ctx.workspace,
builder.graph,
ctx.filesToProcess
);
expect(res).toEqual([
{
sourceProjectName,
sourceProjectFile: 'libs/proj/file-1.ts',
targetProjectName: 'proj4ab',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/file-2.ts',
targetProjectName: 'proj3a',
},
]);
});
it('should not build explicit dependencies when nx-ignore-next-line comments are present', () => {
const sourceProjectName = 'proj';
const { ctx, builder } = createVirtualWorkspace({
sourceProjectName,
sourceProjectFiles: [
{
path: 'libs/proj/file-1.ts',
content: `
const a = {
// nx-ignore-next-line
loadChildren: '@proj/proj4ab#a'
};
`,
},
/**
* TODO: This case, where a multi-line comment is used, is not working
*/
// {
// path: 'libs/proj/file-2.ts',
// content: `
// const a = {
// /* nx-ignore-next-line */
// loadChildren: '@proj/proj4ab#a'
// };
// `,
// },
/**
* TODO: These cases, where loadChildren is on the same line as the variable declaration, are not working
*/
// {
// path: 'libs/proj/file-3.ts',
// content: `
// // nx-ignore-next-line
// const a = { loadChildren: '@proj/proj4ab#a' };
// `,
// },
// {
// path: 'libs/proj/file-4.ts',
// content: `
// /* nx-ignore-next-line */
// const a = { loadChildren: '@proj/proj4ab#a' };
// `,
// },
],
});
const res = buildExplicitTypeScriptDependencies(
ctx.workspace,
builder.graph,
ctx.filesToProcess
);
expect(res).toEqual([]);
});
});
});
interface VirtualWorkspaceConfig {
sourceProjectName: string;
sourceProjectFiles: {
path: string;
content: string;
}[];
}
/**
* Prepares a minimal workspace and virtual file-system for the given files and dependency
* projects in order to be able to execute `buildExplicitTypeScriptDependencies()` in the tests.
*/
function createVirtualWorkspace(config: VirtualWorkspaceConfig) {
const nxJson = {
npmScope: 'proj',
};
const workspaceJson = {
projects: {
[config.sourceProjectName]: {
root: `libs/${config.sourceProjectName}`,
},
},
};
const fsJson = {
'./package.json': `{
"name": "test",
"dependencies": {
"npm-package": "1.0.0"
},
"devDependencies": []
}`,
'./workspace.json': JSON.stringify(workspaceJson),
'./nx.json': JSON.stringify(nxJson),
...config.sourceProjectFiles.reduce(
(acc, file) => ({
...acc,
[file.path]: file.content,
}),
{}
),
};
const tsConfig = {
compilerOptions: {
baseUrl: '.',
paths: {
[`@proj/${config.sourceProjectName}`]: [
`libs/${config.sourceProjectName}/index.ts`,
],
},
},
};
const builder = new ProjectGraphBuilder();
builder.addNode({
name: config.sourceProjectName,
type: 'lib',
data: {
root: `libs/${config.sourceProjectName}`,
files: config.sourceProjectFiles.map(({ path }) => ({
file: path,
})),
},
});
builder.addExternalNode({
name: 'npm:npm-package',
type: 'npm',
data: {
version: '1.0.0',
packageName: 'npm-package',
},
});
for (const [projectName, tsconfigPath] of Object.entries(
dependencyProjectNamesToImportPaths
)) {
fsJson[`libs/${projectName}/index.ts`] = ``;
workspaceJson.projects[projectName] = {
root: `libs/${projectName}`,
};
tsConfig.compilerOptions.paths[tsconfigPath] = [
`libs/${projectName}/index.ts`,
];
builder.addNode({
name: projectName,
type: 'lib',
data: {
root: `libs/${projectName}`,
files: [{ file: `libs/${projectName}/index.ts` }],
},
});
}
fsJson['./tsconfig.base.json'] = JSON.stringify(tsConfig);
vol.fromJSON(fsJson, '/root');
defaultFileHasher.init();
return {
ctx: {
workspace: {
...workspaceJson,
...nxJson,
} as any,
} as Workspace,
filesToProcess: createProjectFileMap(
workspaceJson,
defaultFileHasher.allFileData()
).projectFileMap,
} as any;
projects = {
proj3a: {
name: 'proj3a',
type: 'lib',
data: {
root: 'libs/proj3a',
files: [{ file: 'libs/proj3a/index.ts' }],
},
},
proj2: {
name: 'proj2',
type: 'lib',
data: {
root: 'libs/proj2',
files: [{ file: 'libs/proj2/index.ts' }],
},
},
proj: {
name: 'proj',
type: 'lib',
data: {
root: 'libs/proj',
files: [{ file: 'libs/proj/index.ts' }],
},
},
proj1234: {
name: 'proj1234',
type: 'lib',
data: {
root: 'libs/proj1234',
files: [
{ file: 'libs/proj1234/index.ts' },
{ file: 'libs/proj1234/a.b.ts' },
{ file: 'libs/proj1234/b.c.ts' },
],
},
},
proj123: {
name: 'proj123',
type: 'lib',
data: {
root: 'libs/proj123',
files: [{ file: 'libs/proj123/index.ts' }],
},
},
proj4ab: {
name: 'proj4ab',
type: 'lib',
data: {
root: 'libs/proj4ab',
files: [{ file: 'libs/proj4ab/index.ts' }],
},
},
'proj1234-child': {
name: 'proj1234-child',
type: 'lib',
data: {
root: 'libs/proj1234-child',
files: [{ file: 'libs/proj1234-child/index.ts' }],
},
},
};
});
it(`should add dependencies for projects based on file imports`, () => {
const builder = new ProjectGraphBuilder();
Object.values(projects).forEach((p) => {
builder.addNode(p);
});
const res = buildExplicitTypeScriptDependencies(
ctx.workspace,
builder.graph,
ctx.filesToProcess
);
expect(res).toEqual([
{
sourceProjectFile: 'libs/proj/index.ts',
sourceProjectName: 'proj',
targetProjectName: 'proj2',
},
{
sourceProjectFile: 'libs/proj/index.ts',
sourceProjectName: 'proj',
targetProjectName: 'proj3a',
},
{
sourceProjectFile: 'libs/proj/index.ts',
sourceProjectName: 'proj',
targetProjectName: 'proj4ab',
},
{
sourceProjectFile: 'libs/proj1234/index.ts',
sourceProjectName: 'proj1234',
targetProjectName: 'proj1234-child',
},
]);
});
});
},
builder,
};
}