@touk/federated-types 1.3.0-beta.6 → 1.3.0-beta.8
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.
- package/README.md +14 -0
- package/cli.js +136 -9
- package/package.json +2 -3
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
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
-
const findNodeModules = require('find-node-modules');
|
|
6
5
|
const ts = require('typescript');
|
|
7
6
|
const chalk = require('chalk');
|
|
8
7
|
|
|
@@ -20,7 +19,20 @@ function reportDiagnostic(diagnostic) {
|
|
|
20
19
|
);
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
// nearest node_modules directory up from the CLI script
|
|
23
|
+
const findNodeModules = (start) => {
|
|
24
|
+
let dir = path.resolve(start);
|
|
25
|
+
while (!fs.existsSync(path.join(dir, 'node_modules'))) {
|
|
26
|
+
const parent = path.dirname(dir);
|
|
27
|
+
if (parent === dir) {
|
|
28
|
+
return path.resolve('node_modules');
|
|
29
|
+
}
|
|
30
|
+
dir = parent;
|
|
31
|
+
}
|
|
32
|
+
return path.join(dir, 'node_modules');
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const nodeModules = findNodeModules(process.argv[1]);
|
|
24
36
|
|
|
25
37
|
const hasArg = (argName) => {
|
|
26
38
|
const argIndex = process.argv.indexOf(argName);
|
|
@@ -68,6 +80,7 @@ const configPath = configPathArg ? path.resolve(configPathArg) : null;
|
|
|
68
80
|
const nameArg = getArg('--name');
|
|
69
81
|
const exposesArgs = getAllArgs('--exposes');
|
|
70
82
|
const verboseArg = hasArg('--verbose') || process.env.npm_config_loglevel === 'verbose';
|
|
83
|
+
const pruneArg = hasArg('--prune');
|
|
71
84
|
|
|
72
85
|
const log = (...args) => console.log(...args);
|
|
73
86
|
const logDebug = (...args) => verboseArg && console.debug(chalk.gray.dim(...args));
|
|
@@ -155,6 +168,102 @@ function getModuleDeclareName(exposeName) {
|
|
|
155
168
|
return path.join(federationConfig.name, exposeName).replace(/[\\/]/g, '/');
|
|
156
169
|
}
|
|
157
170
|
|
|
171
|
+
const toPosixPath = (value) => value.replace(/[\\/]/g, '/');
|
|
172
|
+
const stripSourceExtension = (file) => file.replace(/\.(d\.ts|[cm]?tsx?|[cm]?jsx?)$/, '');
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Maps the absolute path (without extension) of every source file in the program to the module
|
|
176
|
+
* name tsc gives it in an outFile bundle: its path relative to the common source directory.
|
|
177
|
+
* Exposes outside of the exposing package (e.g. a package importing a sibling's sources) move
|
|
178
|
+
* that directory up, so the module name cannot be derived from the configured path alone.
|
|
179
|
+
*/
|
|
180
|
+
function getEmittedModuleNames(program) {
|
|
181
|
+
// internal, but stable TypeScript API; fall back to suffix matching without it
|
|
182
|
+
if (typeof program.getCommonSourceDirectory !== 'function') {
|
|
183
|
+
return new Map();
|
|
184
|
+
}
|
|
185
|
+
const commonSourceDirectory = program.getCommonSourceDirectory();
|
|
186
|
+
return new Map(
|
|
187
|
+
program
|
|
188
|
+
.getSourceFiles()
|
|
189
|
+
.filter((sourceFile) => !sourceFile.isDeclarationFile)
|
|
190
|
+
.map((sourceFile) => {
|
|
191
|
+
const absolutePath = stripSourceExtension(path.resolve(sourceFile.fileName));
|
|
192
|
+
return [
|
|
193
|
+
toPosixPath(absolutePath),
|
|
194
|
+
toPosixPath(path.relative(commonSourceDirectory, absolutePath)),
|
|
195
|
+
];
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Matches the module specifier of `from "x"`, `import("x")`, `import "x"` and `require("x")`.
|
|
201
|
+
const moduleReferenceRegex = /\b(?:from|import|require)\s*\(?\s*(["'])([^"'\n]+)\1/g;
|
|
202
|
+
// Global or module augmentations take effect without being imported, so such modules are always kept.
|
|
203
|
+
// Inside a `declare module` block tsc emits them without the `declare` keyword.
|
|
204
|
+
const augmentationRegex = /^\s+(?:declare\s+)?(?:global\s*\{|module\s+["'])/m;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Drops the top-level `declare module` blocks that cannot be reached from the exposed modules
|
|
208
|
+
* through module references. Text outside the blocks is kept as is.
|
|
209
|
+
*/
|
|
210
|
+
function pruneUnreachableModules(typing, isExposedModule) {
|
|
211
|
+
const blockRegex = /^declare module "([^"]+)" \{\n[\s\S]*?^\}\n?/gm;
|
|
212
|
+
const segments = [];
|
|
213
|
+
const modules = new Map();
|
|
214
|
+
let lastIndex = 0;
|
|
215
|
+
let match;
|
|
216
|
+
|
|
217
|
+
while ((match = blockRegex.exec(typing)) !== null) {
|
|
218
|
+
segments.push(typing.slice(lastIndex, match.index));
|
|
219
|
+
const block = { name: match[1], text: match[0] };
|
|
220
|
+
segments.push(block);
|
|
221
|
+
modules.set(block.name, block);
|
|
222
|
+
lastIndex = blockRegex.lastIndex;
|
|
223
|
+
}
|
|
224
|
+
segments.push(typing.slice(lastIndex));
|
|
225
|
+
|
|
226
|
+
const roots = Array.from(modules.values()).filter(
|
|
227
|
+
(block) => isExposedModule(block.name) || augmentationRegex.test(block.text)
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (!roots.some((block) => isExposedModule(block.name))) {
|
|
231
|
+
console.warn(
|
|
232
|
+
chalk.yellow('WARNING:'),
|
|
233
|
+
'No exposed module found in the emitted typings, skipping --prune'
|
|
234
|
+
);
|
|
235
|
+
return typing;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const reachable = new Set();
|
|
239
|
+
const queue = [];
|
|
240
|
+
const visit = (text) => {
|
|
241
|
+
for (const [, , name] of text.matchAll(moduleReferenceRegex)) {
|
|
242
|
+
const block = modules.get(name);
|
|
243
|
+
if (block && !reachable.has(block)) {
|
|
244
|
+
reachable.add(block);
|
|
245
|
+
queue.push(block);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
segments.filter((segment) => typeof segment === 'string').forEach(visit);
|
|
251
|
+
roots.forEach((block) => {
|
|
252
|
+
reachable.add(block);
|
|
253
|
+
queue.push(block);
|
|
254
|
+
});
|
|
255
|
+
while (queue.length > 0) {
|
|
256
|
+
visit(queue.pop().text);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
logDebug(`--prune: kept ${reachable.size} of ${modules.size} modules`);
|
|
260
|
+
|
|
261
|
+
return segments
|
|
262
|
+
.filter((segment) => typeof segment === 'string' || reachable.has(segment))
|
|
263
|
+
.map((segment) => (typeof segment === 'string' ? segment : segment.text))
|
|
264
|
+
.join('');
|
|
265
|
+
}
|
|
266
|
+
|
|
158
267
|
try {
|
|
159
268
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
160
269
|
const outFile = path.resolve(outputDir, `${federationConfig.name}.d.ts`);
|
|
@@ -184,10 +293,30 @@ try {
|
|
|
184
293
|
let typing = fs.readFileSync(outFile, { encoding: 'utf8', flag: 'r' });
|
|
185
294
|
|
|
186
295
|
const moduleRegex = RegExp(/declare module "(.*)"/, 'g');
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
federationConfig.exposes[key]
|
|
190
|
-
|
|
296
|
+
const emittedModuleNames = getEmittedModuleNames(program);
|
|
297
|
+
const exposedModules = compileKeys.map((key) => {
|
|
298
|
+
const exposePath = federationConfig.exposes[key];
|
|
299
|
+
const absolutePath = toPosixPath(stripSourceExtension(path.resolve(exposePath)));
|
|
300
|
+
return {
|
|
301
|
+
key,
|
|
302
|
+
relativePath: exposePath.replace(/\.(tsx?|jsx?)$/, ''),
|
|
303
|
+
moduleName:
|
|
304
|
+
emittedModuleNames.get(absolutePath) ??
|
|
305
|
+
emittedModuleNames.get(`${absolutePath}/index`),
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
// Falls back to suffix matching of the configured path when the source file could not be
|
|
309
|
+
// resolved in the program.
|
|
310
|
+
const findExposeKeys = (name) =>
|
|
311
|
+
exposedModules
|
|
312
|
+
.filter(({ relativePath, moduleName }) =>
|
|
313
|
+
moduleName !== undefined ? moduleName === name : relativePath.endsWith(name)
|
|
314
|
+
)
|
|
315
|
+
.map(({ key }) => key);
|
|
316
|
+
|
|
317
|
+
if (pruneArg) {
|
|
318
|
+
typing = pruneUnreachableModules(typing, (name) => findExposeKeys(name).length > 0);
|
|
319
|
+
}
|
|
191
320
|
|
|
192
321
|
// module name emitted by tsc -> name it is declared under in the federated typings
|
|
193
322
|
const renames = new Map();
|
|
@@ -201,9 +330,7 @@ try {
|
|
|
201
330
|
}
|
|
202
331
|
|
|
203
332
|
// exposeName - relative name of exposed component (if not found - just take moduleName)
|
|
204
|
-
const [exposeName = name, ...aliases] =
|
|
205
|
-
.filter(([, normalizedPath]) => normalizedPath.endsWith(name))
|
|
206
|
-
.map(([key]) => key);
|
|
333
|
+
const [exposeName = name, ...aliases] = findExposeKeys(name);
|
|
207
334
|
|
|
208
335
|
const moduleDeclareName = getModuleDeclareName(exposeName);
|
|
209
336
|
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.
|
|
4
|
+
"version": "1.3.0-beta.8",
|
|
5
5
|
"author": "Julian Wielga <j@touk.pl>",
|
|
6
6
|
"bin": {
|
|
7
7
|
"make-federated-types": "./cli.js"
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
"Jonathan Greene <jgreene@pixability.com>"
|
|
11
11
|
],
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"chalk": "^4.1.2"
|
|
14
|
-
"find-node-modules": "2.1.3"
|
|
13
|
+
"chalk": "^4.1.2"
|
|
15
14
|
},
|
|
16
15
|
"devDependencies": {
|
|
17
16
|
"@commitlint/cli": "^20.4.2",
|