@softarc/native-federation 4.2.0 → 4.3.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.
- package/README.md +127 -39
- package/dist/config.d.ts +3 -1
- package/dist/config.js +4 -6
- package/dist/internal.d.ts +2 -1
- package/dist/internal.js +2 -0
- package/dist/lib/config/project-paths.d.ts +5 -0
- package/dist/lib/config/project-paths.js +49 -0
- package/dist/lib/config/secondaries.d.ts +5 -0
- package/dist/lib/config/secondaries.js +186 -0
- package/dist/lib/config/share-utils.d.ts +13 -10
- package/dist/lib/config/share-utils.js +58 -254
- package/dist/lib/config/version-lookup.d.ts +4 -0
- package/dist/lib/config/version-lookup.js +37 -0
- package/dist/lib/config/with-native-federation.js +8 -5
- package/dist/lib/core/build/build-for-federation.js +4 -9
- package/dist/lib/core/build/bundle-shared.js +4 -11
- package/dist/lib/core/output/densify-externals.d.ts +8 -0
- package/dist/lib/core/output/densify-externals.js +49 -0
- package/dist/lib/domain/config/external-config.contract.d.ts +13 -9
- package/dist/lib/domain/config/federation-config.contract.d.ts +2 -0
- package/dist/lib/domain/config/index.d.ts +1 -1
- package/dist/lib/domain/core/federation-info.contract.d.ts +5 -1
- package/dist/lib/domain/core/index.d.ts +1 -1
- package/dist/lib/utils/normalize.d.ts +1 -0
- package/dist/lib/utils/normalize.js +8 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @softarc/native-federation
|
|
2
2
|
|
|
3
|
-
Native Federation is a "browser-native" implementation of the successful mental model behind
|
|
3
|
+
Native Federation is a "browser-native" implementation of the successful mental model behind webpack Module Federation. It can be **used with any framework and build tool** for implementing **Micro Frontends** and plugin-based architectures.
|
|
4
4
|
|
|
5
5
|
> [!WARNING]
|
|
6
6
|
> **This is our v4 version**. For the v3 version, check out the [module-federation-plugin repository](https://github.com/angular-architects/module-federation-plugin/tree/main/libs/native-federation-core).
|
|
@@ -35,7 +35,7 @@ For this, the mental model introduces several concepts:
|
|
|
35
35
|
|
|
36
36
|
- **Remote:** The remote is a separately built and deployed application. It can **expose EcmaScript** modules that can be loaded into other applications.
|
|
37
37
|
- **Host:** The host loads one or several remotes on demand. For your framework's perspective, this looks like traditional lazy loading. The big difference is that the host doesn't know the remotes at compilation time.
|
|
38
|
-
- **Shared Dependencies:** If
|
|
38
|
+
- **Shared Dependencies:** If several remotes and the host use the same library, you might not want to download it several times. Instead, you might want to just download it once and share it at runtime. For this use case, the mental model allows for defining such shared dependencies.
|
|
39
39
|
- **Version Mismatch:** If two or more applications use a different version of the same shared library, we need to prevent a version mismatch. To deal with it, the mental model defines several strategies, like falling back to another version that fits the application, using a different compatible one (according to semantic versioning) or throwing an error.
|
|
40
40
|
|
|
41
41
|
## Example
|
|
@@ -52,8 +52,8 @@ For this, the mental model introduces several concepts:
|
|
|
52
52
|
Big thanks to:
|
|
53
53
|
|
|
54
54
|
- [Zack Jackson](https://twitter.com/ScriptedAlchemy) for originally coming up with the great idea of Module Federation and its successful mental model
|
|
55
|
-
- [Florian Rappl](https://twitter.com/FlorianRappl) for
|
|
56
|
-
- [Michael Egger-Zikes](https://twitter.com/MikeZks) for contributing to our Module Federation efforts and
|
|
55
|
+
- [Florian Rappl](https://twitter.com/FlorianRappl) for a good discussion about these topics during a speakers dinner in Nuremberg
|
|
56
|
+
- [Michael Egger-Zikes](https://twitter.com/MikeZks) for contributing to our Module Federation efforts and bringing in valuable feedback
|
|
57
57
|
- The Angular CLI-Team, esp. [Alan Agius](https://twitter.com/AlanAgius4) and [Charles Lyding](https://twitter.com/charleslyding), for working on the experimental esbuild builder for Angular
|
|
58
58
|
|
|
59
59
|
## Using this Library
|
|
@@ -143,9 +143,78 @@ The method `federationBuilder.build` bundles the shared and exposed parts of you
|
|
|
143
143
|
|
|
144
144
|
### Configuring Hosts
|
|
145
145
|
|
|
146
|
-
The `withNativeFederation` function sets up a configuration for your applications. This is an example configuration for a host
|
|
146
|
+
The `withNativeFederation` function sets up a configuration for your applications. This is an example configuration for a host.
|
|
147
147
|
|
|
148
|
-
The `
|
|
148
|
+
#### The `fromPackageJson` helper (recommended)
|
|
149
|
+
|
|
150
|
+
`fromPackageJson` is the recommended way to share your dependencies. It shares **all** dependencies found in your `package.json` and exposes a small fluent builder so you can fine-tune the result. The base options you pass are applied to every shared dependency; you then chain `.skip(...)`, `.override(...)` and `.patch(...)` as needed and finish with `.get()`:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// shell/federation.config.js
|
|
154
|
+
|
|
155
|
+
import { withNativeFederation, fromPackageJson } from '@softarc/native-federation/config';
|
|
156
|
+
|
|
157
|
+
export default withNativeFederation({
|
|
158
|
+
name: 'host',
|
|
159
|
+
|
|
160
|
+
shared: fromPackageJson({
|
|
161
|
+
singleton: true,
|
|
162
|
+
strictVersion: true,
|
|
163
|
+
requiredVersion: 'auto',
|
|
164
|
+
includeSecondaries: false,
|
|
165
|
+
}).get(),
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> [!TIP]
|
|
170
|
+
> If you omit the `shared` property entirely, Native Federation applies exactly this `fromPackageJson` configuration for you (with `singleton`, `strictVersion` and `requiredVersion: 'auto'`). So the snippet above is also a good description of the default behavior.
|
|
171
|
+
|
|
172
|
+
The builder returned by `fromPackageJson` offers three chainable methods, each of which returns the builder so you can combine them:
|
|
173
|
+
|
|
174
|
+
- **`.skip(externals)`** — exclude packages from sharing (added on top of the [default skip list](#sharing)).
|
|
175
|
+
- **`.override(externals)`** — replace the configuration for specific packages entirely. Use this when a package needs a completely different set of options.
|
|
176
|
+
- **`.patch(externals, cfg)`** — merge a partial configuration onto specific shared externals, keeping the base options for everything you don't touch.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// shell/federation.config.js
|
|
180
|
+
|
|
181
|
+
import { withNativeFederation, fromPackageJson } from '@softarc/native-federation/config';
|
|
182
|
+
|
|
183
|
+
export default withNativeFederation({
|
|
184
|
+
name: 'host',
|
|
185
|
+
|
|
186
|
+
shared: fromPackageJson({
|
|
187
|
+
singleton: true,
|
|
188
|
+
strictVersion: true,
|
|
189
|
+
requiredVersion: 'auto',
|
|
190
|
+
})
|
|
191
|
+
// Don't share these dependencies at all
|
|
192
|
+
.skip(['my-lib', 'some-dev-only-lib'])
|
|
193
|
+
// Give a package a completely different configuration
|
|
194
|
+
.override({
|
|
195
|
+
'package-a/themes/xyz': {
|
|
196
|
+
singleton: true,
|
|
197
|
+
strictVersion: true,
|
|
198
|
+
requiredVersion: 'auto',
|
|
199
|
+
includeSecondaries: { skip: '@package-a/themes/xyz/*' },
|
|
200
|
+
build: 'package',
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
// Tweak a few options on specific packages while keeping the base config
|
|
204
|
+
.patch(['package-b'], {
|
|
205
|
+
singleton: false,
|
|
206
|
+
includeSecondaries: { skip: 'package-b/icons/*' },
|
|
207
|
+
build: 'package',
|
|
208
|
+
})
|
|
209
|
+
.get(),
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
By default the closest `package.json` (relative to your `federation.config.js`) is used. You can point at a different one by passing its path as the second argument: `fromPackageJson(baseCfg, projectPath)`.
|
|
214
|
+
|
|
215
|
+
#### Alternative: the `shareAll` helper
|
|
216
|
+
|
|
217
|
+
`shareAll` is the older, object-spread style alternative to `fromPackageJson`. It also shares all dependencies defined in your `package.json`, but instead of a fluent builder it returns a plain object that you spread into `shared`:
|
|
149
218
|
|
|
150
219
|
```typescript
|
|
151
220
|
// shell/federation.config.js
|
|
@@ -166,11 +235,9 @@ export default withNativeFederation({
|
|
|
166
235
|
});
|
|
167
236
|
```
|
|
168
237
|
|
|
169
|
-
The options passed to shareAll are applied to all dependencies found in your `package.json`.
|
|
238
|
+
The options passed to `shareAll` are applied to all dependencies found in your `package.json`. This might come in handy in a monorepo scenario and when doing some experiments / troubleshooting.
|
|
170
239
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
> Since v21.1 it's also possible to add overrides to the shareAll for specific packages.
|
|
240
|
+
You can also add overrides to `shareAll` for specific packages, via the second argument:
|
|
174
241
|
|
|
175
242
|
```typescript
|
|
176
243
|
// shell/federation.config.js
|
|
@@ -226,17 +293,17 @@ shared: share({
|
|
|
226
293
|
})
|
|
227
294
|
```
|
|
228
295
|
|
|
229
|
-
The added options are `
|
|
296
|
+
The added options are `requiredVersion: 'auto'` and `includeSecondaries`.
|
|
230
297
|
|
|
231
|
-
####
|
|
298
|
+
#### requiredVersion: 'auto'
|
|
232
299
|
|
|
233
|
-
If you set `
|
|
300
|
+
If you set `requiredVersion` to `'auto'`, the helper takes the version defined in your `package.json`.
|
|
234
301
|
|
|
235
|
-
This helps to solve issues with not (fully) met peer dependencies and secondary entry points
|
|
302
|
+
This helps to solve issues with not (fully) met peer dependencies and secondary entry points.
|
|
236
303
|
|
|
237
|
-
By default, it takes the `package.json` that is closest to the caller (normally the `
|
|
304
|
+
By default, it takes the `package.json` that is closest to the caller (normally the `federation.config.js`). However, you can pass the path to another `package.json` using the second optional parameter. Also, you need to define the shared library within the dependencies in your `package.json`.
|
|
238
305
|
|
|
239
|
-
Instead of setting
|
|
306
|
+
Instead of setting `requiredVersion` to `auto` time and again, you can also skip this option and call `setInferVersion(true)` before:
|
|
240
307
|
|
|
241
308
|
```typescript
|
|
242
309
|
setInferVersion(true);
|
|
@@ -268,7 +335,7 @@ shared: share({
|
|
|
268
335
|
})
|
|
269
336
|
```
|
|
270
337
|
|
|
271
|
-
|
|
338
|
+
Wildcard (`*`) export entry points are not expanded by default. To resolve them into concrete secondary entry points, enable the `resolveGlob` property:
|
|
272
339
|
|
|
273
340
|
```typescript
|
|
274
341
|
shared: share({
|
|
@@ -282,7 +349,7 @@ shared: share({
|
|
|
282
349
|
})
|
|
283
350
|
```
|
|
284
351
|
|
|
285
|
-
|
|
352
|
+
Resolving globs can create a bundle for every valid exported file it finds, **so it is recommended to keep the `ignoreUnusedDeps` feature enabled** (it is on by default) to drop the ones you don't use. If you want to specifically skip certain parts of the glob export, you can also use the wildcard in the skip section:
|
|
286
353
|
|
|
287
354
|
```typescript
|
|
288
355
|
shared: share({
|
|
@@ -296,7 +363,7 @@ shared: share({
|
|
|
296
363
|
})
|
|
297
364
|
```
|
|
298
365
|
|
|
299
|
-
Finally, it's also possible to break out of the
|
|
366
|
+
Finally, it's also possible to break out of the `ignoreUnusedDeps` feature for specific externals if desired, for example when sharing a whole suite of interconnected external dependencies like @angular/core. This can be handy when you want to avoid the chance of cross-version secondary entrypoints being used by the different micro frontends. E.g. mfe1 uses @angular/core v20.1.0 and mfe2 uses @angular/core/rxjs-interop v20.0.8, then you might want consistent use of v20.1.0 so rxjs-interop should be exported by mfe1. The `keepAll` prop allows you to enforce this:
|
|
300
367
|
|
|
301
368
|
```typescript
|
|
302
369
|
shared: share({
|
|
@@ -310,14 +377,13 @@ shared: share({
|
|
|
310
377
|
})
|
|
311
378
|
```
|
|
312
379
|
|
|
313
|
-
The API for configuring and using Native Federation is very similar to the one provided by our Module Federation plugin [@angular-architects/module-federation](https://www.npmjs.com/package/@angular-architects/
|
|
380
|
+
The API for configuring and using Native Federation is very similar to the one provided by our Module Federation plugin [@angular-architects/module-federation](https://www.npmjs.com/package/@angular-architects/module-federation). Hence, most of the articles on it are also valid for Native Federation.
|
|
314
381
|
|
|
315
382
|
### Sharing
|
|
316
383
|
|
|
317
384
|
The `shareAll`-helper used here shares all dependencies found in your `package.json`. Hence, they only need to be loaded once (instead of once per remote and host). If you don't want to share all of them, you can opt-out of sharing by using the `skip` option:
|
|
318
385
|
|
|
319
386
|
```typescript
|
|
320
|
-
n
|
|
321
387
|
export default withNativeFederation({
|
|
322
388
|
[...]
|
|
323
389
|
|
|
@@ -332,7 +398,7 @@ export default withNativeFederation({
|
|
|
332
398
|
|
|
333
399
|
### Sharing Mapped Paths (Monorepo-internal Libraries)
|
|
334
400
|
|
|
335
|
-
Paths mapped in your `tsconfig.json` are shared by default too. While they are part of your (mono) repository, they are
|
|
401
|
+
Paths mapped in your `tsconfig.json` are shared by default too. While they are part of your (mono) repository, they are treated like libraries:
|
|
336
402
|
|
|
337
403
|
```json
|
|
338
404
|
{
|
|
@@ -349,7 +415,7 @@ Paths mapped in your `tsconfig.json` are shared by default too. While they are p
|
|
|
349
415
|
|
|
350
416
|
If you don't want to share (all of) them, put their names into the skip array (see above).
|
|
351
417
|
|
|
352
|
-
###
|
|
418
|
+
### Determining which internal libraries are shared
|
|
353
419
|
|
|
354
420
|
In Nx/monorepo setups, Native Federation shares all libraries from your `tsconfig` path mappings by default.
|
|
355
421
|
|
|
@@ -376,7 +442,9 @@ Notes:
|
|
|
376
442
|
- Mapped paths are read from the workspace root tsconfig file: `tsconfig.base.json` if present, otherwise `tsconfig.json`.
|
|
377
443
|
- The workspace root is detected by searching upward from the current working directory until a `package.json` is found.
|
|
378
444
|
|
|
379
|
-
|
|
445
|
+
The `mappingVersion` feature flag controls whether mapped paths get a version. It is **enabled by default**: Native Federation reads the version from the mapped library's nearest `package.json` and shares it with strict versioning, just like a published library.
|
|
446
|
+
|
|
447
|
+
If your mapped paths point at plain internal source that isn't distributed as a versioned, buildable library, you can disable it. The mapped paths are then shared without a version constraint.
|
|
380
448
|
|
|
381
449
|
```js
|
|
382
450
|
module.exports = withNativeFederation({
|
|
@@ -394,11 +462,9 @@ module.exports = withNativeFederation({
|
|
|
394
462
|
});
|
|
395
463
|
```
|
|
396
464
|
|
|
397
|
-
If enabled, Native Federation tries to read the version from the mapped library's nearest `package.json`. By default this feature is set to `false`.
|
|
398
|
-
|
|
399
465
|
### Code-Splitting for Shared Dependencies
|
|
400
466
|
|
|
401
|
-
By default, Native Federation enables code-splitting (chunking) for shared dependencies. This means large libraries can be split into smaller chunks which reduces the
|
|
467
|
+
By default, Native Federation enables code-splitting (chunking) for shared dependencies. This means large libraries can be split into smaller chunks which reduces the overall size, improving initial load times.
|
|
402
468
|
|
|
403
469
|
You can configure code-splitting at two levels:
|
|
404
470
|
|
|
@@ -436,18 +502,19 @@ module.exports = withNativeFederation({
|
|
|
436
502
|
strictVersion: true,
|
|
437
503
|
requiredVersion: 'auto',
|
|
438
504
|
},
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
505
|
+
{
|
|
506
|
+
overrides: {
|
|
507
|
+
// Disable code-splitting for a specific package
|
|
508
|
+
'large-lib': {
|
|
509
|
+
singleton: true,
|
|
510
|
+
strictVersion: true,
|
|
511
|
+
requiredVersion: 'auto',
|
|
512
|
+
chunks: false,
|
|
513
|
+
build: 'package', // necessary for isolated bundles
|
|
514
|
+
},
|
|
446
515
|
},
|
|
447
516
|
}
|
|
448
517
|
),
|
|
449
|
-
// Disable code-splitting for a specific package
|
|
450
|
-
|
|
451
518
|
},
|
|
452
519
|
});
|
|
453
520
|
```
|
|
@@ -475,6 +542,27 @@ module.exports = withNativeFederation({
|
|
|
475
542
|
|
|
476
543
|
When enabled, instead of listing each chunk as a separate shared dependency, chunks are grouped by bundle name in a dedicated `chunks` object. Each shared dependency gets a `bundle` property linking it to its chunk bundle. This results in a smaller `remoteEntry.json` and allows chunks to be skipped if the dependency is not used in the final import map.
|
|
477
544
|
|
|
545
|
+
#### Dense Externals
|
|
546
|
+
|
|
547
|
+
The `denseExternals` feature flag reshapes the `shared` array in `remoteEntry.json` so that all entrypoints of a shared external (its primary import plus every secondary and shared mapping) are grouped under a single object:
|
|
548
|
+
|
|
549
|
+
```js
|
|
550
|
+
module.exports = withNativeFederation({
|
|
551
|
+
shared: {
|
|
552
|
+
...shareAll({
|
|
553
|
+
singleton: true,
|
|
554
|
+
strictVersion: true,
|
|
555
|
+
requiredVersion: 'auto',
|
|
556
|
+
}),
|
|
557
|
+
},
|
|
558
|
+
features: {
|
|
559
|
+
denseExternals: true,
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
When enabled, instead of one flat entry per entrypoint, each package becomes one object whose `entries` map keys the full import name to its output file (e.g. `{ "@angular/common": "...", "@angular/common/http": "..." }`). Entrypoints whose sharing metadata (`singleton`, `strictVersion`, `requiredVersion`, `version`, `shareScope`) diverges are split into separate groups. Bundler chunks stay flat, and `importmap.json` is unaffected. The flag is opt-in and fully backward compatible: the runtime auto-detects each entry by shape, so old and new `remoteEntry.json` both load.
|
|
565
|
+
|
|
478
566
|
### Configuring Remotes
|
|
479
567
|
|
|
480
568
|
When configuring a remote, you can expose files that can be loaded into the shell at runtime:
|
|
@@ -597,11 +685,11 @@ const { loadRemoteModule } = await initFederation(manifest, {
|
|
|
597
685
|
|
|
598
686
|
## React and Other CommonJS Libs
|
|
599
687
|
|
|
600
|
-
Native Federation uses Web Standards like EcmaScript Modules. Most libs and frameworks support them meanwhile. Unfortunately, React still uses CommonJS (
|
|
688
|
+
Native Federation uses Web Standards like EcmaScript Modules. Most libs and frameworks support them meanwhile. Unfortunately, React still uses CommonJS (and UMD). We do our best to convert these libs to EcmaScript Modules. In the case of React there are some challenges due to the dynamic way the React bundles use the `exports` object.
|
|
601
689
|
|
|
602
|
-
As the community is moving to
|
|
690
|
+
As the community is moving to EcmaScript Modules, we expect that these issues will vanish over time. In between, we provide some solutions for dealing with CommonJS-based libraries using `exports` in a dynamic way.
|
|
603
691
|
|
|
604
|
-
One of them is `
|
|
692
|
+
One of them is `fileReplacements`:
|
|
605
693
|
|
|
606
694
|
```javascript
|
|
607
695
|
import { reactReplacements } from '@softarc/native-federation-esbuild/src/lib/react-replacements';
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from './lib/domain/config/index.js';
|
|
2
2
|
export { withNativeFederation } from './lib/config/with-native-federation.js';
|
|
3
|
-
export { findRootTsConfigJson
|
|
3
|
+
export { findRootTsConfigJson } from './lib/config/project-paths.js';
|
|
4
|
+
export { setInferVersion } from './lib/config/version-lookup.js';
|
|
5
|
+
export { share, shareAll, fromPackageJson } from './lib/config/share-utils.js';
|
|
4
6
|
export { DEFAULT_SKIP_LIST } from './lib/config/default-skip-list.js';
|
package/dist/config.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
export * from "./lib/domain/config/index.js";
|
|
2
2
|
import { withNativeFederation } from "./lib/config/with-native-federation.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
shareAll,
|
|
7
|
-
setInferVersion
|
|
8
|
-
} from "./lib/config/share-utils.js";
|
|
3
|
+
import { findRootTsConfigJson } from "./lib/config/project-paths.js";
|
|
4
|
+
import { setInferVersion } from "./lib/config/version-lookup.js";
|
|
5
|
+
import { share, shareAll, fromPackageJson } from "./lib/config/share-utils.js";
|
|
9
6
|
import { DEFAULT_SKIP_LIST } from "./lib/config/default-skip-list.js";
|
|
10
7
|
export {
|
|
11
8
|
DEFAULT_SKIP_LIST,
|
|
12
9
|
findRootTsConfigJson,
|
|
10
|
+
fromPackageJson,
|
|
13
11
|
setInferVersion,
|
|
14
12
|
share,
|
|
15
13
|
shareAll,
|
package/dist/internal.d.ts
CHANGED
|
@@ -10,5 +10,6 @@ export type { NormalizedFederationConfig } from './lib/domain/config/federation-
|
|
|
10
10
|
export { getDefaultCachePath, getChecksum } from './lib/core/cache/cache-persistence.js';
|
|
11
11
|
export { isESMExport, type ExportCondition, type ExportEntry, } from './lib/utils/package/package-info.js';
|
|
12
12
|
export { isInSkipList, prepareSkipList } from './lib/config/default-skip-list.js';
|
|
13
|
-
export
|
|
13
|
+
export { densifyExternals } from './lib/core/output/densify-externals.js';
|
|
14
|
+
export type { NfFileWatcher, NfFileWatcherOptions, } from './lib/domain/utils/file-watcher.contract.js';
|
|
14
15
|
export { syncNfFileWatcher, createNfWatcher } from './lib/utils/file-watcher.js';
|
package/dist/internal.js
CHANGED
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
isESMExport
|
|
10
10
|
} from "./lib/utils/package/package-info.js";
|
|
11
11
|
import { isInSkipList, prepareSkipList } from "./lib/config/default-skip-list.js";
|
|
12
|
+
import { densifyExternals } from "./lib/core/output/densify-externals.js";
|
|
12
13
|
import { syncNfFileWatcher, createNfWatcher } from "./lib/utils/file-watcher.js";
|
|
13
14
|
export {
|
|
14
15
|
RebuildQueue,
|
|
15
16
|
createNfWatcher,
|
|
17
|
+
densifyExternals,
|
|
16
18
|
getChecksum,
|
|
17
19
|
getDefaultCachePath,
|
|
18
20
|
hashFile,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FileReaderPort } from '../domain/utils/io-port.contract.js';
|
|
2
|
+
export declare function findRootTsConfigJson(): string;
|
|
3
|
+
export declare function findRootTsConfigJsonCore(io: FileReaderPort): string;
|
|
4
|
+
export declare function findPackageJson(io: FileReaderPort, folder: string): string;
|
|
5
|
+
export declare function inferProjectPath(projectPath: string | undefined): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { cwd } from "process";
|
|
3
|
+
import { getConfigContext } from "./configuration-context.js";
|
|
4
|
+
import { nodeIo } from "../utils/io/node-io-adapter.js";
|
|
5
|
+
function findRootTsConfigJson() {
|
|
6
|
+
return findRootTsConfigJsonCore(nodeIo);
|
|
7
|
+
}
|
|
8
|
+
function findRootTsConfigJsonCore(io) {
|
|
9
|
+
const packageJson = findPackageJson(io, cwd());
|
|
10
|
+
const projectRoot = path.dirname(packageJson);
|
|
11
|
+
const tsConfigBaseJson = path.join(projectRoot, "tsconfig.base.json");
|
|
12
|
+
const tsConfigJson = path.join(projectRoot, "tsconfig.json");
|
|
13
|
+
if (io.exists(tsConfigBaseJson)) {
|
|
14
|
+
return tsConfigBaseJson;
|
|
15
|
+
} else if (io.exists(tsConfigJson)) {
|
|
16
|
+
return tsConfigJson;
|
|
17
|
+
}
|
|
18
|
+
throw new Error("Neither a tsconfig.json nor a tsconfig.base.json was found");
|
|
19
|
+
}
|
|
20
|
+
function findPackageJson(io, folder) {
|
|
21
|
+
while (!io.exists(path.join(folder, "package.json")) && path.dirname(folder) !== folder) {
|
|
22
|
+
folder = path.dirname(folder);
|
|
23
|
+
}
|
|
24
|
+
const filePath = path.join(folder, "package.json");
|
|
25
|
+
if (io.exists(filePath)) {
|
|
26
|
+
return filePath;
|
|
27
|
+
}
|
|
28
|
+
throw new Error(
|
|
29
|
+
"no package.json found. Searched the following folder and all parents: " + folder
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
function inferProjectPath(projectPath) {
|
|
33
|
+
if (!projectPath && getConfigContext().packageJson) {
|
|
34
|
+
projectPath = path.dirname(getConfigContext().packageJson || "");
|
|
35
|
+
}
|
|
36
|
+
if (!projectPath && getConfigContext().workspaceRoot) {
|
|
37
|
+
projectPath = getConfigContext().workspaceRoot || "";
|
|
38
|
+
}
|
|
39
|
+
if (!projectPath) {
|
|
40
|
+
projectPath = cwd();
|
|
41
|
+
}
|
|
42
|
+
return projectPath;
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
findPackageJson,
|
|
46
|
+
findRootTsConfigJson,
|
|
47
|
+
findRootTsConfigJsonCore,
|
|
48
|
+
inferProjectPath
|
|
49
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type PreparedSkipList } from '../domain/config/skip-list.contract.js';
|
|
2
|
+
import type { FileReaderPort, GlobPort } from '../domain/utils/io-port.contract.js';
|
|
3
|
+
import type { ExternalConfig, IncludeSecondariesOptions, SharedExternalsConfig } from '../domain/config/external-config.contract.js';
|
|
4
|
+
export declare function getSecondaries(io: FileReaderPort & GlobPort, includeSecondaries: IncludeSecondariesOptions, libPath: string, key: string, shareObject: ExternalConfig, preparedSkipList: PreparedSkipList): SharedExternalsConfig | null;
|
|
5
|
+
export declare function addSecondaries(secondaries: Record<string, ExternalConfig>, result: Record<string, ExternalConfig>): void;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { isInSkipList } from "./default-skip-list.js";
|
|
3
|
+
import { resolvePackageJsonExportsWildcardCore } from "../utils/package/resolve-wildcard-keys.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
function _findSecondaries(io, libPath, excludes, shareObject, acc, preparedSkipList) {
|
|
6
|
+
const files = io.readDir(libPath);
|
|
7
|
+
const secondaries = files.map((f) => path.join(libPath, f)).filter((f) => io.isDirectory(f) && !f.endsWith("node_modules"));
|
|
8
|
+
for (const s of secondaries) {
|
|
9
|
+
if (io.exists(path.join(s, "package.json"))) {
|
|
10
|
+
const secondaryLibName = s.replace(/\\/g, "/").replace(/^.*node_modules[/]/, "");
|
|
11
|
+
const inCustomSkipList = excludes.some(
|
|
12
|
+
(e) => e === secondaryLibName || e.endsWith("*") && secondaryLibName.startsWith(e.slice(0, -1))
|
|
13
|
+
);
|
|
14
|
+
if (inCustomSkipList) continue;
|
|
15
|
+
if (isInSkipList(secondaryLibName, preparedSkipList)) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
acc[secondaryLibName] = { ...shareObject };
|
|
19
|
+
}
|
|
20
|
+
_findSecondaries(io, s, excludes, shareObject, acc, preparedSkipList);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function findSecondaries(io, libPath, excludes, shareObject, preparedSkipList) {
|
|
24
|
+
const acc = {};
|
|
25
|
+
_findSecondaries(io, libPath, excludes, shareObject, acc, preparedSkipList);
|
|
26
|
+
return acc;
|
|
27
|
+
}
|
|
28
|
+
function getSecondaries(io, includeSecondaries, libPath, key, shareObject, preparedSkipList) {
|
|
29
|
+
let exclude = [];
|
|
30
|
+
let resolveGlob = false;
|
|
31
|
+
if (typeof includeSecondaries === "object") {
|
|
32
|
+
if (includeSecondaries.skip) {
|
|
33
|
+
if (Array.isArray(includeSecondaries.skip)) {
|
|
34
|
+
exclude = includeSecondaries.skip;
|
|
35
|
+
} else if (typeof includeSecondaries.skip === "string") {
|
|
36
|
+
exclude = [includeSecondaries.skip];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
resolveGlob = !!includeSecondaries.resolveGlob;
|
|
40
|
+
}
|
|
41
|
+
if (!io.exists(libPath)) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
const configured = readConfiguredSecondaries(
|
|
45
|
+
io,
|
|
46
|
+
key,
|
|
47
|
+
libPath,
|
|
48
|
+
exclude,
|
|
49
|
+
shareObject,
|
|
50
|
+
preparedSkipList,
|
|
51
|
+
resolveGlob
|
|
52
|
+
);
|
|
53
|
+
if (configured) {
|
|
54
|
+
return configured;
|
|
55
|
+
}
|
|
56
|
+
const secondaries = findSecondaries(io, libPath, exclude, shareObject, preparedSkipList);
|
|
57
|
+
return secondaries;
|
|
58
|
+
}
|
|
59
|
+
function readConfiguredSecondaries(io, parent, libPath, exclude, shareObject, preparedSkipList, resolveGlob) {
|
|
60
|
+
const libPackageJson = path.join(libPath, "package.json");
|
|
61
|
+
if (!io.exists(libPackageJson)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const packageJson = JSON.parse(io.readText(libPackageJson));
|
|
65
|
+
const version = packageJson["version"];
|
|
66
|
+
const esm = packageJson["type"] === "module";
|
|
67
|
+
const exports = packageJson["exports"];
|
|
68
|
+
if (!exports) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const keys = Object.keys(exports).filter(
|
|
72
|
+
(key) => key !== "." && key !== "./package.json" && key.startsWith("./") && (exports[key]?.["default"] || exports[key]?.["import"] || typeof exports[key] === "string")
|
|
73
|
+
);
|
|
74
|
+
const result = {};
|
|
75
|
+
const discoveredFiles = /* @__PURE__ */ new Set();
|
|
76
|
+
for (const key of keys) {
|
|
77
|
+
const secondaryName = path.join(parent, key).replace(/\\/g, "/");
|
|
78
|
+
const inCustomSkipList = exclude.some(
|
|
79
|
+
(e) => e === secondaryName || e.endsWith("*") && secondaryName.startsWith(e.slice(0, -1))
|
|
80
|
+
);
|
|
81
|
+
if (inCustomSkipList) continue;
|
|
82
|
+
if (isInSkipList(secondaryName, preparedSkipList)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const entry = getDefaultEntry(exports, key);
|
|
86
|
+
if (typeof entry !== "string") {
|
|
87
|
+
logger.warn("No entry point found for " + secondaryName);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!key.includes("*") && !isJsFile(entry)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const items = resolveGlobSecondaries(
|
|
94
|
+
io,
|
|
95
|
+
key,
|
|
96
|
+
libPath,
|
|
97
|
+
parent,
|
|
98
|
+
secondaryName,
|
|
99
|
+
entry,
|
|
100
|
+
{ discovered: discoveredFiles, skip: exclude },
|
|
101
|
+
resolveGlob
|
|
102
|
+
);
|
|
103
|
+
items.forEach((e) => discoveredFiles.add(typeof e === "string" ? e : e.value));
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
if (typeof item === "object") {
|
|
106
|
+
result[item.key] = {
|
|
107
|
+
...shareObject,
|
|
108
|
+
packageInfo: {
|
|
109
|
+
entryPoint: item.value,
|
|
110
|
+
version: shareObject.version ?? version,
|
|
111
|
+
esm
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
} else {
|
|
115
|
+
result[item] = {
|
|
116
|
+
...shareObject
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function resolveGlobSecondaries(io, key, libPath, parent, secondaryName, entry, excludes, resolveGlob) {
|
|
124
|
+
let items = [];
|
|
125
|
+
if (key.includes("*")) {
|
|
126
|
+
if (!resolveGlob) return items;
|
|
127
|
+
const expanded = resolvePackageJsonExportsWildcardCore(io, key, entry, libPath);
|
|
128
|
+
items = expanded.map((e) => ({
|
|
129
|
+
key: path.join(parent, e.key),
|
|
130
|
+
value: path.join(libPath, e.value)
|
|
131
|
+
})).filter((i) => {
|
|
132
|
+
if (!isJsFile(i.value)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (excludes.skip.some(
|
|
136
|
+
(e) => e.endsWith("*") ? i.key.startsWith(e.slice(0, -1)) : e === i.key
|
|
137
|
+
)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (excludes.discovered.has(i.value)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
items = [secondaryName];
|
|
147
|
+
}
|
|
148
|
+
return items;
|
|
149
|
+
}
|
|
150
|
+
function isJsFile(file) {
|
|
151
|
+
return file.endsWith(".js") || file.endsWith(".mjs") || file.endsWith(".cjs");
|
|
152
|
+
}
|
|
153
|
+
function getDefaultEntry(exports, key) {
|
|
154
|
+
let entry = "";
|
|
155
|
+
if (typeof exports[key] === "string") {
|
|
156
|
+
entry = exports[key];
|
|
157
|
+
}
|
|
158
|
+
if (!entry) {
|
|
159
|
+
entry = exports[key]?.["default"];
|
|
160
|
+
if (typeof entry === "object") {
|
|
161
|
+
entry = entry["default"];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!entry) {
|
|
165
|
+
entry = exports[key]?.["import"];
|
|
166
|
+
if (typeof entry === "object") {
|
|
167
|
+
entry = entry["import"] ?? entry["default"];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!entry) {
|
|
171
|
+
entry = exports[key]?.["require"];
|
|
172
|
+
if (typeof entry === "object") {
|
|
173
|
+
entry = entry["require"] ?? entry["default"];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return entry;
|
|
177
|
+
}
|
|
178
|
+
function addSecondaries(secondaries, result) {
|
|
179
|
+
for (const key in secondaries) {
|
|
180
|
+
result[key] = secondaries[key];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export {
|
|
184
|
+
addSecondaries,
|
|
185
|
+
getSecondaries
|
|
186
|
+
};
|
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import { type SkipList
|
|
1
|
+
import { type SkipList } from '../domain/config/skip-list.contract.js';
|
|
2
2
|
import type { PackageJsonRepository } from '../domain/utils/package-json.contract.js';
|
|
3
3
|
import type { FileReaderPort, GlobPort } from '../domain/utils/io-port.contract.js';
|
|
4
|
-
import type { ExternalConfig,
|
|
5
|
-
export declare
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import type { ExternalConfig, ResolvedSharedExternalsConfig, ShareAllExternalsOptions, ShareExternalsOptions } from '../domain/config/external-config.contract.js';
|
|
5
|
+
export declare const fromPackageJson: (baseCfg: ShareAllExternalsOptions, projectPath?: string) => {
|
|
6
|
+
skip(externals: SkipList): /*elided*/ any;
|
|
7
|
+
override(externals: ShareExternalsOptions): /*elided*/ any;
|
|
8
|
+
patch(externals: string[], cfg: Partial<ExternalConfig>): /*elided*/ any;
|
|
9
|
+
get(): ResolvedSharedExternalsConfig;
|
|
10
|
+
};
|
|
8
11
|
export declare function shareAll(config: ShareAllExternalsOptions, opts?: {
|
|
9
12
|
skipList?: SkipList;
|
|
10
13
|
projectPath?: string;
|
|
11
14
|
overrides?: ShareExternalsOptions;
|
|
12
|
-
}):
|
|
15
|
+
}): ResolvedSharedExternalsConfig;
|
|
13
16
|
export declare function shareAllCore(io: FileReaderPort & GlobPort, config: ShareAllExternalsOptions, opts?: {
|
|
14
17
|
skipList?: SkipList;
|
|
15
18
|
projectPath?: string;
|
|
16
19
|
overrides?: ShareExternalsOptions;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export declare function share(configuredShareObjects: ShareExternalsOptions, projectPath?: string, skipList?: SkipList):
|
|
20
|
-
export declare function shareCore(io: FileReaderPort & GlobPort, configuredShareObjects: ShareExternalsOptions, projectPath?: string, skipList?: SkipList, repo?: PackageJsonRepository):
|
|
20
|
+
patchList?: Record<string, Partial<ExternalConfig>>;
|
|
21
|
+
}, repo?: PackageJsonRepository): ResolvedSharedExternalsConfig;
|
|
22
|
+
export declare function share(configuredShareObjects: ShareExternalsOptions, projectPath?: string, skipList?: SkipList): ResolvedSharedExternalsConfig;
|
|
23
|
+
export declare function shareCore(io: FileReaderPort & GlobPort, configuredShareObjects: ShareExternalsOptions, projectPath?: string, skipList?: SkipList, repo?: PackageJsonRepository): ResolvedSharedExternalsConfig;
|