Skip to content

Commit

Permalink
feat(devkit): add module federation utils (#13048)
Browse files Browse the repository at this point in the history
  • Loading branch information
Coly010 committed Nov 8, 2022
1 parent 269038f commit e040433
Show file tree
Hide file tree
Showing 11 changed files with 1,081 additions and 2 deletions.
158 changes: 158 additions & 0 deletions packages/devkit/src/utils/module-federation/dependencies.spec.ts
@@ -0,0 +1,158 @@
import * as tsUtils from './typescript';
import { getDependentPackagesForProject } from './dependencies';

describe('getDependentPackagesForProject', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should collect npm packages and workspaces libraries without duplicates', () => {
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/lib1': ['libs/lib1/src/index.ts'],
'@myorg/lib2': ['libs/lib2/src/index.ts'],
});

const dependencies = getDependentPackagesForProject(
{
dependencies: {
shell: [
{ source: 'shell', target: 'lib1', type: 'static' },
{ source: 'shell', target: 'lib2', type: 'static' },
{ source: 'shell', target: 'npm:lodash', type: 'static' },
],
lib1: [{ source: 'lib1', target: 'lib2', type: 'static' }],
lib2: [{ source: 'lib2', target: 'npm:lodash', type: 'static' }],
},
nodes: {
shell: {
name: 'shell',
data: { root: 'apps/shell', sourceRoot: 'apps/shell/src' },
type: 'app',
},
lib1: {
name: 'lib1',
data: { root: 'libs/lib1', sourceRoot: 'libs/lib1/src' },
type: 'lib',
},
lib2: {
name: 'lib2',
data: { root: 'libs/lib2', sourceRoot: 'libs/lib2/src' },
type: 'lib',
},
},
},
'shell'
);

expect(dependencies).toEqual({
workspaceLibraries: [
{ name: 'lib1', root: 'libs/lib1', importKey: '@myorg/lib1' },
{ name: 'lib2', root: 'libs/lib2', importKey: '@myorg/lib2' },
],
npmPackages: ['lodash'],
});
});

it('should collect workspaces libraries recursively', () => {
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/lib1': ['libs/lib1/src/index.ts'],
'@myorg/lib2': ['libs/lib2/src/index.ts'],
'@myorg/lib3': ['libs/lib3/src/index.ts'],
});

const dependencies = getDependentPackagesForProject(
{
dependencies: {
shell: [{ source: 'shell', target: 'lib1', type: 'static' }],
lib1: [{ source: 'lib1', target: 'lib2', type: 'static' }],
lib2: [{ source: 'lib2', target: 'lib3', type: 'static' }],
},
nodes: {
shell: {
name: 'shell',
data: { root: 'apps/shell', sourceRoot: 'apps/shell/src' },
type: 'app',
},
lib1: {
name: 'lib1',
data: { root: 'libs/lib1', sourceRoot: 'libs/lib1/src' },
type: 'lib',
},
lib2: {
name: 'lib2',
data: { root: 'libs/lib2', sourceRoot: 'libs/lib2/src' },
type: 'lib',
},
lib3: {
name: 'lib3',
data: { root: 'libs/lib3', sourceRoot: 'libs/lib3/src' },
type: 'lib',
},
},
},
'shell'
);

expect(dependencies).toEqual({
workspaceLibraries: [
{ name: 'lib1', root: 'libs/lib1', importKey: '@myorg/lib1' },
{ name: 'lib2', root: 'libs/lib2', importKey: '@myorg/lib2' },
{ name: 'lib3', root: 'libs/lib3', importKey: '@myorg/lib3' },
],
npmPackages: [],
});
});

it('should ignore TS path mappings with wildcards', () => {
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
'@myorg/lib1': ['libs/lib1/src/index.ts'],
'@myorg/lib1/*': ['libs/lib1/src/lib/*'],
'@myorg/lib2': ['libs/lib2/src/index.ts'],
'@myorg/lib2/*': ['libs/lib2/src/lib/*'],
'@myorg/lib3': ['libs/lib3/src/index.ts'],
'@myorg/lib3/*': ['libs/lib3/src/lib/*'],
});

const dependencies = getDependentPackagesForProject(
{
dependencies: {
shell: [{ source: 'shell', target: 'lib1', type: 'static' }],
lib1: [{ source: 'lib1', target: 'lib2', type: 'static' }],
lib2: [{ source: 'lib2', target: 'lib3', type: 'static' }],
},
nodes: {
shell: {
name: 'shell',
data: { root: 'apps/shell', sourceRoot: 'apps/shell/src' },
type: 'app',
},
lib1: {
name: 'lib1',
data: { root: 'libs/lib1', sourceRoot: 'libs/lib1/src' },
type: 'lib',
},
lib2: {
name: 'lib2',
data: { root: 'libs/lib2', sourceRoot: 'libs/lib2/src' },
type: 'lib',
},
lib3: {
name: 'lib3',
data: { root: 'libs/lib3', sourceRoot: 'libs/lib3/src' },
type: 'lib',
},
},
},
'shell'
);

expect(dependencies).toEqual({
workspaceLibraries: [
{ name: 'lib1', root: 'libs/lib1', importKey: '@myorg/lib1' },
{ name: 'lib2', root: 'libs/lib2', importKey: '@myorg/lib2' },
{ name: 'lib3', root: 'libs/lib3', importKey: '@myorg/lib3' },
],
npmPackages: [],
});
});
});
70 changes: 70 additions & 0 deletions packages/devkit/src/utils/module-federation/dependencies.ts
@@ -0,0 +1,70 @@
import type { ProjectGraph } from 'nx/src/config/project-graph';
import type { WorkspaceLibrary } from './models';
import { readTsPathMappings } from './typescript';

export function getDependentPackagesForProject(
projectGraph: ProjectGraph,
name: string
): {
workspaceLibraries: WorkspaceLibrary[];
npmPackages: string[];
} {
const { npmPackages, workspaceLibraries } = collectDependencies(
projectGraph,
name
);

return {
workspaceLibraries: [...workspaceLibraries.values()],
npmPackages: [...npmPackages],
};
}

function collectDependencies(
projectGraph: ProjectGraph,
name: string,
dependencies = {
workspaceLibraries: new Map<string, WorkspaceLibrary>(),
npmPackages: new Set<string>(),
},
seen: Set<string> = new Set()
): {
workspaceLibraries: Map<string, WorkspaceLibrary>;
npmPackages: Set<string>;
} {
if (seen.has(name)) {
return dependencies;
}
seen.add(name);

(projectGraph.dependencies[name] ?? []).forEach((dependency) => {
if (dependency.target.startsWith('npm:')) {
dependencies.npmPackages.add(dependency.target.replace('npm:', ''));
} else {
dependencies.workspaceLibraries.set(dependency.target, {
name: dependency.target,
root: projectGraph.nodes[dependency.target].data.root,
importKey: getLibraryImportPath(dependency.target, projectGraph),
});
collectDependencies(projectGraph, dependency.target, dependencies, seen);
}
});

return dependencies;
}

function getLibraryImportPath(
library: string,
projectGraph: ProjectGraph
): string | undefined {
const tsConfigPathMappings = readTsPathMappings();

const sourceRoot = projectGraph.nodes[library].data.sourceRoot;
for (const [key, value] of Object.entries(tsConfigPathMappings)) {
if (value.find((path) => path.startsWith(sourceRoot))) {
return key;
}
}

return undefined;
}
4 changes: 4 additions & 0 deletions packages/devkit/src/utils/module-federation/index.ts
@@ -0,0 +1,4 @@
export * from './share';
export * from './dependencies';
export * from './package-json';
export * from './models';
48 changes: 48 additions & 0 deletions packages/devkit/src/utils/module-federation/models/index.ts
@@ -0,0 +1,48 @@
import type { NormalModuleReplacementPlugin } from 'webpack';

export type ModuleFederationLibrary = { type: string; name: string };
export type WorkspaceLibrary = {
name: string;
root: string;
importKey: string | undefined;
};

export type SharedWorkspaceLibraryConfig = {
getAliases: () => Record<string, string>;
getLibraries: (eager?: boolean) => Record<string, SharedLibraryConfig>;
getReplacementPlugin: () => NormalModuleReplacementPlugin;
};

export type Remotes = string[] | [remoteName: string, remoteUrl: string][];

export interface SharedLibraryConfig {
singleton?: boolean;
strictVersion?: boolean;
requiredVersion?: false | string;
eager?: boolean;
}

export type SharedFunction = (
libraryName: string,
sharedConfig: SharedLibraryConfig
) => undefined | false | SharedLibraryConfig;

export type AdditionalSharedConfig = Array<
| string
| [libraryName: string, sharedConfig: SharedLibraryConfig]
| { libraryName: string; sharedConfig: SharedLibraryConfig }
>;

export interface ModuleFederationConfig {
name: string;
remotes?: Remotes;
library?: ModuleFederationLibrary;
exposes?: Record<string, string>;
shared?: SharedFunction;
additionalShared?: AdditionalSharedConfig;
}

export type WorkspaceLibrarySecondaryEntryPoint = {
name: string;
path: string;
};
18 changes: 18 additions & 0 deletions packages/devkit/src/utils/module-federation/package-json.ts
@@ -0,0 +1,18 @@
import { joinPathFragments } from 'nx/src/utils/path';
import { readJsonFile } from 'nx/src/utils/fileutils';
import { workspaceRoot } from 'nx/src/utils/workspace-root';
import { existsSync } from 'fs';

export function readRootPackageJson(): {
dependencies?: { [key: string]: string };
devDependencies?: { [key: string]: string };
} {
const pkgJsonPath = joinPathFragments(workspaceRoot, 'package.json');
if (!existsSync(pkgJsonPath)) {
throw new Error(
'NX MF: Could not find root package.json to determine dependency versions.'
);
}

return readJsonFile(pkgJsonPath);
}

1 comment on commit e040433

@vercel
Copy link

@vercel vercel bot commented on e040433 Nov 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-dev-git-master-nrwl.vercel.app
nx-five.vercel.app
nx-dev-nrwl.vercel.app
nx.dev

Please sign in to comment.