@touk/federated-types 1.3.0-beta.5 → 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 +104 -22
  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`);
@@ -162,9 +230,6 @@ try {
162
230
  fs.unlinkSync(outFile);
163
231
  }
164
232
 
165
- const tsBuildInfoFile = path.resolve(process.cwd(), 'node_modules', '.cache', 'federated-types', `${federationConfig.name}.tsbuildinfo`);
166
- fs.mkdirSync(path.dirname(tsBuildInfoFile), { recursive: true });
167
-
168
233
  // write the typings file
169
234
  const program = ts.createProgram(compileFiles, {
170
235
  outFile,
@@ -172,8 +237,6 @@ try {
172
237
  emitDeclarationOnly: true,
173
238
  skipLibCheck: true,
174
239
  noCheck: true,
175
- incremental: true,
176
- tsBuildInfoFile,
177
240
  jsx: ts.JsxEmit.React,
178
241
  esModuleInterop: true,
179
242
  });
@@ -189,35 +252,54 @@ try {
189
252
  let typing = fs.readFileSync(outFile, { encoding: 'utf8', flag: 'r' });
190
253
 
191
254
  const moduleRegex = RegExp(/declare module "(.*)"/, 'g');
192
- const moduleNames = [];
255
+ const exposedPaths = compileKeys.map((key) => [
256
+ key,
257
+ federationConfig.exposes[key].replace(/\.(tsx?|jsx?)$/, ''),
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
+ }
267
+
268
+ // module name emitted by tsc -> name it is declared under in the federated typings
269
+ const renames = new Map();
270
+ const aliasModules = [];
193
271
 
194
272
  let execResults;
195
273
  while ((execResults = moduleRegex.exec(typing)) !== null) {
196
- moduleNames.push(execResults[1]);
197
- }
274
+ const name = execResults[1];
275
+ if (renames.has(name)) {
276
+ continue;
277
+ }
198
278
 
199
- moduleNames.forEach((name) => {
200
279
  // exposeName - relative name of exposed component (if not found - just take moduleName)
201
- const [exposeName = name, ...aliases] = compileKeys.filter((key) => {
202
- const normalizedPath = federationConfig.exposes[key].replace(/\.(tsx?|jsx?)$/, '');
203
- return normalizedPath.endsWith(name);
204
- });
205
- const regex = RegExp(`"${name}"`, 'g');
280
+ const [exposeName = name, ...aliases] = findExposeKeys(name);
206
281
 
207
282
  const moduleDeclareName = getModuleDeclareName(exposeName);
283
+ renames.set(name, moduleDeclareName);
208
284
 
209
285
  // language=TypeScript
210
- const createAliasModule = (name) => `
211
- declare module "${getModuleDeclareName(name)}" {
286
+ aliases.forEach((alias) =>
287
+ aliasModules.push(`
288
+ declare module "${getModuleDeclareName(alias)}" {
212
289
  export * from "${moduleDeclareName}"
213
290
  }
214
- `;
291
+ `)
292
+ );
293
+ }
215
294
 
216
- typing = [
217
- typing.replace(regex, `"${moduleDeclareName}"`),
218
- ...aliases.map(createAliasModule),
219
- ].join('\n');
220
- });
295
+ if (renames.size > 0) {
296
+ // Rename every exact quoted occurrence of a module name in a single pass.
297
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
298
+ const namesRegex = RegExp(`"(${Array.from(renames.keys(), escapeRegExp).join('|')})"`, 'g');
299
+ typing = typing.replace(namesRegex, (match, name) => `"${renames.get(name)}"`);
300
+ }
301
+
302
+ typing = [typing, ...aliasModules].join('\n');
221
303
 
222
304
  outputDirs.forEach((_outputDir) => {
223
305
  const _outFile = path.resolve(_outputDir, `${federationConfig.name}.d.ts`);
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.5",
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"