Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(devkit): add module federation utils (#13048)
- Loading branch information
Showing
11 changed files
with
1,081 additions
and
2 deletions.
There are no files selected for viewing
158 changes: 158 additions & 0 deletions
158
packages/devkit/src/utils/module-federation/dependencies.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
70
packages/devkit/src/utils/module-federation/dependencies.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from './share'; | ||
export * from './dependencies'; | ||
export * from './package-json'; | ||
export * from './models'; |
48 changes: 48 additions & 0 deletions
48
packages/devkit/src/utils/module-federation/models/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
18
packages/devkit/src/utils/module-federation/package-json.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
Oops, something went wrong.
e040433
There was a problem hiding this comment.
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