@tramvai/cli 2.111.0 → 2.112.0

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 (55) hide show
  1. package/lib/api/start/providers/application/shared.js +4 -4
  2. package/lib/api/start/providers/application/shared.js.map +1 -1
  3. package/lib/api/start/providers/child-app/shared.js +1 -4
  4. package/lib/api/start/providers/child-app/shared.js.map +1 -1
  5. package/lib/api/start/providers/module/shared.js +1 -4
  6. package/lib/api/start/providers/module/shared.js.map +1 -1
  7. package/lib/api/start-prod/providers/application.js +4 -4
  8. package/lib/api/start-prod/providers/application.js.map +1 -1
  9. package/lib/api/start-prod/providers/child-app.js +4 -4
  10. package/lib/api/start-prod/providers/child-app.js.map +1 -1
  11. package/lib/builder/webpack/tokens.d.ts +3 -0
  12. package/lib/commands/update/dependantLibs.d.ts +0 -1
  13. package/lib/commands/update/dependantLibs.js +3 -24
  14. package/lib/commands/update/dependantLibs.js.map +1 -1
  15. package/lib/commands/update/updatePackageJson.js +3 -5
  16. package/lib/commands/update/updatePackageJson.js.map +1 -1
  17. package/lib/di/tokens/config.d.ts +1 -0
  18. package/lib/library/typescript/index.d.ts +1 -1
  19. package/lib/library/webpack/application/client/common.js +7 -1
  20. package/lib/library/webpack/application/client/common.js.map +1 -1
  21. package/lib/library/webpack/child-app/client/common.js +7 -1
  22. package/lib/library/webpack/child-app/client/common.js.map +1 -1
  23. package/lib/library/webpack/child-app/moduleFederationShared.js +4 -4
  24. package/lib/library/webpack/plugins/ModuleFederationFixRange.d.ts +23 -0
  25. package/lib/library/webpack/plugins/ModuleFederationFixRange.js +125 -0
  26. package/lib/library/webpack/plugins/ModuleFederationFixRange.js.map +1 -0
  27. package/lib/schema/autogeneratedSchema.json +21 -3
  28. package/lib/typings/configEntry/cli.d.ts +7 -1
  29. package/lib/utils/detectPortSync.d.ts +13 -1
  30. package/lib/utils/detectPortSync.js +12 -2
  31. package/lib/utils/detectPortSync.js.map +1 -1
  32. package/lib/utils/tramvaiVersions.d.ts +3 -0
  33. package/lib/utils/tramvaiVersions.js +30 -0
  34. package/lib/utils/tramvaiVersions.js.map +1 -0
  35. package/package.json +6 -6
  36. package/schema.json +21 -3
  37. package/src/api/start/__integration__/start.test.ts +22 -3
  38. package/src/api/start/providers/application/shared.ts +5 -2
  39. package/src/api/start/providers/child-app/shared.ts +1 -1
  40. package/src/api/start/providers/module/shared.ts +1 -1
  41. package/src/api/start-prod/providers/application.ts +5 -2
  42. package/src/api/start-prod/providers/child-app.ts +4 -1
  43. package/src/commands/update/dependantLibs.ts +1 -23
  44. package/src/commands/update/updatePackageJson.ts +2 -5
  45. package/src/library/swc/__integration__/__snapshots__/swc.start.test.ts.snap +14 -14
  46. package/src/library/webpack/application/client/common.ts +9 -1
  47. package/src/library/webpack/child-app/client/common.ts +9 -1
  48. package/src/library/webpack/child-app/moduleFederationShared.ts +4 -4
  49. package/src/library/webpack/plugins/ModuleFederationFixRange.ts +174 -0
  50. package/src/models/config.spec.ts +4 -0
  51. package/src/schema/autogeneratedSchema.json +21 -3
  52. package/src/schema/tramvai.spec.ts +2 -0
  53. package/src/typings/configEntry/cli.ts +7 -1
  54. package/src/utils/detectPortSync.ts +18 -2
  55. package/src/utils/tramvaiVersions.ts +26 -0
@@ -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 []
@@ -1,7 +1,23 @@
1
1
  import { execSync } from 'child_process';
2
2
 
3
- export const detectPortSync = (port: number): number => {
4
- const commandResult = execSync(`npx detect-port ${port}`);
3
+ interface Payload {
4
+ request?: number;
5
+ fallback: number;
6
+ }
7
+
8
+ /**
9
+ * Try to detect port synchronously considering the fact, that if user requests
10
+ * a port explicitly, we should not try to detect a free one.
11
+ *
12
+ * Also, handle zero port (it means any random port) as the edge case,
13
+ * because we must pass a final number to the config manager.
14
+ */
15
+ export const detectPortSync = ({ request, fallback }: Payload): number => {
16
+ if (request !== undefined && request !== 0) {
17
+ return request;
18
+ }
19
+
20
+ const commandResult = execSync(`npx detect-port ${request ?? fallback}`);
5
21
 
6
22
  return parseInt(commandResult.toString('utf-8'), 10);
7
23
  };
@@ -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
+ };