@tramvai/cli 2.110.0 → 2.111.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/lib/builder/webpack/tokens.d.ts +3 -0
  2. package/lib/commands/update/dependantLibs.d.ts +0 -1
  3. package/lib/commands/update/dependantLibs.js +3 -24
  4. package/lib/commands/update/dependantLibs.js.map +1 -1
  5. package/lib/commands/update/updatePackageJson.js +3 -5
  6. package/lib/commands/update/updatePackageJson.js.map +1 -1
  7. package/lib/di/tokens/config.d.ts +1 -0
  8. package/lib/library/webpack/application/client/common.js +7 -1
  9. package/lib/library/webpack/application/client/common.js.map +1 -1
  10. package/lib/library/webpack/application/server/prod.js +24 -0
  11. package/lib/library/webpack/application/server/prod.js.map +1 -1
  12. package/lib/library/webpack/blocks/optimize.js +4 -0
  13. package/lib/library/webpack/blocks/optimize.js.map +1 -1
  14. package/lib/library/webpack/child-app/client/common.js +7 -1
  15. package/lib/library/webpack/child-app/client/common.js.map +1 -1
  16. package/lib/library/webpack/child-app/moduleFederationShared.js +4 -4
  17. package/lib/library/webpack/plugins/ModuleFederationFixRange.d.ts +23 -0
  18. package/lib/library/webpack/plugins/ModuleFederationFixRange.js +125 -0
  19. package/lib/library/webpack/plugins/ModuleFederationFixRange.js.map +1 -0
  20. package/lib/schema/autogeneratedSchema.json +21 -3
  21. package/lib/typings/configEntry/cli.d.ts +7 -1
  22. package/lib/utils/tramvaiVersions.d.ts +3 -0
  23. package/lib/utils/tramvaiVersions.js +30 -0
  24. package/lib/utils/tramvaiVersions.js.map +1 -0
  25. package/package.json +8 -8
  26. package/schema.json +21 -3
  27. package/src/commands/update/dependantLibs.ts +1 -23
  28. package/src/commands/update/updatePackageJson.ts +2 -5
  29. package/src/library/swc/__integration__/__snapshots__/swc.build.test.ts.snap +149 -196
  30. package/src/library/swc/__integration__/__snapshots__/swc.start.test.ts.snap +30 -20
  31. package/src/library/webpack/application/client/common.ts +9 -1
  32. package/src/library/webpack/application/server/prod.ts +25 -0
  33. package/src/library/webpack/blocks/optimize.ts +4 -0
  34. package/src/library/webpack/child-app/client/common.ts +9 -1
  35. package/src/library/webpack/child-app/moduleFederationShared.ts +4 -4
  36. package/src/library/webpack/plugins/ModuleFederationFixRange.ts +174 -0
  37. package/src/models/config.spec.ts +4 -0
  38. package/src/schema/autogeneratedSchema.json +21 -3
  39. package/src/schema/tramvai.spec.ts +2 -0
  40. package/src/typings/configEntry/cli.ts +7 -1
  41. package/src/utils/tramvaiVersions.ts +26 -0
@@ -3,7 +3,7 @@
3
3
  exports[`client: create-token-pure.ts 1`] = `
4
4
  "__webpack_require__.r(__webpack_exports__);
5
5
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
6
- /* harmony export */ "TEST_TOKEN": function() { return /* binding */ TEST_TOKEN; }
6
+ /* harmony export */ TEST_TOKEN: function() { return /* binding */ TEST_TOKEN; }
7
7
  /* harmony export */ });
8
8
  /* harmony import */ var _tramvai_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/library/swc/__integration__/mocks/tramvai-core.ts");
9
9
  /* provided dependency */ var __react_refresh_utils__ = __webpack_require__("../../node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js");
@@ -149,7 +149,7 @@ __webpack_require__.$Refresh$.register(_c, "SvgLogo");
149
149
  exports[`client: lazy-component.tsx 1`] = `
150
150
  "__webpack_require__.r(__webpack_exports__);
151
151
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
152
- /* harmony export */ "Component": function() { return /* binding */ Component; }
152
+ /* harmony export */ Component: function() { return /* binding */ Component; }
153
153
  /* harmony export */ });
154
154
  /* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("webpack/sharing/consume/default/react/jsx-dev-runtime/react/jsx-dev-runtime");
155
155
  /* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__);
@@ -242,13 +242,15 @@ if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Pr
242
242
  exports[`client: node-env.ts 1`] = `
243
243
  "__webpack_require__.r(__webpack_exports__);
244
244
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
245
- /* harmony export */ "func": function() { return /* binding */ func; }
245
+ /* harmony export */ func: function() { return /* binding */ func; }
246
246
  /* harmony export */ });
247
247
  /* provided dependency */ var __react_refresh_utils__ = __webpack_require__("../../node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js");
248
248
  __webpack_require__.$Refresh$.runtime = __webpack_require__("../../node_modules/react-refresh/runtime.js");
249
249
 
250
250
  let internalFunc;
251
- if (true) internalFunc = ()=>"Are you developer, for sure?";
251
+ if (true) {
252
+ internalFunc = ()=>"Are you developer, for sure?";
253
+ }
252
254
  if (false) {}
253
255
  const func = ()=>{
254
256
  return internalFunc();
@@ -292,7 +294,7 @@ if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Pr
292
294
  exports[`client: provider-stack.ts 1`] = `
293
295
  "__webpack_require__.r(__webpack_exports__);
294
296
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
295
- /* harmony export */ "providers": function() { return /* binding */ providers; }
297
+ /* harmony export */ providers: function() { return /* binding */ providers; }
296
298
  /* harmony export */ });
297
299
  /* harmony import */ var _tramvai_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/library/swc/__integration__/mocks/tramvai-core.ts");
298
300
  /* harmony import */ var _create_token_pure__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/library/swc/__integration__/create-token-pure.ts");
@@ -350,7 +352,7 @@ if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Pr
350
352
  exports[`client: react-svg.tsx 1`] = `
351
353
  "__webpack_require__.r(__webpack_exports__);
352
354
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
353
- /* harmony export */ "Component": function() { return /* binding */ Component; }
355
+ /* harmony export */ Component: function() { return /* binding */ Component; }
354
356
  /* harmony export */ });
355
357
  /* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("webpack/sharing/consume/default/react/jsx-dev-runtime/react/jsx-dev-runtime");
356
358
  /* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__);
@@ -415,7 +417,7 @@ if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Pr
415
417
  exports[`client: server.inline.ts 1`] = `
416
418
  "__webpack_require__.r(__webpack_exports__);
417
419
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
418
- /* harmony export */ "ForBrowser": function() { return /* binding */ ForBrowser; }
420
+ /* harmony export */ ForBrowser: function() { return /* binding */ ForBrowser; }
419
421
  /* harmony export */ });
420
422
  /* provided dependency */ var __react_refresh_utils__ = __webpack_require__("../../node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js");
421
423
  __webpack_require__.$Refresh$.runtime = __webpack_require__("../../node_modules/react-refresh/runtime.js");
@@ -465,13 +467,16 @@ if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Pr
465
467
  exports[`client: typeof-window.ts 1`] = `
466
468
  "__webpack_require__.r(__webpack_exports__);
467
469
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
468
- /* harmony export */ "func": function() { return /* binding */ func; }
470
+ /* harmony export */ func: function() { return /* binding */ func; }
469
471
  /* harmony export */ });
470
472
  /* provided dependency */ var __react_refresh_utils__ = __webpack_require__("../../node_modules/@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js");
471
473
  __webpack_require__.$Refresh$.runtime = __webpack_require__("../../node_modules/react-refresh/runtime.js");
472
474
 
473
475
  let internalFunc;
474
- internalFunc = ()=>"Hello, I'm in browser";
476
+ if (true) {
477
+ internalFunc = ()=>"Hello, I'm in browser";
478
+ }
479
+ if (false) {}
475
480
  const func = ()=>{
476
481
  return internalFunc();
477
482
  };
@@ -514,7 +519,7 @@ if (typeof Promise !== 'undefined' && $ReactRefreshCurrentExports$ instanceof Pr
514
519
  exports[`server: create-token-pure.ts 1`] = `
515
520
  "__webpack_require__.r(__webpack_exports__);
516
521
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
517
- /* harmony export */ "TEST_TOKEN": function() { return /* binding */ TEST_TOKEN; }
522
+ /* harmony export */ TEST_TOKEN: function() { return /* binding */ TEST_TOKEN; }
518
523
  /* harmony export */ });
519
524
  /* harmony import */ var _tramvai_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/library/swc/__integration__/mocks/tramvai-core.ts");
520
525
 
@@ -609,11 +614,11 @@ const SvgLogo = (props)=>/*#__PURE__*/ (0,react_jsx_dev_runtime__WEBPACK_IMPORTE
609
614
  exports[`server: lazy-component.tsx 1`] = `
610
615
  "__webpack_require__.r(__webpack_exports__);
611
616
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
612
- /* harmony export */ "Component": function() { return /* binding */ Component; }
617
+ /* harmony export */ Component: function() { return /* binding */ Component; }
613
618
  /* harmony export */ });
614
619
  /* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("webpack/sharing/consume/default/react/jsx-dev-runtime/react/jsx-dev-runtime");
615
620
  /* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__);
616
- /* harmony import */ var _swc_helpers__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__("../../node_modules/@swc/helpers/src/_interop_require_wildcard.mjs");
621
+ /* harmony import */ var _swc_helpers__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__("../../node_modules/@swc/helpers/esm/_interop_require_wildcard.js");
617
622
  /* harmony import */ var _tramvai_react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/library/swc/__integration__/mocks/tramvai-react.ts");
618
623
 
619
624
 
@@ -629,7 +634,7 @@ const LazyComponent = (0,_tramvai_react__WEBPACK_IMPORTED_MODULE_1__.lazy)({
629
634
  const key = this.resolve(props);
630
635
  return !!__webpack_require__.m[key];
631
636
  },
632
- importAsync: ()=>Promise.resolve().then(()=>(0,_swc_helpers__WEBPACK_IMPORTED_MODULE_2__["default"])(__webpack_require__(/* webpackChunkName: "components-lazy-inner" */ "./src/library/swc/__integration__/components/lazy-inner.tsx"))),
637
+ importAsync: ()=>Promise.resolve().then(()=>(0,_swc_helpers__WEBPACK_IMPORTED_MODULE_2__._interop_require_wildcard)(__webpack_require__(/* webpackChunkName: "components-lazy-inner" */ "./src/library/swc/__integration__/components/lazy-inner.tsx"))),
633
638
  requireAsync (props) {
634
639
  return this.importAsync(props).then((resolved)=>{
635
640
  return resolved;
@@ -665,10 +670,12 @@ const Component = ()=>{
665
670
  exports[`server: node-env.ts 1`] = `
666
671
  "__webpack_require__.r(__webpack_exports__);
667
672
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
668
- /* harmony export */ "func": function() { return /* binding */ func; }
673
+ /* harmony export */ func: function() { return /* binding */ func; }
669
674
  /* harmony export */ });
670
675
  let internalFunc;
671
- if (true) internalFunc = ()=>"Are you developer, for sure?";
676
+ if (true) {
677
+ internalFunc = ()=>"Are you developer, for sure?";
678
+ }
672
679
  if (false) {}
673
680
  const func = ()=>{
674
681
  return internalFunc();
@@ -681,7 +688,7 @@ const func = ()=>{
681
688
  exports[`server: provider-stack.ts 1`] = `
682
689
  "__webpack_require__.r(__webpack_exports__);
683
690
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
684
- /* harmony export */ "providers": function() { return /* binding */ providers; }
691
+ /* harmony export */ providers: function() { return /* binding */ providers; }
685
692
  /* harmony export */ });
686
693
  /* harmony import */ var _tramvai_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/library/swc/__integration__/mocks/tramvai-core.ts");
687
694
  /* harmony import */ var _create_token_pure__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./src/library/swc/__integration__/create-token-pure.ts");
@@ -705,7 +712,7 @@ const providers = [
705
712
  exports[`server: react-svg.tsx 1`] = `
706
713
  "__webpack_require__.r(__webpack_exports__);
707
714
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
708
- /* harmony export */ "Component": function() { return /* binding */ Component; }
715
+ /* harmony export */ Component: function() { return /* binding */ Component; }
709
716
  /* harmony export */ });
710
717
  /* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("webpack/sharing/consume/default/react/jsx-dev-runtime/react/jsx-dev-runtime");
711
718
  /* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_0__);
@@ -733,7 +740,7 @@ const Component = ()=>{
733
740
  exports[`server: server.inline.ts 1`] = `
734
741
  "__webpack_require__.r(__webpack_exports__);
735
742
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
736
- /* harmony export */ "ForBrowser": function() { return /* binding */ ForBrowser; }
743
+ /* harmony export */ ForBrowser: function() { return /* binding */ ForBrowser; }
737
744
  /* harmony export */ });
738
745
  var ForBrowser = /*#__PURE__*/ function() {
739
746
  "use strict";
@@ -753,10 +760,13 @@ ForBrowser.prop = "static";
753
760
  exports[`server: typeof-window.ts 1`] = `
754
761
  "__webpack_require__.r(__webpack_exports__);
755
762
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
756
- /* harmony export */ "func": function() { return /* binding */ func; }
763
+ /* harmony export */ func: function() { return /* binding */ func; }
757
764
  /* harmony export */ });
758
765
  let internalFunc;
759
- internalFunc = ()=>"I'm a big server";
766
+ if (false) {}
767
+ if (true) {
768
+ internalFunc = ()=>"I'm a big server";
769
+ }
760
770
  const func = ()=>{
761
771
  return internalFunc();
762
772
  };
@@ -20,9 +20,11 @@ import { pagesResolve } from '../../blocks/pagesResolve';
20
20
  import { configToEnv } from '../../blocks/configToEnv';
21
21
  import { DEFAULT_STATS_OPTIONS, DEFAULT_STATS_FIELDS } from '../../constants/stats';
22
22
  import { pwaBlock } from '../../blocks/pwa/client';
23
+ import type { ModuleFederationFixRangeOptions } from '../../plugins/ModuleFederationFixRange';
24
+ import { ModuleFederationFixRange } from '../../plugins/ModuleFederationFixRange';
23
25
 
24
26
  export default (configManager: ConfigManager<ApplicationConfigEntry>) => (config: Config) => {
25
- const { polyfill, fileSystemPages } = configManager;
27
+ const { polyfill, fileSystemPages, shared } = configManager;
26
28
 
27
29
  const portal = path.resolve(configManager.rootDir, `packages/${process.env.APP_ID}/portal.js`);
28
30
  const polyfillPath = path.resolve(configManager.rootDir, polyfill ?? 'src/polyfill');
@@ -52,6 +54,12 @@ export default (configManager: ConfigManager<ApplicationConfigEntry>) => (config
52
54
  .when(portalExists, (cfg) => cfg.entry('portal').add(portal))
53
55
  .when(polyfillExists, (cfg) => cfg.entry('polyfill').add(polyfillPath));
54
56
 
57
+ config.plugin('module-federation-validate-duplicates').use(ModuleFederationFixRange, [
58
+ {
59
+ flexibleTramvaiVersions: shared.flexibleTramvaiVersions,
60
+ } as ModuleFederationFixRangeOptions,
61
+ ]);
62
+
55
63
  config
56
64
  .plugin('stats-plugin')
57
65
  .use(StatsWriterPlugin, [
@@ -29,6 +29,30 @@ export const webpackServerConfig = ({
29
29
  config.optimization.set('moduleIds', 'named');
30
30
  // prevent modules from concatenation in single module to easier debug
31
31
  config.optimization.set('concatenateModules', false);
32
+
33
+ config.plugin('terser').use(TerserPlugin, [
34
+ {
35
+ extractComments: false,
36
+ terserOptions: {
37
+ ecma: 5, // на сервере в страницу встраивается код, который может подключаться через import и terser его соптимизирует в es6
38
+ mangle: false,
39
+ // сохраняем имена функций, чтобы легче было найти ошибку в трамвае
40
+ keep_fnames: true,
41
+ compress: {
42
+ passes: 2,
43
+ drop_debugger: !debug,
44
+ dead_code: true,
45
+ },
46
+ output: {
47
+ comments: true,
48
+ semicolons: false,
49
+ preserve_annotations: true,
50
+ indent_start: 2,
51
+ beautify: true,
52
+ },
53
+ },
54
+ },
55
+ ]);
32
56
  } else {
33
57
  config.plugin('terser').use(TerserPlugin, [
34
58
  {
@@ -39,6 +63,7 @@ export const webpackServerConfig = ({
39
63
  // сохраняем имена функций, чтобы легче было найти ошибку в трамвае
40
64
  keep_fnames: true,
41
65
  compress: {
66
+ passes: 2,
42
67
  drop_debugger: !debug,
43
68
  dead_code: true,
44
69
  },
@@ -27,6 +27,10 @@ export default (configManager: ConfigManager<CliConfigEntry>) => (config: Config
27
27
  indent_start: 2,
28
28
  beautify: true,
29
29
  },
30
+ compress: {
31
+ passes: 2,
32
+ drop_debugger: !debug,
33
+ },
30
34
  },
31
35
  },
32
36
  ]);
@@ -8,9 +8,11 @@ import files from '../../blocks/filesClient';
8
8
  import nodeClient from '../../blocks/nodeClient';
9
9
  import postcssAssets from '../../blocks/postcssAssets';
10
10
  import type { ChildAppConfigEntry } from '../../../../typings/configEntry/child-app';
11
+ import type { ModuleFederationFixRangeOptions } from '../../plugins/ModuleFederationFixRange';
12
+ import { ModuleFederationFixRange } from '../../plugins/ModuleFederationFixRange';
11
13
 
12
14
  export default (configManager: ConfigManager<ChildAppConfigEntry>) => (config: Config) => {
13
- const { name, version } = configManager;
15
+ const { name, version, shared } = configManager;
14
16
  config.name('client');
15
17
 
16
18
  config.batch(common(configManager));
@@ -32,5 +34,11 @@ export default (configManager: ConfigManager<ChildAppConfigEntry>) => (config: C
32
34
  },
33
35
  ]);
34
36
 
37
+ config.plugin('module-federation-validate-duplicates').use(ModuleFederationFixRange, [
38
+ {
39
+ flexibleTramvaiVersions: shared.flexibleTramvaiVersions,
40
+ } as ModuleFederationFixRangeOptions,
41
+ ]);
42
+
35
43
  config.batch(files(configManager)).batch(nodeClient(configManager));
36
44
  };
@@ -20,24 +20,24 @@ export const getSharedModules = (
20
20
  } = configManager;
21
21
  const isChild = type === 'child-app';
22
22
 
23
- let defaultDependenicesList = defaultTramvaiDependencies ? DEFAULT_DEPENDENCIES_LIST : [];
23
+ let defaultDependenciesList = defaultTramvaiDependencies ? DEFAULT_DEPENDENCIES_LIST : [];
24
24
 
25
25
  if (typeof defaultTramvaiDependencies === 'undefined') {
26
26
  if (type === 'child-app') {
27
- defaultDependenicesList = DEFAULT_DEPENDENCIES_LIST;
27
+ defaultDependenciesList = DEFAULT_DEPENDENCIES_LIST;
28
28
  } else if (type === 'application') {
29
29
  const packageJson = safeRequire(path.resolve(rootDir, 'package.json'), true);
30
30
 
31
31
  // add default dependencies only if child-app is in use to minimize bundle size for apps
32
32
  // without child-apps
33
33
  if (packageJson?.dependencies?.['@tramvai/module-child-app']) {
34
- defaultDependenicesList = DEFAULT_DEPENDENCIES_LIST;
34
+ defaultDependenciesList = DEFAULT_DEPENDENCIES_LIST;
35
35
  }
36
36
  }
37
37
  }
38
38
 
39
39
  return {
40
- ...defaultDependenicesList.concat(deps).reduce((acc, dep) => {
40
+ ...defaultDependenciesList.concat(deps).reduce((acc, dep) => {
41
41
  const { name, singleton = false } = typeof dep === 'string' ? { name: dep } : dep;
42
42
 
43
43
  acc[name] = {
@@ -0,0 +1,174 @@
1
+ import type webpack from 'webpack';
2
+ import type { Compiler, NormalModule } from 'webpack';
3
+ import { sync as resolve } from 'resolve';
4
+ import type packageJson from 'package-json';
5
+ import chalk from 'chalk';
6
+ // eslint-disable-next-line no-restricted-imports
7
+ import { parseRange } from 'webpack/lib/util/semver';
8
+ import { isDependantLib, isUnifiedVersion } from '../../../utils/tramvaiVersions';
9
+
10
+ const PLUGIN_NAME = 'ModuleFederationValidateDuplicates';
11
+
12
+ type SemverRange = [number, number, number, number];
13
+
14
+ interface SharedModuleOptions {
15
+ shareKey: string;
16
+ requiredVersion?: SemverRange;
17
+ importResolved: string;
18
+ singleton?: boolean;
19
+ }
20
+
21
+ interface SharedModule extends NormalModule {
22
+ options?: SharedModuleOptions;
23
+ }
24
+
25
+ const resolvePackageVersion = (name: string, basedir: string) => {
26
+ try {
27
+ const packageJsonPath = resolve(`${name}/package.json`, {
28
+ basedir,
29
+ });
30
+ const packageJson: packageJson.FullMetadataOptions = require(packageJsonPath);
31
+
32
+ return packageJson.version;
33
+ } catch (error: any) {
34
+ console.warn(`ModuleFederation: could not infer version for package "${name}"`);
35
+ }
36
+ };
37
+
38
+ export interface ModuleFederationFixRangeOptions {
39
+ flexibleTramvaiVersions: boolean;
40
+ }
41
+
42
+ export class ModuleFederationFixRange implements webpack.WebpackPluginInstance {
43
+ private flexibleTramvaiVersions: boolean;
44
+ // { name: { importResolved: { number }} }
45
+ private sharedModules: Map<string, Map<string, Set<number>>> = new Map();
46
+
47
+ constructor({ flexibleTramvaiVersions }: ModuleFederationFixRangeOptions) {
48
+ this.flexibleTramvaiVersions = flexibleTramvaiVersions ?? false;
49
+ }
50
+
51
+ apply(compiler: Compiler) {
52
+ compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (_, { normalModuleFactory }) => {
53
+ normalModuleFactory.hooks.factorize.intercept({
54
+ register: (tap) => {
55
+ if (tap.name === 'ConsumeSharedPlugin') {
56
+ const originalFn = tap.fn;
57
+ // eslint-disable-next-line no-param-reassign
58
+ tap.fn = async (...args) => {
59
+ const module: SharedModule | undefined = await originalFn(...args);
60
+
61
+ if (module?.options) {
62
+ this.fixVersionRange(module);
63
+ }
64
+
65
+ return module;
66
+ };
67
+ }
68
+
69
+ return tap;
70
+ },
71
+ });
72
+ });
73
+
74
+ compiler.hooks.done.tap(PLUGIN_NAME, () => {
75
+ const duplicates: Array<{ name: string; paths: string[] }> = [];
76
+ const criticalDuplicates: Array<{ name: string; path: string }> = [];
77
+
78
+ for (const [name, shared] of this.sharedModules) {
79
+ if (shared.size > 1) {
80
+ duplicates.push({
81
+ name,
82
+ paths: [...shared.keys()],
83
+ });
84
+ }
85
+
86
+ for (const [path, versions] of shared) {
87
+ if (versions.size > 1) {
88
+ criticalDuplicates.push({ name, path });
89
+ }
90
+ }
91
+ }
92
+
93
+ // reset sharedModules info after compilation has ended
94
+ this.sharedModules = new Map();
95
+
96
+ if (duplicates.length) {
97
+ console.warn(`⚠️ ModuleFederation: Found duplicates for next shared modules:
98
+ ${duplicates
99
+ .map(({ name, paths }) => {
100
+ return `\t${chalk.yellowBright(name)}: ${paths.join(', ')}`;
101
+ })
102
+ .join('\n')}
103
+ `);
104
+ }
105
+
106
+ if (criticalDuplicates.length) {
107
+ console.error(
108
+ `❗ ModuleFederation: Found duplicates for shared modules with different major versions that are resolved to the same path:
109
+ ${criticalDuplicates
110
+ .map(({ name, path }) => {
111
+ return `\t${chalk.red(name)}: ${path}`;
112
+ })
113
+ .join('\n')}`
114
+ );
115
+
116
+ throw new Error(
117
+ 'ModuleFederation: Different major versions have resolved to the same path for shared modules, please review errors above'
118
+ );
119
+ }
120
+ });
121
+ }
122
+
123
+ fixVersionRange(module: SharedModule) {
124
+ const {
125
+ options,
126
+ options: { shareKey: name, singleton, importResolved },
127
+ context,
128
+ } = module;
129
+ let { requiredVersion } = options;
130
+
131
+ // ignore singletons as the actual version won't change anything
132
+ // and usually it is just a react and co libraries
133
+ if (!requiredVersion && context && !singleton) {
134
+ const version = resolvePackageVersion(name, context);
135
+ if (version) {
136
+ requiredVersion = parseRange(version);
137
+ }
138
+ }
139
+
140
+ if (!requiredVersion) {
141
+ return;
142
+ }
143
+
144
+ if (requiredVersion && this.flexibleTramvaiVersions) {
145
+ const isTramvai = isUnifiedVersion(name) || isDependantLib(name);
146
+
147
+ if (isTramvai && requiredVersion[0] > 2) {
148
+ // 1 is used for `^` range modifier
149
+ // see https://github.com/webpack/webpack/blob/56363993156e06a1230c9759eba277a22e6b6604/lib/util/semver.js#LL235C20-L235C20
150
+ requiredVersion[0] = 1;
151
+ }
152
+ }
153
+
154
+ // change version in webpack module
155
+ options.requiredVersion = requiredVersion;
156
+
157
+ let shared = this.sharedModules.get(name);
158
+
159
+ if (!shared) {
160
+ shared = new Map();
161
+ this.sharedModules.set(name, shared);
162
+ }
163
+
164
+ let versions = shared.get(importResolved);
165
+
166
+ if (!versions) {
167
+ versions = new Set();
168
+ shared.set(importResolved, versions);
169
+ }
170
+
171
+ // save major version of the semver array
172
+ versions.add(requiredVersion[1]);
173
+ }
174
+ }
@@ -103,6 +103,7 @@ it('should populate defaults for config', () => {
103
103
  "serverApiDir": "src/api",
104
104
  "shared": {
105
105
  "deps": [],
106
+ "flexibleTramvaiVersions": true,
106
107
  },
107
108
  "sourceMap": false,
108
109
  "splitChunks": {
@@ -159,6 +160,7 @@ it('should populate defaults for config', () => {
159
160
  "root": "packages/child-app",
160
161
  "shared": {
161
162
  "deps": [],
163
+ "flexibleTramvaiVersions": true,
162
164
  },
163
165
  "sourceMap": false,
164
166
  "terser": {
@@ -336,6 +338,7 @@ it('should populate defaults for overridable options', () => {
336
338
  "serverApiDir": "src/api",
337
339
  "shared": {
338
340
  "deps": [],
341
+ "flexibleTramvaiVersions": true,
339
342
  },
340
343
  "sourceMap": false,
341
344
  "splitChunks": {
@@ -406,6 +409,7 @@ it('should populate defaults for overridable options', () => {
406
409
  "root": "packages/child-app",
407
410
  "shared": {
408
411
  "deps": [],
412
+ "flexibleTramvaiVersions": true,
409
413
  },
410
414
  "sourceMap": {
411
415
  "development": true,
@@ -1225,7 +1225,13 @@
1225
1225
  "properties": {
1226
1226
  "defaultTramvaiDependencies": {
1227
1227
  "title": "Should default dependencies list be added to shared list",
1228
- "description": "It includes the list of commonly used dependencies in the child-apps\nBy default, it is enabled in application in case of",
1228
+ "description": "It includes the list of commonly used dependencies in the child-apps\nBy default, it is enabled in application in case of tramvai/module-child-app is specified in package.json\nand for child-apps",
1229
+ "type": "boolean"
1230
+ },
1231
+ "flexibleTramvaiVersions": {
1232
+ "title": "add caret range specifier for tramvai dependencies",
1233
+ "description": "minimal versions are inferred from package.json",
1234
+ "default": true,
1229
1235
  "type": "boolean"
1230
1236
  },
1231
1237
  "deps": {
@@ -1746,7 +1752,13 @@
1746
1752
  "properties": {
1747
1753
  "defaultTramvaiDependencies": {
1748
1754
  "title": "Should default dependencies list be added to shared list",
1749
- "description": "It includes the list of commonly used dependencies in the child-apps\nBy default, it is enabled in application in case of",
1755
+ "description": "It includes the list of commonly used dependencies in the child-apps\nBy default, it is enabled in application in case of tramvai/module-child-app is specified in package.json\nand for child-apps",
1756
+ "type": "boolean"
1757
+ },
1758
+ "flexibleTramvaiVersions": {
1759
+ "title": "add caret range specifier for tramvai dependencies",
1760
+ "description": "minimal versions are inferred from package.json",
1761
+ "default": true,
1750
1762
  "type": "boolean"
1751
1763
  },
1752
1764
  "deps": {
@@ -2267,7 +2279,13 @@
2267
2279
  "properties": {
2268
2280
  "defaultTramvaiDependencies": {
2269
2281
  "title": "Should default dependencies list be added to shared list",
2270
- "description": "It includes the list of commonly used dependencies in the child-apps\nBy default, it is enabled in application in case of",
2282
+ "description": "It includes the list of commonly used dependencies in the child-apps\nBy default, it is enabled in application in case of tramvai/module-child-app is specified in package.json\nand for child-apps",
2283
+ "type": "boolean"
2284
+ },
2285
+ "flexibleTramvaiVersions": {
2286
+ "title": "add caret range specifier for tramvai dependencies",
2287
+ "description": "minimal versions are inferred from package.json",
2288
+ "default": true,
2271
2289
  "type": "boolean"
2272
2290
  },
2273
2291
  "deps": {
@@ -126,6 +126,7 @@ describe('JSON schema для tramvai.json', () => {
126
126
  "serverApiDir": "src/api",
127
127
  "shared": {
128
128
  "deps": [],
129
+ "flexibleTramvaiVersions": true,
129
130
  },
130
131
  "sourceMap": false,
131
132
  "splitChunks": {
@@ -182,6 +183,7 @@ describe('JSON schema для tramvai.json', () => {
182
183
  "root": "src/module",
183
184
  "shared": {
184
185
  "deps": [],
186
+ "flexibleTramvaiVersions": true,
185
187
  },
186
188
  "sourceMap": false,
187
189
  "terser": {
@@ -287,10 +287,16 @@ export interface CliConfigEntry extends ConfigEntry {
287
287
  /**
288
288
  * @title Should default dependencies list be added to shared list
289
289
  * @description It includes the list of commonly used dependencies in the child-apps
290
- * By default, it is enabled in application in case of @tramvai/module-child-app is specified in package.json
290
+ * By default, it is enabled in application in case of tramvai/module-child-app is specified in package.json
291
291
  * and for child-apps
292
292
  */
293
293
  defaultTramvaiDependencies?: boolean;
294
+ /**
295
+ * @title add caret range specifier for tramvai dependencies
296
+ * @description minimal versions are inferred from package.json
297
+ * @default true
298
+ */
299
+ flexibleTramvaiVersions: boolean;
294
300
  /**
295
301
  * @title list of the dependencies that will be shared
296
302
  * @default []
@@ -0,0 +1,26 @@
1
+ // map of packages that is not in unified versioning
2
+ // but we still want to update it
3
+ // actual version to update will be calculated from the some of the @tramvai/module
4
+ export const DEPENDANT_LIBS_MAP = new Map([
5
+ ['@tinkoff/logger', '@tramvai/module-log'],
6
+ ['@tinkoff/dippy', '@tramvai/core'],
7
+ ['@tinkoff/router', '@tramvai/module-router'],
8
+ ['@tinkoff/url', '@tramvai/module-common'],
9
+ ['@tinkoff/errors', '@tramvai/module-common'],
10
+ ['@tinkoff/roles', '@tramvai/module-authenticate'],
11
+ ['@tinkoff/pubsub', '@tramvai/module-common'],
12
+ ['@tinkoff/hook-runner', '@tramvai/module-common'],
13
+ ['@tinkoff/htmlpagebuilder', '@tramvai/module-render'],
14
+ ['@tinkoff/browser-timings', '@tramvai/module-metrics'],
15
+ ['@tinkoff/meta-tags-generate', '@tramvai/module-render'],
16
+ ['@tinkoff/pack-polyfills', ''],
17
+ ['@tinkoff/browserslist-config', '@tramvai/cli'],
18
+ ]);
19
+
20
+ export const isUnifiedVersion = (name: string) => {
21
+ return name.startsWith('@tramvai');
22
+ };
23
+
24
+ export const isDependantLib = (name: string) => {
25
+ return DEPENDANT_LIBS_MAP.has(name);
26
+ };