Skip to content

Commit 7e19f00

Browse files
committed
chore: wip
1 parent 27b294b commit 7e19f00

File tree

36 files changed

+1682
-1009
lines changed

36 files changed

+1682
-1009
lines changed

apps/shared-treeshake/basic/App.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import UiLib, { Button } from 'ui-lib';
2+
3+
export default () => {
4+
return `Uilib has ${Object.keys(UiLib).length} exports, and Button value is ${Button}`;
5+
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
const fs = __non_webpack_require__('fs');
2+
const path = __non_webpack_require__('path');
3+
4+
__webpack_require__.p = 'PUBLIC_PATH';
5+
it('should treeshake ui-lib correctly', async () => {
6+
const app = await import('./App.js');
7+
expect(app.default()).toEqual(
8+
'Uilib has 2 exports, and Button value is Button',
9+
);
10+
11+
const bundlePath = path.join(__dirname, 'node_modules_ui-lib_index_js.js');
12+
const bundleContent = fs.readFileSync(bundlePath, 'utf-8');
13+
expect(bundleContent).toContain('Button');
14+
expect(bundleContent).toContain('Badge');
15+
expect(bundleContent).not.toContain('List');
16+
});
17+
18+
it('should treeshake ui-lib-dynamic-specific-export correctly', async () => {
19+
const { List } = await import('ui-lib-dynamic-specific-export');
20+
expect(List).toEqual('List');
21+
22+
const bundlePath = path.join(
23+
__dirname,
24+
'node_modules_ui-lib-dynamic-specific-export_index_js.js',
25+
);
26+
const bundleContent = fs.readFileSync(bundlePath, 'utf-8');
27+
expect(bundleContent).toContain('List');
28+
expect(bundleContent).not.toContain('Button');
29+
expect(bundleContent).not.toContain('Badge');
30+
});
31+
32+
it('should not treeshake ui-lib-dynamic-default-export', async () => {
33+
const uiLib = await import('ui-lib-dynamic-default-export');
34+
expect(uiLib.List).toEqual('List');
35+
36+
const bundlePath = path.join(
37+
__dirname,
38+
'node_modules_ui-lib-dynamic-default-export_index_js.js',
39+
);
40+
const bundleContent = fs.readFileSync(bundlePath, 'utf-8');
41+
expect(bundleContent).toContain('List');
42+
expect(bundleContent).toContain('Button');
43+
expect(bundleContent).toContain('Badge');
44+
});
45+
46+
it('should not treeshake ui-lib-side-effect if not set sideEffect:false ', async () => {
47+
const uiLibSideEffect = await import('ui-lib-side-effect');
48+
expect(uiLibSideEffect.List).toEqual('List');
49+
50+
const bundlePath = path.join(
51+
__dirname,
52+
'node_modules_ui-lib-side-effect_index_js.js',
53+
);
54+
const bundleContent = fs.readFileSync(bundlePath, 'utf-8');
55+
expect(bundleContent).toContain('List');
56+
expect(bundleContent).toContain('Button');
57+
expect(bundleContent).toContain('Badge');
58+
});
59+
60+
it('should inject usedExports into entry chunk by default', async () => {
61+
expect(
62+
__webpack_require__.federation.usedExports['ui-lib']['main'].sort(),
63+
).toEqual(['Badge', 'Button']);
64+
});
65+
66+
it('should inject usedExports into manifest and stats if enable manifest', async () => {
67+
const { Button } = await import('ui-lib');
68+
expect(Button).toEqual('Button');
69+
70+
const manifestPath = path.join(__dirname, 'mf-manifest.json');
71+
const manifestContent = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
72+
expect(
73+
JSON.stringify(
74+
manifestContent.shared
75+
.find((s) => s.name === 'ui-lib')
76+
.usedExports.sort(),
77+
),
78+
).toEqual(JSON.stringify(['Badge', 'Button']));
79+
80+
const statsPath = path.join(__dirname, 'mf-stats.json');
81+
const statsContent = JSON.parse(fs.readFileSync(statsPath, 'utf-8'));
82+
expect(
83+
JSON.stringify(
84+
statsContent.shared.find((s) => s.name === 'ui-lib').usedExports.sort(),
85+
),
86+
).toEqual(JSON.stringify(['Badge', 'Button']));
87+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "shared-treeshake-basic",
3+
"private": true,
4+
"version": "0.1.34",
5+
"scripts": {
6+
"build": "webpack build"
7+
},
8+
"engines": {
9+
"node": ">=16.18.1"
10+
},
11+
"lint-staged": {
12+
"*.{js,jsx,ts,tsx,mjs,cjs}": [
13+
"node --max_old_space_size=8192 ./node_modules/eslint/bin/eslint.js --fix --color --cache --quiet"
14+
]
15+
},
16+
"devDependencies": {
17+
"webpack": "5.94.0",
18+
"webpack-cli": "5.1.4",
19+
"@module-federation/enhanced": "workspace:*"
20+
}
21+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "shared-treeshake-basic",
3+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "apps/shared-treeshake/basic/src",
5+
"projectType": "application",
6+
"tags": [],
7+
"implicitDependencies": ["typescript"],
8+
"targets": {
9+
"build": {
10+
"executor": "nx:run-commands",
11+
"options": {
12+
"dependsOn": [
13+
{
14+
"target": "build",
15+
"dependencies": true
16+
}
17+
],
18+
"commands": [
19+
{
20+
"command": "cd apps/shared-treeshake/basic; pnpm run build",
21+
"forwardAllArgs": true
22+
}
23+
]
24+
}
25+
}
26+
}
27+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const { ModuleFederationPlugin } = require('@module-federation/enhanced');
2+
3+
module.exports = {
4+
target: 'async-node',
5+
entry: './index.js',
6+
mode: 'production',
7+
optimization: {
8+
minimize: true,
9+
chunkIds: 'named',
10+
moduleIds: 'named',
11+
},
12+
output: {
13+
publicPath: '/',
14+
chunkFilename: '[id].js',
15+
},
16+
plugins: [
17+
new ModuleFederationPlugin({
18+
name: 'treeshake_share',
19+
manifest: true,
20+
library: { type: 'commonjs-module' },
21+
exposes: {
22+
'./App': './App.js',
23+
},
24+
// shared: {
25+
// 'ui-lib': {
26+
// requiredVersion: '*',
27+
// treeshake: true,
28+
// },
29+
// 'ui-lib-dynamic-specific-export': {
30+
// requiredVersion: '*',
31+
// treeshake: true,
32+
// },
33+
// 'ui-lib-dynamic-default-export': {
34+
// requiredVersion: '*',
35+
// treeshake: true,
36+
// },
37+
// 'ui-lib-side-effect': {
38+
// requiredVersion: '*',
39+
// treeshake: true,
40+
// },
41+
// },
42+
}),
43+
],
44+
};

packages/enhanced/src/lib/sharing/treeshake/CollectSharedEntryPlugin.ts

Lines changed: 17 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const PLUGIN_NAME = 'CollectSharedEntryPlugin';
88
export type ShareRequestsMap = Record<
99
string,
1010
{
11+
// request, version
1112
requests: [string, string][];
1213
}
1314
>;
@@ -17,16 +18,6 @@ export type CollectSharedEntryPluginOptions = {
1718
shareScope?: string;
1819
};
1920

20-
function extractPathAfterNodeModules(filePath: string): string | null {
21-
// Fast check for 'node_modules' substring
22-
if (~filePath.indexOf('node_modules')) {
23-
const nodeModulesIndex = filePath.lastIndexOf('node_modules');
24-
const result = filePath.substring(nodeModulesIndex + 13); // 13 = 'node_modules/'.length
25-
return result;
26-
}
27-
return null;
28-
}
29-
3021
function inferPkgVersionFromResource(resource: string): string | undefined {
3122
try {
3223
const nmIndex = resource.lastIndexOf('node_modules');
@@ -96,47 +87,28 @@ class CollectSharedEntryPlugin {
9687
compiler.hooks.compilation.tap(
9788
'CollectSharedEntryPlugin',
9889
(_compilation, { normalModuleFactory }) => {
99-
const matchProvides = new Map();
100-
10190
normalModuleFactory.hooks.module.tap(
10291
'CollectSharedEntryPlugin',
10392
(module, { resource }, resolveData) => {
104-
if (
105-
!resource ||
106-
!sharedOptions.find((item) => item[0] === (resource as string))
107-
) {
93+
if (!resource || !('rawRequest' in module)) {
10894
return module;
10995
}
110-
111-
const { request: originalRequestString } = resolveData;
112-
const modulePathAfterNodeModules =
113-
extractPathAfterNodeModules(resource);
114-
if (modulePathAfterNodeModules) {
115-
// 2a. Direct match with reconstructed path
116-
const reconstructedLookupKey = modulePathAfterNodeModules;
117-
const configFromReconstructedDirect = matchProvides.get(
118-
reconstructedLookupKey,
119-
);
120-
if (
121-
configFromReconstructedDirect?.nodeModulesReconstructedLookup
122-
) {
123-
const entry = (collectedEntries[resource] ||= { requests: [] });
124-
const version = inferPkgVersionFromResource(resource);
125-
if (!version) {
126-
throw new Error(
127-
`Cannot infer version from resource ${resource}`,
128-
);
129-
}
130-
const exists = entry.requests.some(
131-
([req, ver]) =>
132-
req === originalRequestString && ver === version,
133-
);
134-
if (!exists) {
135-
entry.requests.push([originalRequestString, version]);
136-
}
137-
resolveData.cacheable = false;
138-
}
96+
const matchedSharedOption = sharedOptions.find(
97+
(item) => item[0] === (module.rawRequest as string),
98+
);
99+
if (!matchedSharedOption) {
100+
return module;
101+
}
102+
const [sharedName, _] = matchedSharedOption;
103+
const sharedVersion = inferPkgVersionFromResource(resource);
104+
if (!sharedVersion) {
105+
return module;
139106
}
107+
collectedEntries[sharedName] ||= { requests: [] };
108+
collectedEntries[sharedName].requests.push([
109+
resource,
110+
sharedVersion,
111+
]);
140112
return module;
141113
},
142114
);

0 commit comments

Comments
 (0)