@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.
- package/README.md +14 -0
- package/cli.js +104 -22
- 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
|
|
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
|
-
|
|
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] =
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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.
|
|
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"
|