feat(module-federation): add and expose share helpers and types (#12989)
This commit is contained in:
parent
d27114c317
commit
e08d7848b3
@ -0,0 +1,2 @@
|
|||||||
|
export * from './src/models';
|
||||||
|
export * from './src/utils';
|
||||||
@ -16,8 +16,8 @@
|
|||||||
"Module Federation",
|
"Module Federation",
|
||||||
"CLI"
|
"CLI"
|
||||||
],
|
],
|
||||||
"main": "src/index.js",
|
"main": "index.js",
|
||||||
"typings": "src/index.d.ts",
|
"typings": "index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/nrwl/nx/issues"
|
"url": "https://github.com/nrwl/nx/issues"
|
||||||
@ -27,7 +27,11 @@
|
|||||||
"requirements": {},
|
"requirements": {},
|
||||||
"migrations": "./migrations.json"
|
"migrations": "./migrations.json"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
|
"@nrwl/devkit": "file:../devkit",
|
||||||
|
"@nrwl/workspace": "file:../workspace",
|
||||||
|
"webpack": "^5.58.1"
|
||||||
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
}
|
}
|
||||||
|
|||||||
43
packages/module-federation/src/models/index.ts
Normal file
43
packages/module-federation/src/models/index.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
158
packages/module-federation/src/utils/dependencies.spec.ts
Normal file
158
packages/module-federation/src/utils/dependencies.spec.ts
Normal file
@ -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: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
75
packages/module-federation/src/utils/dependencies.ts
Normal file
75
packages/module-federation/src/utils/dependencies.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { ProjectGraph } from '@nrwl/devkit';
|
||||||
|
import { readTsPathMappings } from './typescript';
|
||||||
|
|
||||||
|
export type WorkspaceLibrary = {
|
||||||
|
name: string;
|
||||||
|
root: string;
|
||||||
|
importKey: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
1
packages/module-federation/src/utils/index.ts
Normal file
1
packages/module-federation/src/utils/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './share';
|
||||||
16
packages/module-federation/src/utils/package-json.ts
Normal file
16
packages/module-federation/src/utils/package-json.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { joinPathFragments, readJsonFile, workspaceRoot } from '@nrwl/devkit';
|
||||||
|
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);
|
||||||
|
}
|
||||||
141
packages/module-federation/src/utils/secondary-entry-points.ts
Normal file
141
packages/module-federation/src/utils/secondary-entry-points.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import type { WorkspaceLibrary } from '../models';
|
||||||
|
import { dirname, join, relative } from 'path';
|
||||||
|
import { joinPathFragments, readJsonFile, workspaceRoot } from '@nrwl/devkit';
|
||||||
|
import { existsSync, lstatSync, readdirSync } from 'fs';
|
||||||
|
import { PackageJson, readModulePackageJson } from 'nx/src/utils/package-json';
|
||||||
|
|
||||||
|
export function collectWorkspaceLibrarySecondaryEntryPoints(
|
||||||
|
library: WorkspaceLibrary,
|
||||||
|
tsconfigPathAliases: Record<string, string[]>
|
||||||
|
) {
|
||||||
|
const libraryRoot = join(workspaceRoot, library.root);
|
||||||
|
const needsSecondaryEntryPointsCollected = existsSync(
|
||||||
|
join(libraryRoot, 'ng-package.json')
|
||||||
|
);
|
||||||
|
|
||||||
|
const secondaryEntryPoints = [];
|
||||||
|
if (needsSecondaryEntryPointsCollected) {
|
||||||
|
const tsConfigAliasesForLibWithSecondaryEntryPoints = Object.entries(
|
||||||
|
tsconfigPathAliases
|
||||||
|
).reduce((acc, [tsKey, tsPaths]) => {
|
||||||
|
if (!tsKey.startsWith(library.importKey)) {
|
||||||
|
return { ...acc };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tsPaths.some((path) => path.startsWith(`${library.root}/`))) {
|
||||||
|
acc = { ...acc, [tsKey]: tsPaths };
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
for (const [alias] of Object.entries(
|
||||||
|
tsConfigAliasesForLibWithSecondaryEntryPoints
|
||||||
|
)) {
|
||||||
|
const pathToLib = dirname(
|
||||||
|
join(workspaceRoot, tsconfigPathAliases[alias][0])
|
||||||
|
);
|
||||||
|
let searchDir = pathToLib;
|
||||||
|
while (searchDir !== libraryRoot) {
|
||||||
|
if (existsSync(join(searchDir, 'ng-package.json'))) {
|
||||||
|
secondaryEntryPoints.push({ name: alias, path: pathToLib });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
searchDir = dirname(searchDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return secondaryEntryPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNonNodeModulesSubDirs(directory: string): string[] {
|
||||||
|
return readdirSync(directory)
|
||||||
|
.filter((file) => file !== 'node_modules')
|
||||||
|
.map((file) => join(directory, file))
|
||||||
|
.filter((file) => lstatSync(file).isDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recursivelyCollectSecondaryEntryPointsFromDirectory(
|
||||||
|
pkgName: string,
|
||||||
|
pkgVersion: string,
|
||||||
|
pkgRoot: string,
|
||||||
|
mainEntryPointExports: any | undefined,
|
||||||
|
directories: string[],
|
||||||
|
collectedPackages: { name: string; version: string }[]
|
||||||
|
): void {
|
||||||
|
for (const directory of directories) {
|
||||||
|
const packageJsonPath = join(directory, 'package.json');
|
||||||
|
const relativeEntryPointPath = relative(pkgRoot, directory);
|
||||||
|
const entryPointName = joinPathFragments(pkgName, relativeEntryPointPath);
|
||||||
|
if (existsSync(packageJsonPath)) {
|
||||||
|
try {
|
||||||
|
// require the secondary entry point to try to rule out sample code
|
||||||
|
require.resolve(entryPointName, { paths: [workspaceRoot] });
|
||||||
|
const { name } = readJsonFile(packageJsonPath);
|
||||||
|
// further check to make sure what we were able to require is the
|
||||||
|
// same as the package name
|
||||||
|
if (name === entryPointName) {
|
||||||
|
collectedPackages.push({ name, version: pkgVersion });
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
} else if (mainEntryPointExports) {
|
||||||
|
// if the package.json doesn't exist, check if the directory is
|
||||||
|
// exported by the main entry point
|
||||||
|
const entryPointExportKey = `./${relativeEntryPointPath}`;
|
||||||
|
const entryPointInfo = mainEntryPointExports[entryPointExportKey];
|
||||||
|
if (entryPointInfo) {
|
||||||
|
collectedPackages.push({
|
||||||
|
name: entryPointName,
|
||||||
|
version: pkgVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const subDirs = getNonNodeModulesSubDirs(directory);
|
||||||
|
recursivelyCollectSecondaryEntryPointsFromDirectory(
|
||||||
|
pkgName,
|
||||||
|
pkgVersion,
|
||||||
|
pkgRoot,
|
||||||
|
mainEntryPointExports,
|
||||||
|
subDirs,
|
||||||
|
collectedPackages
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectPackageSecondaryEntryPoints(
|
||||||
|
pkgName: string,
|
||||||
|
pkgVersion: string,
|
||||||
|
collectedPackages: { name: string; version: string }[]
|
||||||
|
): void {
|
||||||
|
let pathToPackage: string;
|
||||||
|
let packageJsonPath: string;
|
||||||
|
let packageJson: PackageJson;
|
||||||
|
try {
|
||||||
|
({ path: packageJsonPath, packageJson } = readModulePackageJson(pkgName));
|
||||||
|
pathToPackage = dirname(packageJsonPath);
|
||||||
|
} catch {
|
||||||
|
// the package.json might not resolve if the package has the "exports"
|
||||||
|
// entry and is not exporting the package.json file, fall back to trying
|
||||||
|
// to find it from the top-level node_modules
|
||||||
|
pathToPackage = join(workspaceRoot, 'node_modules', pkgName);
|
||||||
|
packageJsonPath = join(pathToPackage, 'package.json');
|
||||||
|
if (!existsSync(packageJsonPath)) {
|
||||||
|
// might not exist if it's nested in another package, just return here
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
packageJson = readJsonFile(packageJsonPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { exports } = packageJson;
|
||||||
|
const subDirs = getNonNodeModulesSubDirs(pathToPackage);
|
||||||
|
recursivelyCollectSecondaryEntryPointsFromDirectory(
|
||||||
|
pkgName,
|
||||||
|
pkgVersion,
|
||||||
|
pathToPackage,
|
||||||
|
exports,
|
||||||
|
subDirs,
|
||||||
|
collectedPackages
|
||||||
|
);
|
||||||
|
}
|
||||||
393
packages/module-federation/src/utils/share.spec.ts
Normal file
393
packages/module-federation/src/utils/share.spec.ts
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
jest.mock('fs');
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as tsUtils from './typescript';
|
||||||
|
|
||||||
|
import * as devkit from '@nrwl/devkit';
|
||||||
|
import { sharePackages, shareWorkspaceLibraries } from './share';
|
||||||
|
|
||||||
|
describe('MF Share Utils', () => {
|
||||||
|
afterEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
describe('ShareWorkspaceLibraries', () => {
|
||||||
|
it('should error when the tsconfig file does not exist', () => {
|
||||||
|
// ARRANGE
|
||||||
|
(fs.existsSync as jest.Mock).mockReturnValue(false);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
try {
|
||||||
|
shareWorkspaceLibraries([
|
||||||
|
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
// ASSERT
|
||||||
|
expect(error.message).toContain(
|
||||||
|
'NX MF: TsConfig Path for workspace libraries does not exist!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an object with correct setup', () => {
|
||||||
|
// ARRANGE
|
||||||
|
(fs.existsSync as jest.Mock).mockReturnValue(true);
|
||||||
|
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
|
||||||
|
'@myorg/shared': ['/libs/shared/src/index.ts'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const sharedLibraries = shareWorkspaceLibraries([
|
||||||
|
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(sharedLibraries.getAliases()).toHaveProperty('@myorg/shared');
|
||||||
|
expect(sharedLibraries.getAliases()['@myorg/shared']).toContain(
|
||||||
|
'libs/shared/src/index.ts'
|
||||||
|
);
|
||||||
|
expect(sharedLibraries.getLibraries()).toEqual({
|
||||||
|
'@myorg/shared': {
|
||||||
|
eager: undefined,
|
||||||
|
requiredVersion: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle path mappings with wildcards correctly in non-buildable libraries', () => {
|
||||||
|
// ARRANGE
|
||||||
|
(fs.existsSync as jest.Mock).mockImplementation(
|
||||||
|
(file: string) => !file?.endsWith('package.json')
|
||||||
|
);
|
||||||
|
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({
|
||||||
|
'@myorg/shared': ['/libs/shared/src/index.ts'],
|
||||||
|
'@myorg/shared/*': ['/libs/shared/src/lib/*'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const sharedLibraries = shareWorkspaceLibraries([
|
||||||
|
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(sharedLibraries.getAliases()).toHaveProperty('@myorg/shared');
|
||||||
|
expect(sharedLibraries.getAliases()['@myorg/shared']).toContain(
|
||||||
|
'libs/shared/src/index.ts'
|
||||||
|
);
|
||||||
|
expect(sharedLibraries.getLibraries()).toEqual({
|
||||||
|
'@myorg/shared': {
|
||||||
|
eager: undefined,
|
||||||
|
requiredVersion: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an object with empty setup when tsconfig does not contain the shared lib', () => {
|
||||||
|
// ARRANGE
|
||||||
|
(fs.existsSync as jest.Mock).mockReturnValue(true);
|
||||||
|
jest.spyOn(tsUtils, 'readTsPathMappings').mockReturnValue({});
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const sharedLibraries = shareWorkspaceLibraries([
|
||||||
|
{ name: 'shared', root: 'libs/shared', importKey: '@myorg/shared' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(sharedLibraries.getAliases()).toEqual({});
|
||||||
|
expect(sharedLibraries.getLibraries()).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SharePackages', () => {
|
||||||
|
it('should throw when it cannot find root package.json', () => {
|
||||||
|
// ARRANGE
|
||||||
|
(fs.existsSync as jest.Mock).mockReturnValue(false);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
try {
|
||||||
|
sharePackages(['@angular/core']);
|
||||||
|
} catch (error) {
|
||||||
|
// ASSERT
|
||||||
|
expect(error.message).toEqual(
|
||||||
|
'NX MF: Could not find root package.json to determine dependency versions.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly map the shared packages to objects', () => {
|
||||||
|
// ARRANGE
|
||||||
|
(fs.existsSync as jest.Mock).mockReturnValue(true);
|
||||||
|
jest.spyOn(devkit, 'readJsonFile').mockImplementation((file) => ({
|
||||||
|
name: file.replace(/\\/g, '/').replace(/^.*node_modules[/]/, ''),
|
||||||
|
dependencies: {
|
||||||
|
'@angular/core': '~13.2.0',
|
||||||
|
'@angular/common': '~13.2.0',
|
||||||
|
rxjs: '~7.4.0',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
(fs.readdirSync as jest.Mock).mockReturnValue([]);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const packages = sharePackages([
|
||||||
|
'@angular/core',
|
||||||
|
'@angular/common',
|
||||||
|
'rxjs',
|
||||||
|
]);
|
||||||
|
// ASSERT
|
||||||
|
expect(packages).toEqual({
|
||||||
|
'@angular/core': {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
},
|
||||||
|
'@angular/common': {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
},
|
||||||
|
rxjs: {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly map the shared packages to objects even with nested entry points', () => {
|
||||||
|
// ARRANGE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This creates a bunch of mocks that aims to test that
|
||||||
|
* the sharePackages function can handle nested
|
||||||
|
* entrypoints in the package that is being shared.
|
||||||
|
*
|
||||||
|
* This will set up a directory structure that matches
|
||||||
|
* the following:
|
||||||
|
*
|
||||||
|
* - @angular/core/
|
||||||
|
* - package.json
|
||||||
|
* - @angular/common/
|
||||||
|
* - http/
|
||||||
|
* - testing/
|
||||||
|
* - package.json
|
||||||
|
* - package.json
|
||||||
|
* - rxjs
|
||||||
|
* - package.json
|
||||||
|
*
|
||||||
|
* The result is that there would be 4 packages that
|
||||||
|
* need to be shared, as determined by the folders
|
||||||
|
* containing the package.json files
|
||||||
|
*/
|
||||||
|
createMockedFSForNestedEntryPoints();
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const packages = sharePackages([
|
||||||
|
'@angular/core',
|
||||||
|
'@angular/common',
|
||||||
|
'rxjs',
|
||||||
|
]);
|
||||||
|
// ASSERT
|
||||||
|
expect(packages).toEqual({
|
||||||
|
'@angular/core': {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
},
|
||||||
|
'@angular/common': {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
},
|
||||||
|
'@angular/common/http/testing': {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
},
|
||||||
|
rxjs: {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '~7.4.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not collect a folder with a package.json when cannot be required', () => {
|
||||||
|
// ARRANGE
|
||||||
|
(fs.existsSync as jest.Mock).mockReturnValue(true);
|
||||||
|
jest.spyOn(devkit, 'readJsonFile').mockImplementation((file) => {
|
||||||
|
// the "schematics" folder is not an entry point
|
||||||
|
if (file.endsWith('@angular/core/schematics/package.json')) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: file
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/^.*node_modules[/]/, '')
|
||||||
|
.replace('/package.json', ''),
|
||||||
|
dependencies: { '@angular/core': '~13.2.0' },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
(fs.readdirSync as jest.Mock).mockImplementation(
|
||||||
|
(directoryPath: string) => {
|
||||||
|
const packages = {
|
||||||
|
'@angular/core': ['testing', 'schematics'],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key of Object.keys(packages)) {
|
||||||
|
if (directoryPath.endsWith(key)) {
|
||||||
|
return packages[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
(fs.lstatSync as jest.Mock).mockReturnValue({ isDirectory: () => true });
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const packages = sharePackages(['@angular/core']);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(packages).toStrictEqual({
|
||||||
|
'@angular/core': {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
},
|
||||||
|
'@angular/core/testing': {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should collect secondary entry points from exports and fall back to lookinp up for package.json', () => {
|
||||||
|
// ARRANGE
|
||||||
|
(fs.existsSync as jest.Mock).mockImplementation(
|
||||||
|
(path) => !path.endsWith('/secondary/package.json')
|
||||||
|
);
|
||||||
|
jest.spyOn(devkit, 'readJsonFile').mockImplementation((file) => {
|
||||||
|
if (file.endsWith('pkg1/package.json')) {
|
||||||
|
return {
|
||||||
|
name: 'pkg1',
|
||||||
|
version: '1.0.0',
|
||||||
|
exports: {
|
||||||
|
'.': './index.js',
|
||||||
|
'./package.json': './package.json',
|
||||||
|
'./secondary': './secondary/index.js',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// @angular/core/package.json won't have exports, so it looks up for package.json
|
||||||
|
return {
|
||||||
|
name: file
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/^.*node_modules[/]/, '')
|
||||||
|
.replace('/package.json', ''),
|
||||||
|
dependencies: { pkg1: '1.0.0', '@angular/core': '~13.2.0' },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
(fs.readdirSync as jest.Mock).mockImplementation(
|
||||||
|
(directoryPath: string) => {
|
||||||
|
const packages = {
|
||||||
|
pkg1: ['secondary'],
|
||||||
|
'@angular/core': ['testing'],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key of Object.keys(packages)) {
|
||||||
|
if (directoryPath.endsWith(key)) {
|
||||||
|
return packages[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
(fs.lstatSync as jest.Mock).mockReturnValue({ isDirectory: () => true });
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const packages = sharePackages(['pkg1', '@angular/core']);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(packages).toStrictEqual({
|
||||||
|
pkg1: {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '1.0.0',
|
||||||
|
},
|
||||||
|
'pkg1/secondary': {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '1.0.0',
|
||||||
|
},
|
||||||
|
'@angular/core': {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
},
|
||||||
|
'@angular/core/testing': {
|
||||||
|
singleton: true,
|
||||||
|
strictVersion: true,
|
||||||
|
requiredVersion: '~13.2.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw when the main entry point package.json cannot be required', () => {
|
||||||
|
// ARRANGE
|
||||||
|
(fs.existsSync as jest.Mock).mockImplementation(
|
||||||
|
(file) => !file.endsWith('non-existent-top-level-package/package.json')
|
||||||
|
);
|
||||||
|
jest.spyOn(devkit, 'readJsonFile').mockImplementation((file) => ({
|
||||||
|
name: file
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/^.*node_modules[/]/, '')
|
||||||
|
.replace('/package.json', ''),
|
||||||
|
dependencies: { '@angular/core': '~13.2.0' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
expect(() =>
|
||||||
|
sharePackages(['non-existent-top-level-package'])
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createMockedFSForNestedEntryPoints() {
|
||||||
|
(fs.existsSync as jest.Mock).mockImplementation((file: string) => {
|
||||||
|
if (file.endsWith('http/package.json')) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(devkit, 'readJsonFile').mockImplementation((file) => ({
|
||||||
|
name: file
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
.replace(/^.*node_modules[/]/, '')
|
||||||
|
.replace('/package.json', ''),
|
||||||
|
dependencies: {
|
||||||
|
'@angular/core': '~13.2.0',
|
||||||
|
'@angular/common': '~13.2.0',
|
||||||
|
rxjs: '~7.4.0',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
(fs.readdirSync as jest.Mock).mockImplementation((directoryPath: string) => {
|
||||||
|
const PACKAGE_SETUP = {
|
||||||
|
'@angular/core': [],
|
||||||
|
'@angular/common': ['http'],
|
||||||
|
http: ['testing'],
|
||||||
|
testing: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key of Object.keys(PACKAGE_SETUP)) {
|
||||||
|
if (directoryPath.endsWith(key)) {
|
||||||
|
return PACKAGE_SETUP[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
(fs.lstatSync as jest.Mock).mockReturnValue({ isDirectory: () => true });
|
||||||
|
}
|
||||||
155
packages/module-federation/src/utils/share.ts
Normal file
155
packages/module-federation/src/utils/share.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import type {
|
||||||
|
SharedLibraryConfig,
|
||||||
|
SharedWorkspaceLibraryConfig,
|
||||||
|
WorkspaceLibrary,
|
||||||
|
} from '../models';
|
||||||
|
import { NormalModuleReplacementPlugin } from 'webpack';
|
||||||
|
import { logger, workspaceRoot } from '@nrwl/devkit';
|
||||||
|
import { dirname, join, normalize } from 'path';
|
||||||
|
import { getRootTsConfigPath } from '@nrwl/workspace/src/utilities/typescript';
|
||||||
|
import { readRootPackageJson } from './package-json';
|
||||||
|
import { readTsPathMappings } from './typescript';
|
||||||
|
import {
|
||||||
|
collectPackageSecondaryEntryPoints,
|
||||||
|
collectWorkspaceLibrarySecondaryEntryPoints,
|
||||||
|
} from './secondary-entry-points';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an object of functions to be used with the ModuleFederationPlugin to
|
||||||
|
* share Nx Workspace Libraries between Hosts and Remotes.
|
||||||
|
*
|
||||||
|
* @param libraries - The Nx Workspace Libraries to share
|
||||||
|
* @param tsConfigPath - The path to TS Config File that contains the Path Mappings for the Libraries
|
||||||
|
*/
|
||||||
|
export function shareWorkspaceLibraries(
|
||||||
|
libraries: WorkspaceLibrary[],
|
||||||
|
tsConfigPath = process.env.NX_TSCONFIG_PATH ?? getRootTsConfigPath()
|
||||||
|
): SharedWorkspaceLibraryConfig {
|
||||||
|
if (!libraries) {
|
||||||
|
return getEmptySharedLibrariesConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
const tsconfigPathAliases = readTsPathMappings(tsConfigPath);
|
||||||
|
if (!Object.keys(tsconfigPathAliases).length) {
|
||||||
|
return getEmptySharedLibrariesConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathMappings: { name: string; path: string }[] = [];
|
||||||
|
for (const [key, paths] of Object.entries(tsconfigPathAliases)) {
|
||||||
|
const library = libraries.find((lib) => lib.importKey === key);
|
||||||
|
if (!library) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is for Angular Projects that use ng-package.json
|
||||||
|
// It will do nothing for React Projects
|
||||||
|
collectWorkspaceLibrarySecondaryEntryPoints(
|
||||||
|
library,
|
||||||
|
tsconfigPathAliases
|
||||||
|
).forEach(({ name, path }) =>
|
||||||
|
pathMappings.push({
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
pathMappings.push({
|
||||||
|
name: key,
|
||||||
|
path: normalize(join(workspaceRoot, paths[0])),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getAliases: () =>
|
||||||
|
pathMappings.reduce(
|
||||||
|
(aliases, library) => ({ ...aliases, [library.name]: library.path }),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
getLibraries: (eager?: boolean): Record<string, SharedLibraryConfig> =>
|
||||||
|
pathMappings.reduce(
|
||||||
|
(libraries, library) => ({
|
||||||
|
...libraries,
|
||||||
|
[library.name]: { requiredVersion: false, eager },
|
||||||
|
}),
|
||||||
|
{} as Record<string, SharedLibraryConfig>
|
||||||
|
),
|
||||||
|
getReplacementPlugin: () =>
|
||||||
|
new NormalModuleReplacementPlugin(/./, (req) => {
|
||||||
|
if (!req.request.startsWith('.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = req.context;
|
||||||
|
const to = normalize(join(req.context, req.request));
|
||||||
|
|
||||||
|
for (const library of pathMappings) {
|
||||||
|
const libFolder = normalize(dirname(library.path));
|
||||||
|
if (!from.startsWith(libFolder) && to.startsWith(libFolder)) {
|
||||||
|
req.request = library.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the Module Federation Share Config for a specific package and the
|
||||||
|
* specified version of that package.
|
||||||
|
* @param pkgName - Name of the package to share
|
||||||
|
* @param version - Version of the package to require by other apps in the Module Federation setup
|
||||||
|
*/
|
||||||
|
export function getNpmPackageSharedConfig(
|
||||||
|
pkgName: string,
|
||||||
|
version: string
|
||||||
|
): SharedLibraryConfig | undefined {
|
||||||
|
if (!version) {
|
||||||
|
logger.warn(
|
||||||
|
`Could not find a version for "${pkgName}" in the root "package.json" ` +
|
||||||
|
'when collecting shared packages for the Module Federation setup. ' +
|
||||||
|
'The package will not be shared.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { singleton: true, strictVersion: true, requiredVersion: version };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a dictionary of packages and their Module Federation Shared Config
|
||||||
|
* from an array of package names.
|
||||||
|
*
|
||||||
|
* Lookup the versions of the packages from the root package.json file in the
|
||||||
|
* workspace.
|
||||||
|
* @param packages - Array of package names as strings
|
||||||
|
*/
|
||||||
|
export function sharePackages(
|
||||||
|
packages: string[]
|
||||||
|
): Record<string, SharedLibraryConfig> {
|
||||||
|
const pkgJson = readRootPackageJson();
|
||||||
|
const allPackages: { name: string; version: string }[] = [];
|
||||||
|
packages.forEach((pkg) => {
|
||||||
|
const pkgVersion =
|
||||||
|
pkgJson.dependencies?.[pkg] ?? pkgJson.devDependencies?.[pkg];
|
||||||
|
allPackages.push({ name: pkg, version: pkgVersion });
|
||||||
|
collectPackageSecondaryEntryPoints(pkg, pkgVersion, allPackages);
|
||||||
|
});
|
||||||
|
|
||||||
|
return allPackages.reduce((shared, pkg) => {
|
||||||
|
const config = getNpmPackageSharedConfig(pkg.name, pkg.version);
|
||||||
|
if (config) {
|
||||||
|
shared[pkg.name] = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shared;
|
||||||
|
}, {} as Record<string, SharedLibraryConfig>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmptySharedLibrariesConfig() {
|
||||||
|
return {
|
||||||
|
getAliases: () => ({}),
|
||||||
|
getLibraries: () => ({}),
|
||||||
|
getReplacementPlugin: () =>
|
||||||
|
new NormalModuleReplacementPlugin(/./, () => {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
24
packages/module-federation/src/utils/typescript.spec.ts
Normal file
24
packages/module-federation/src/utils/typescript.spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import * as tsUtils from '@nrwl/workspace/src/utilities/typescript';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { readTsPathMappings } from './typescript';
|
||||||
|
|
||||||
|
describe('readTsPathMappings', () => {
|
||||||
|
it('should normalize paths', () => {
|
||||||
|
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||||
|
jest.spyOn(tsUtils, 'readTsConfig').mockReturnValue({
|
||||||
|
options: {
|
||||||
|
paths: {
|
||||||
|
'@myorg/lib1': ['./libs/lib1/src/index.ts'],
|
||||||
|
'@myorg/lib2': ['libs/lib2/src/index.ts'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const paths = readTsPathMappings('/path/to/tsconfig.json');
|
||||||
|
|
||||||
|
expect(paths).toEqual({
|
||||||
|
'@myorg/lib1': ['libs/lib1/src/index.ts'],
|
||||||
|
'@myorg/lib2': ['libs/lib2/src/index.ts'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
35
packages/module-federation/src/utils/typescript.ts
Normal file
35
packages/module-federation/src/utils/typescript.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
getRootTsConfigPath,
|
||||||
|
readTsConfig,
|
||||||
|
} from '@nrwl/workspace/src/utilities/typescript';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { ParsedCommandLine } from 'typescript';
|
||||||
|
|
||||||
|
let tsConfig: ParsedCommandLine;
|
||||||
|
let tsPathMappings: ParsedCommandLine['options']['paths'];
|
||||||
|
|
||||||
|
export function readTsPathMappings(
|
||||||
|
tsConfigPath: string = process.env.NX_TSCONFIG_PATH ?? getRootTsConfigPath()
|
||||||
|
): ParsedCommandLine['options']['paths'] {
|
||||||
|
if (tsPathMappings) {
|
||||||
|
return tsPathMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
tsConfig ??= readTsConfiguration(tsConfigPath);
|
||||||
|
tsPathMappings = {};
|
||||||
|
Object.entries(tsConfig.options?.paths ?? {}).forEach(([alias, paths]) => {
|
||||||
|
tsPathMappings[alias] = paths.map((path) => path.replace(/^\.\//, ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
return tsPathMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTsConfiguration(tsConfigPath: string): ParsedCommandLine {
|
||||||
|
if (!existsSync(tsConfigPath)) {
|
||||||
|
throw new Error(
|
||||||
|
`NX MF: TsConfig Path for workspace libraries does not exist! (${tsConfigPath}).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return readTsConfig(tsConfigPath);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user