Skip to content

Commit 705e78d

Browse files
committed
feat(nf): compensate missing exports in edge cases, e. g. for react
1 parent fea4025 commit 705e78d

File tree

12 files changed

+247
-26
lines changed

12 files changed

+247
-26
lines changed

libs/mf-runtime/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@angular-architects/module-federation-runtime",
33
"license": "MIT",
4-
"version": "15.0.2",
4+
"version": "15.0.3",
55
"peerDependencies": {
66
"@angular/common": ">=15.0.0",
77
"@angular/core": ">=15.0.0"

libs/mf-tools/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"name": "@angular-architects/module-federation-tools",
3-
"version": "15.0.2",
3+
"version": "15.0.3",
44
"license": "MIT",
55
"peerDependencies": {
66
"@angular/common": ">=15.0.0",
77
"@angular/core": ">=15.0.0",
88
"@angular/router": ">=15.0.0",
9-
"@angular-architects/module-federation": "^15.0.2",
9+
"@angular-architects/module-federation": "^15.0.3",
1010
"@angular/platform-browser": ">=15.0.0",
1111
"rxjs": ">= 6.0.0"
1212
},

libs/mf/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@angular-architects/module-federation",
3-
"version": "15.0.2",
3+
"version": "15.0.3",
44
"license": "MIT",
55
"repository": {
66
"type": "GitHub",
@@ -17,7 +17,7 @@
1717
"schematics": "./collection.json",
1818
"builders": "./builders.json",
1919
"dependencies": {
20-
"@angular-architects/module-federation-runtime": "15.0.2",
20+
"@angular-architects/module-federation-runtime": "15.0.3",
2121
"word-wrap": "^1.2.3",
2222
"callsite": "^1.0.0",
2323
"node-fetch": "^2.6.7",

libs/mf/src/utils/with-mf-plugin.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ModifyEntryPlugin } from './modify-entry-plugin';
55
import ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
66

77
export function withModuleFederationPlugin(config: unknown) {
8-
const sharedMappings = config['sharedMappings'] || [];
8+
const sharedMappings = config['sharedMappings'];
99
delete config['sharedMappings'];
1010

1111
const skip = [
@@ -16,8 +16,12 @@ export function withModuleFederationPlugin(config: unknown) {
1616

1717
delete config['skip'];
1818

19+
if (sharedMappings) {
20+
sharedMappings.filter(m => !skip.includes(m));
21+
}
22+
1923
const mappings = new SharedMappings();
20-
mappings.register(findRootTsConfigJson(), sharedMappings.filter(m => !skip.includes(m)));
24+
mappings.register(findRootTsConfigJson(), sharedMappings);
2125

2226
setDefaults(config, mappings, skip);
2327
const modifyEntryPlugin = createModifyEntryPlugin(config);

libs/native-federation-esbuild/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"rollup": "^2.79.0",
1010
"rollup-plugin-node-externals": "^4.1.1",
1111
"esbuild": "^0.15.5",
12-
"npmlog": "^6.0.2"
12+
"npmlog": "^6.0.2",
13+
"acorn": "^8.8.1"
14+
1315
}
1416
}

libs/native-federation-esbuild/src/lib/adapter.ts

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import * as esbuild from 'esbuild';
77
import { rollup } from 'rollup';
88
import resolve from '@rollup/plugin-node-resolve';
99
import { externals } from 'rollup-plugin-node-externals';
10+
import { collectExports } from './collect-exports';
11+
import * as fs from 'fs';
1012

1113
// eslint-disable-next-line @typescript-eslint/no-var-requires
1214
const commonjs = require('@rollup/plugin-commonjs');
@@ -18,9 +20,15 @@ export const esBuildAdapter: BuildAdapter = createEsBuildAdapter({
1820
plugins: [],
1921
});
2022

23+
export type ReplacementConfig = {
24+
file: string,
25+
tryCompensateMissingExports: boolean
26+
};
27+
2128
export interface EsBuildAdapterConfig {
2229
plugins: esbuild.Plugin[];
23-
fileReplacements?: Record<string, string>
30+
fileReplacements?: Record<string, string | ReplacementConfig>
31+
skipRollup?: boolean,
2432
}
2533

2634
export function createEsBuildAdapter(config: EsBuildAdapterConfig) {
@@ -35,7 +43,7 @@ export function createEsBuildAdapter(config: EsBuildAdapterConfig) {
3543
await prepareNodePackage(entryPoint, external, tmpFolder, config);
3644
}
3745

38-
await esbuild.build({
46+
const r = await esbuild.build({
3947
entryPoints: [isPkg ? tmpFolder : entryPoint],
4048
external,
4149
outfile,
@@ -57,19 +65,52 @@ export function createEsBuildAdapter(config: EsBuildAdapterConfig) {
5765
target: ['esnext'],
5866
plugins: [...config.plugins],
5967
});
68+
69+
postProcess(config, entryPoint, outfile);
6070
};
6171
}
6272

73+
function postProcess(config: EsBuildAdapterConfig, entryPoint: string, outfile: string) {
74+
if (config.fileReplacements) {
75+
const replacements = normalize(config.fileReplacements);
76+
77+
const normalizedPath = entryPoint.replace(/\\/g, '/');
78+
const key = Object.keys(replacements).find(key => normalizedPath.endsWith(key))
79+
80+
if (key && replacements[key] && replacements[key].tryCompensateMissingExports) {
81+
const file = replacements[key].file;
82+
compensateExports(file, outfile);
83+
}
84+
}
85+
}
86+
87+
function compensateExports(entryPoint: string, outfile: string): void {
88+
const inExports = collectExports(entryPoint);
89+
const outExports = collectExports(outfile);
90+
91+
if (!outExports.hasDefaultExport || outExports.hasFurtherExports) {
92+
return;
93+
}
94+
const defaultName = outExports.defaultExportName;
95+
96+
let exports = '/*Try to compensate missing exports*/\n\n';
97+
for (const exp of inExports.exports) {
98+
exports += `let ${exp}$softarc = ${defaultName}.${exp};\n`;
99+
exports += `export { ${exp}$softarc as ${exp} };\n`;
100+
}
101+
102+
fs.appendFileSync(outfile, exports, 'utf-8');
103+
}
104+
63105
async function prepareNodePackage(
64106
entryPoint: string,
65107
external: string[],
66108
tmpFolder: string,
67109
config: EsBuildAdapterConfig,
68110
) {
69111

70-
71112
if (config.fileReplacements) {
72-
entryPoint = replaceEntryPoint(entryPoint, config.fileReplacements);
113+
entryPoint = replaceEntryPoint(entryPoint, normalize(config.fileReplacements));
73114
}
74115

75116
const result = await rollup({
@@ -102,11 +143,27 @@ function inferePkgName(entryPoint: string) {
102143
.replace(/[^A-Za-z0-9.]/g, '_');
103144
}
104145

105-
function replaceEntryPoint(entryPoint: string, fileReplacements: Record<string, string>): string {
146+
function normalize(config: Record<string, string | ReplacementConfig>): Record<string,ReplacementConfig> {
147+
const result: Record<string,ReplacementConfig> = {};
148+
for (const key in config) {
149+
if (typeof config[key] === 'string') {
150+
result[key] = {
151+
file: config[key] as string,
152+
tryCompensateMissingExports: false
153+
}
154+
}
155+
else {
156+
result[key] = config[key] as ReplacementConfig;
157+
}
158+
}
159+
return result;
160+
}
161+
162+
function replaceEntryPoint(entryPoint: string, fileReplacements: Record<string, ReplacementConfig>): string {
106163
entryPoint = entryPoint.replace(/\\/g, '/');
107164

108165
for(const key in fileReplacements) {
109-
entryPoint = entryPoint.replace(new RegExp(`${key}$`), fileReplacements[key]);
166+
entryPoint = entryPoint.replace(new RegExp(`${key}$`), fileReplacements[key].file);
110167
}
111168

112169
return entryPoint;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import * as fs from "fs";
2+
import { parse } from "acorn";
3+
4+
type Node = any;
5+
6+
type visitFn = (node: Node) => void;
7+
8+
export function collectExports(path: string) {
9+
const src = fs.readFileSync(path, "utf8");
10+
11+
const parseTree = parse(src, {
12+
ecmaVersion: "latest",
13+
allowHashBang: true,
14+
sourceType: "module",
15+
});
16+
17+
let hasDefaultExport = false;
18+
let hasFurtherExports = false;
19+
let defaultExportName = "";
20+
const exports: string[] = [];
21+
22+
traverse(parseTree, (node) => {
23+
if (
24+
node.type === "AssignmentExpression" &&
25+
node?.left?.object?.name === "exports" // &&
26+
) {
27+
exports.push(node.left.property?.name);
28+
return;
29+
}
30+
31+
if (hasDefaultExport && hasFurtherExports) {
32+
return;
33+
}
34+
35+
if (node.type !== "ExportNamedDeclaration") {
36+
return;
37+
}
38+
39+
if (!node.specifiers) {
40+
hasFurtherExports = true;
41+
return;
42+
}
43+
44+
for (const s of node.specifiers) {
45+
if (isDefaultExport(s)) {
46+
defaultExportName = s?.local?.name;
47+
hasDefaultExport = true;
48+
} else {
49+
hasFurtherExports = true;
50+
}
51+
}
52+
});
53+
54+
return {
55+
hasDefaultExport,
56+
hasFurtherExports,
57+
defaultExportName,
58+
exports,
59+
};
60+
}
61+
62+
function traverse(node: Node, visit: visitFn) {
63+
visit(node);
64+
for (const key in node) {
65+
const prop = node[key];
66+
if (prop && typeof prop === "object") {
67+
traverse(prop as Node, visit);
68+
} else if (Array.isArray(prop)) {
69+
for (const sub of prop) {
70+
traverse(sub, visit);
71+
}
72+
}
73+
}
74+
}
75+
76+
function isDefaultExport(exportSpecifier: Node) {
77+
return (
78+
exportSpecifier.exported?.type === "Identifier" &&
79+
exportSpecifier.exported?.name === "default"
80+
);
81+
}
Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
1-
export const reactReplacements = {
2-
dev: {
3-
'node_modules/react/index.js': 'node_modules/react/cjs/react.development.js',
4-
'node_modules/react/jsx-dev-runtime.js' : 'node_modules/react/cjs/react-jsx-dev-runtime.development.js',
5-
'node_modules/react/jsx-runtime.js': 'node_modules/react/cjs/react-jsx-runtime.development.js'
6-
},
7-
prod: {
8-
'node_modules/react/index.js': 'node_modules/react/cjs/react.production.min.js',
9-
'node_modules/react/jsx-dev-runtime.js' : 'node_modules/react/cjs/react-jsx-dev-runtime.production.min.js',
10-
'node_modules/react/jsx-runtime.js': 'node_modules/react/cjs/react-jsx-runtime.production.min.js'
11-
}
12-
}
1+
import { ReplacementConfig } from './adapter';
2+
3+
export const reactReplacements: Record<
4+
string,
5+
Record<string, ReplacementConfig>
6+
> = {
7+
dev: {
8+
'node_modules/react/index.js': {
9+
file: 'node_modules/react/cjs/react.development.js',
10+
tryCompensateMissingExports: true,
11+
},
12+
'node_modules/react/jsx-dev-runtime.js': {
13+
file: 'node_modules/react/cjs/react-jsx-dev-runtime.development.js',
14+
tryCompensateMissingExports: true,
15+
},
16+
'node_modules/react/jsx-runtime.js': {
17+
file: 'node_modules/react/cjs/react-jsx-runtime.development.js',
18+
tryCompensateMissingExports: true,
19+
},
20+
},
21+
prod: {
22+
'node_modules/react/index.js': {
23+
file: 'node_modules/react/cjs/react.production.min.js',
24+
tryCompensateMissingExports: true,
25+
},
26+
'node_modules/react/jsx-dev-runtime.js': {
27+
file: 'node_modules/react/cjs/react-jsx-dev-runtime.production.min.js',
28+
tryCompensateMissingExports: true,
29+
},
30+
'node_modules/react/jsx-runtime.js': {
31+
file: 'node_modules/react/cjs/react-jsx-runtime.production.min.js',
32+
tryCompensateMissingExports: true,
33+
},
34+
},
35+
};

0 commit comments

Comments
 (0)