@touk/federated-types 1.3.0-beta.6 → 1.3.0-beta.7

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 (3) hide show
  1. package/README.md +14 -0
  2. package/cli.js +77 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -82,6 +82,20 @@ scripts: {
82
82
  }
83
83
  ```
84
84
 
85
+ ## Pruning unreachable modules
86
+
87
+ By default the generated typings contain every module of the TypeScript program built from your exposes, including modules that are only used by their implementation. Pass `--prune` to keep only the modules reachable from the exposed modules through their types (`import`/`export ... from`, `import("...")`, `import "..."`, `require("...")`):
88
+
89
+ ```javascript
90
+ //package.json
91
+
92
+ scripts: {
93
+ "make-types": "make-federated-types --prune"
94
+ }
95
+ ```
96
+
97
+ Modules that augment the global scope (`declare global`) or other modules are always kept. With `--prune`, non-exposed modules can no longer be imported by their deep name (e.g. `myApp/utils/internal`) unless they are reachable from an exposed module.
98
+
85
99
  ## Inline configuration (without config file)
86
100
 
87
101
  If you prefer not to maintain a separate `federation.config.json` file, you can provide the configuration directly via CLI arguments using `--name` and `--exposes`:
package/cli.js CHANGED
@@ -68,6 +68,7 @@ const configPath = configPathArg ? path.resolve(configPathArg) : null;
68
68
  const nameArg = getArg('--name');
69
69
  const exposesArgs = getAllArgs('--exposes');
70
70
  const verboseArg = hasArg('--verbose') || process.env.npm_config_loglevel === 'verbose';
71
+ const pruneArg = hasArg('--prune');
71
72
 
72
73
  const log = (...args) => console.log(...args);
73
74
  const logDebug = (...args) => verboseArg && console.debug(chalk.gray.dim(...args));
@@ -155,6 +156,73 @@ function getModuleDeclareName(exposeName) {
155
156
  return path.join(federationConfig.name, exposeName).replace(/[\\/]/g, '/');
156
157
  }
157
158
 
159
+ // Matches the module specifier of `from "x"`, `import("x")`, `import "x"` and `require("x")`.
160
+ const moduleReferenceRegex = /\b(?:from|import|require)\s*\(?\s*(["'])([^"'\n]+)\1/g;
161
+ // Global or module augmentations take effect without being imported, so such modules are always kept.
162
+ // Inside a `declare module` block tsc emits them without the `declare` keyword.
163
+ const augmentationRegex = /^\s+(?:declare\s+)?(?:global\s*\{|module\s+["'])/m;
164
+
165
+ /**
166
+ * Drops the top-level `declare module` blocks that cannot be reached from the exposed modules
167
+ * through module references. Text outside the blocks is kept as is.
168
+ */
169
+ function pruneUnreachableModules(typing, isExposedModule) {
170
+ const blockRegex = /^declare module "([^"]+)" \{\n[\s\S]*?^\}\n?/gm;
171
+ const segments = [];
172
+ const modules = new Map();
173
+ let lastIndex = 0;
174
+ let match;
175
+
176
+ while ((match = blockRegex.exec(typing)) !== null) {
177
+ segments.push(typing.slice(lastIndex, match.index));
178
+ const block = { name: match[1], text: match[0] };
179
+ segments.push(block);
180
+ modules.set(block.name, block);
181
+ lastIndex = blockRegex.lastIndex;
182
+ }
183
+ segments.push(typing.slice(lastIndex));
184
+
185
+ const roots = Array.from(modules.values()).filter(
186
+ (block) => isExposedModule(block.name) || augmentationRegex.test(block.text)
187
+ );
188
+
189
+ if (!roots.some((block) => isExposedModule(block.name))) {
190
+ console.warn(
191
+ chalk.yellow('WARNING:'),
192
+ 'No exposed module found in the emitted typings, skipping --prune'
193
+ );
194
+ return typing;
195
+ }
196
+
197
+ const reachable = new Set();
198
+ const queue = [];
199
+ const visit = (text) => {
200
+ for (const [, , name] of text.matchAll(moduleReferenceRegex)) {
201
+ const block = modules.get(name);
202
+ if (block && !reachable.has(block)) {
203
+ reachable.add(block);
204
+ queue.push(block);
205
+ }
206
+ }
207
+ };
208
+
209
+ segments.filter((segment) => typeof segment === 'string').forEach(visit);
210
+ roots.forEach((block) => {
211
+ reachable.add(block);
212
+ queue.push(block);
213
+ });
214
+ while (queue.length > 0) {
215
+ visit(queue.pop().text);
216
+ }
217
+
218
+ logDebug(`--prune: kept ${reachable.size} of ${modules.size} modules`);
219
+
220
+ return segments
221
+ .filter((segment) => typeof segment === 'string' || reachable.has(segment))
222
+ .map((segment) => (typeof segment === 'string' ? segment : segment.text))
223
+ .join('');
224
+ }
225
+
158
226
  try {
159
227
  fs.mkdirSync(outputDir, { recursive: true });
160
228
  const outFile = path.resolve(outputDir, `${federationConfig.name}.d.ts`);
@@ -188,6 +256,14 @@ try {
188
256
  key,
189
257
  federationConfig.exposes[key].replace(/\.(tsx?|jsx?)$/, ''),
190
258
  ]);
259
+ const findExposeKeys = (name) =>
260
+ exposedPaths
261
+ .filter(([, normalizedPath]) => normalizedPath.endsWith(name))
262
+ .map(([key]) => key);
263
+
264
+ if (pruneArg) {
265
+ typing = pruneUnreachableModules(typing, (name) => findExposeKeys(name).length > 0);
266
+ }
191
267
 
192
268
  // module name emitted by tsc -> name it is declared under in the federated typings
193
269
  const renames = new Map();
@@ -201,9 +277,7 @@ try {
201
277
  }
202
278
 
203
279
  // exposeName - relative name of exposed component (if not found - just take moduleName)
204
- const [exposeName = name, ...aliases] = exposedPaths
205
- .filter(([, normalizedPath]) => normalizedPath.endsWith(name))
206
- .map(([key]) => key);
280
+ const [exposeName = name, ...aliases] = findExposeKeys(name);
207
281
 
208
282
  const moduleDeclareName = getModuleDeclareName(exposeName);
209
283
  renames.set(name, moduleDeclareName);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@touk/federated-types",
3
3
  "description": "share typings for your federated typescript packages around your mono-repo",
4
- "version": "1.3.0-beta.6",
4
+ "version": "1.3.0-beta.7",
5
5
  "author": "Julian Wielga <j@touk.pl>",
6
6
  "bin": {
7
7
  "make-federated-types": "./cli.js"