@softarc/native-federation 4.0.0-RC8 → 4.0.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.
Files changed (53) hide show
  1. package/package.json +2 -2
  2. package/src/index.d.ts +1 -2
  3. package/src/index.js +1 -2
  4. package/src/internal.d.ts +4 -1
  5. package/src/internal.js +2 -0
  6. package/src/lib/config/default-skip-list.js +3 -15
  7. package/src/lib/config/remove-unused-deps.d.ts +3 -0
  8. package/src/lib/config/remove-unused-deps.js +10 -0
  9. package/src/lib/config/share-utils.d.ts +0 -1
  10. package/src/lib/config/share-utils.js +7 -13
  11. package/src/lib/config/with-native-federation.js +40 -61
  12. package/src/lib/core/build-for-federation.js +21 -22
  13. package/src/lib/core/bundle-exposed-and-mappings.js +15 -16
  14. package/src/lib/core/bundle-shared.d.ts +1 -0
  15. package/src/lib/core/bundle-shared.js +8 -7
  16. package/src/lib/core/federation-builder.d.ts +6 -1
  17. package/src/lib/core/federation-builder.js +18 -8
  18. package/src/lib/core/get-externals.js +1 -1
  19. package/src/lib/core/normalize-options.d.ts +12 -0
  20. package/src/lib/core/normalize-options.js +58 -0
  21. package/src/lib/core/rebuild-for-federation.js +2 -2
  22. package/src/lib/domain/config/external-config.contract.d.ts +3 -1
  23. package/src/lib/domain/config/federation-config.contract.d.ts +8 -2
  24. package/src/lib/domain/config/with-native-federation.contract.d.ts +2 -0
  25. package/src/lib/domain/config/with-native-federation.contract.js +1 -0
  26. package/src/lib/domain/core/build-adapter.contract.d.ts +12 -6
  27. package/src/lib/domain/core/federation-options.contract.d.ts +5 -5
  28. package/src/lib/domain/core/index.d.ts +1 -1
  29. package/src/lib/domain/utils/file-watcher.contract.d.ts +10 -0
  30. package/src/lib/domain/utils/file-watcher.contract.js +1 -0
  31. package/src/lib/domain/utils/index.d.ts +1 -1
  32. package/src/lib/domain/utils/mapped-path.contract.d.ts +1 -4
  33. package/src/lib/domain/utils/mapped-path.contract.js +4 -0
  34. package/src/lib/domain/utils/used-dependencies.contract.d.ts +5 -0
  35. package/src/lib/domain/utils/used-dependencies.contract.js +1 -0
  36. package/src/lib/utils/file-watcher.d.ts +5 -0
  37. package/src/lib/utils/file-watcher.js +51 -0
  38. package/src/lib/utils/get-used-dependencies.d.ts +7 -0
  39. package/src/lib/utils/get-used-dependencies.js +123 -0
  40. package/src/lib/utils/mapped-paths.d.ts +7 -7
  41. package/src/lib/utils/mapped-paths.js +14 -12
  42. package/src/lib/utils/resolve-wildcard-keys.d.ts +29 -2
  43. package/src/lib/utils/resolve-wildcard-keys.js +105 -38
  44. package/src/lib/core/default-server-deps-list.d.ts +0 -2
  45. package/src/lib/core/default-server-deps-list.js +0 -6
  46. package/src/lib/core/load-federation-config.d.ts +0 -3
  47. package/src/lib/core/load-federation-config.js +0 -18
  48. package/src/lib/core/normalize-federation-options.d.ts +0 -4
  49. package/src/lib/core/normalize-federation-options.js +0 -10
  50. package/src/lib/core/remove-unused-deps.d.ts +0 -2
  51. package/src/lib/core/remove-unused-deps.js +0 -90
  52. package/src/lib/utils/config-utils.d.ts +0 -2
  53. package/src/lib/utils/config-utils.js +0 -9
@@ -1,45 +1,111 @@
1
1
  import fg from 'fast-glob';
2
- function escapeRegex(str) {
3
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4
- }
5
- // Convert package.json exports pattern to glob pattern
6
- // * in exports means "one segment", but for glob we need **/* for deep matching
7
- // Src: https://hirok.io/posts/package-json-exports#exposing-all-package-files
8
- function convertExportsToGlob(pattern) {
9
- return pattern.replace(/(?<!\*)\*(?!\*)/g, '**/*');
10
- }
11
- function compilePattern(pattern) {
12
- const tokens = pattern.split(/(\*\*|\*)/);
13
- const regexParts = [];
14
- for (const token of tokens) {
15
- if (token === '*') {
16
- regexParts.push('(.*)');
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { logger } from './logger.js';
5
+ // TypeScript's module resolution for directories checks these in order
6
+ // @see https://www.typescriptlang.org/docs/handbook/modules/theory.html#module-resolution
7
+ const TS_INDEX_FILES = [
8
+ 'index.ts',
9
+ 'index.tsx',
10
+ 'index.mts',
11
+ 'index.cts',
12
+ 'index.d.ts',
13
+ 'index.js',
14
+ 'index.jsx',
15
+ 'index.mjs',
16
+ 'index.cjs',
17
+ ];
18
+ /**
19
+ * Resolves tsconfig wildcard paths.
20
+ *
21
+ * In tsconfig.json, paths like `@features/*` → `libs/features/src/*` work as follows:
22
+ * - The `*` captures a single path segment (the module name)
23
+ * - When importing `@features/feature-a`, TypeScript captures `feature-a`
24
+ * - It then replaces `*` in the value pattern: `libs/features/src/feature-a`
25
+ *
26
+ * For discovery, we find all directories at the wildcard position that TypeScript
27
+ * would recognize as valid modules (directories with index files or package.json).
28
+ *
29
+ // @see https://www.typescriptlang.org/docs/handbook/modules/theory.html#module-resolution
30
+ */
31
+ export function resolveTsConfigWildcard(keyPattern, valuePattern, cwd) {
32
+ const normalizedPattern = valuePattern.replace(/^\.?\/+/, '');
33
+ const asteriskIndex = normalizedPattern.indexOf('*');
34
+ if (asteriskIndex === -1) {
35
+ return [];
36
+ }
37
+ const prefix = normalizedPattern.substring(0, asteriskIndex);
38
+ const suffix = normalizedPattern.substring(asteriskIndex + 1);
39
+ const searchPath = path.join(cwd, prefix);
40
+ let entries;
41
+ try {
42
+ entries = fs.readdirSync(searchPath);
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ const keys = [];
48
+ for (const entry of entries) {
49
+ const entryPath = path.join(searchPath, entry);
50
+ let stats;
51
+ try {
52
+ stats = fs.statSync(entryPath);
17
53
  }
18
- else {
19
- regexParts.push(escapeRegex(token));
54
+ catch {
55
+ continue;
20
56
  }
21
- }
22
- return new RegExp(`^${regexParts.join('')}$`);
23
- }
24
- function withoutWildcard(template, wildcardValues) {
25
- const tokens = template.split(/(\*\*|\*)/);
26
- let result = '';
27
- let i = 0;
28
- for (const token of tokens) {
29
- if (token === '*') {
30
- result += wildcardValues[i++];
57
+ if (!stats.isDirectory()) {
58
+ // Skipping individual files, we only process modules
59
+ continue;
31
60
  }
32
- else {
33
- result += token;
61
+ let modulePath = path.join(prefix, entry, suffix).replace(/\\/g, '/');
62
+ const fullPath = path.join(cwd, modulePath);
63
+ let fullPathStats;
64
+ try {
65
+ fullPathStats = fs.statSync(fullPath);
34
66
  }
67
+ catch {
68
+ continue;
69
+ }
70
+ const key = keyPattern.replace('*', entry);
71
+ if (fullPathStats.isDirectory()) {
72
+ const indexFile = TS_INDEX_FILES.find(indexFile => fs.existsSync(path.join(fullPath, indexFile)));
73
+ if (!indexFile) {
74
+ logger.warn(`[shared-mappings] Internal lib '${key}' does not contain an entryPoint (barrel file).`);
75
+ continue;
76
+ }
77
+ modulePath = path.join(modulePath, indexFile);
78
+ }
79
+ else if (!fullPathStats.isFile()) {
80
+ continue;
81
+ }
82
+ keys.push({
83
+ key,
84
+ value: modulePath,
85
+ });
35
86
  }
36
- return result;
87
+ return keys;
37
88
  }
38
- export function resolveWildcardKeys(keyPattern, valuePattern, cwd) {
89
+ /**
90
+ * Resolves package.json exports wildcard patterns.
91
+ *
92
+ * In package.json exports, patterns like `./features/*.js` → `./src/features/*.js` work as follows:
93
+ * - The `*` is a literal string replacement that can include path separators
94
+ * - Importing `pkg/features/a/b.js` captures `a/b` and replaces `*` → `./src/features/a/b.js`
95
+ * - This matches actual files, not directories
96
+ *
97
+ * @see https://nodejs.org/api/packages.html#subpath-patterns
98
+ */
99
+ export function resolvePackageJsonExportsWildcard(keyPattern, valuePattern, cwd) {
39
100
  const normalizedPattern = valuePattern.replace(/^\.?\/+/, '');
40
- const globPattern = convertExportsToGlob(normalizedPattern);
41
- const regex = compilePattern(normalizedPattern);
42
- const files = fg.sync(globPattern, {
101
+ const asteriskIndex = normalizedPattern.indexOf('*');
102
+ if (asteriskIndex === -1) {
103
+ return [];
104
+ }
105
+ const prefix = normalizedPattern.substring(0, asteriskIndex);
106
+ const suffix = normalizedPattern.substring(asteriskIndex + 1);
107
+ // fast-glob requires **/* pattern for matching files at any depth
108
+ const files = fg.sync(prefix + '**/*' + suffix, {
43
109
  cwd,
44
110
  onlyFiles: true,
45
111
  deep: Infinity,
@@ -47,11 +113,12 @@ export function resolveWildcardKeys(keyPattern, valuePattern, cwd) {
47
113
  const keys = [];
48
114
  for (const file of files) {
49
115
  const relPath = file.replace(/\\/g, '/').replace(/^\.\//, '');
50
- const wildcards = relPath.match(regex);
51
- if (!wildcards)
52
- continue;
116
+ const captured = suffix
117
+ ? relPath.slice(prefix.length, -suffix.length)
118
+ : relPath.slice(prefix.length);
119
+ const key = keyPattern.replace('*', captured);
53
120
  keys.push({
54
- key: withoutWildcard(keyPattern, wildcards.slice(1)),
121
+ key,
55
122
  value: relPath,
56
123
  });
57
124
  }
@@ -1,2 +0,0 @@
1
- export declare const DEFAULT_SERVER_DEPS_LIST: string[];
2
- export declare const DEFAULT_SERVER_DEPS_SET: Set<string>;
@@ -1,6 +0,0 @@
1
- export const DEFAULT_SERVER_DEPS_LIST = [
2
- '@angular/platform-server',
3
- '@angular/platform-server/init',
4
- '@angular/ssr',
5
- ];
6
- export const DEFAULT_SERVER_DEPS_SET = new Set(DEFAULT_SERVER_DEPS_LIST);
@@ -1,3 +0,0 @@
1
- import type { NormalizedFederationConfig } from '../domain/config/federation-config.contract.js';
2
- import type { FederationOptions } from '../domain/core/federation-options.contract.js';
3
- export declare function loadFederationConfig(fedOptions: FederationOptions): Promise<NormalizedFederationConfig>;
@@ -1,18 +0,0 @@
1
- import * as path from 'path';
2
- import * as fs from 'fs';
3
- import { removeUnusedDeps } from './remove-unused-deps.js';
4
- export async function loadFederationConfig(fedOptions) {
5
- const fullConfigPath = path.join(fedOptions.workspaceRoot, fedOptions.federationConfig);
6
- if (!fs.existsSync(fullConfigPath)) {
7
- throw new Error('Expected ' + fullConfigPath);
8
- }
9
- const config = (await import(fullConfigPath))?.default;
10
- if (config.features.ignoreUnusedDeps && !fedOptions.entryPoint) {
11
- throw new Error(`The feature ignoreUnusedDeps needs the application's entry point. Please set it in your federation options!`);
12
- }
13
- if (config.features.ignoreUnusedDeps) {
14
- // const entryPoint = path.join(fedOptions.workspaceRoot, fedOptions.entryPoint ?? '');
15
- return removeUnusedDeps(config, fedOptions.entryPoint ?? '', fedOptions.workspaceRoot);
16
- }
17
- return config;
18
- }
@@ -1,4 +0,0 @@
1
- import type { FederationCache } from '../../domain.js';
2
- import type { FederationOptions, NormalizedFederationOptions } from '../domain/core/federation-options.contract.js';
3
- export declare function normalizeFederationOptions(options: FederationOptions): NormalizedFederationOptions<undefined>;
4
- export declare function normalizeFederationOptions<TBundlerCache>(options: FederationOptions, cache: FederationCache<TBundlerCache>): NormalizedFederationOptions<TBundlerCache>;
@@ -1,10 +0,0 @@
1
- import { getDefaultCachePath } from '../utils/cache-persistence.js';
2
- import { createFederationCache } from './federation-cache.js';
3
- export function normalizeFederationOptions(options, cache) {
4
- const federationCache = cache ??
5
- createFederationCache(getDefaultCachePath(options.workspaceRoot));
6
- return {
7
- ...options,
8
- federationCache,
9
- };
10
- }
@@ -1,2 +0,0 @@
1
- import type { NormalizedFederationConfig } from '../domain/config/federation-config.contract.js';
2
- export declare function removeUnusedDeps(config: NormalizedFederationConfig, main: string, workspaceRoot: string): NormalizedFederationConfig;
@@ -1,90 +0,0 @@
1
- import { getProjectData } from '@softarc/sheriff-core';
2
- import path from 'path';
3
- import fs from 'fs';
4
- import { cwd } from 'process';
5
- import { getPackageInfo } from '../utils/package-info.js';
6
- import { getExternalImports as extractExternalImports } from '../utils/get-external-imports.js';
7
- import { normalizePackageName } from '../utils/normalize.js';
8
- import { resolveProjectName } from '../utils/config-utils.js';
9
- export function removeUnusedDeps(config, main, workspaceRoot) {
10
- const fileInfos = getProjectData(main, cwd(), {
11
- includeExternalLibraries: true,
12
- });
13
- const usedDeps = findUsedDeps(fileInfos, workspaceRoot, config);
14
- const usedPackageNames = usedDeps.usedPackageNames;
15
- const usedMappings = usedDeps.usedMappings;
16
- const projectName = resolveProjectName(config);
17
- const usedPackageNamesWithTransient = addTransientDeps(usedPackageNames, workspaceRoot, projectName);
18
- const filteredShared = filterShared(config, usedPackageNamesWithTransient);
19
- return {
20
- ...config,
21
- shared: filteredShared,
22
- sharedMappings: [...usedMappings],
23
- };
24
- }
25
- function filterShared(config, usedPackageNamesWithTransient) {
26
- return Object.entries(config.shared)
27
- .filter(([shared, meta]) => !!meta.includeSecondaries || usedPackageNamesWithTransient.has(shared))
28
- .reduce((acc, [shared, meta]) => ({ ...acc, [shared]: meta }), {});
29
- }
30
- function findUsedDeps(fileInfos, workspaceRoot, config) {
31
- const usedPackageNames = new Set();
32
- const usedMappings = new Set();
33
- for (const fileName of Object.keys(fileInfos)) {
34
- const fileInfo = fileInfos[fileName];
35
- if (!fileInfo) {
36
- continue;
37
- }
38
- const libs = [...(fileInfo.externalLibraries || []), ...(fileInfo.unresolvedImports || [])];
39
- for (const pckg of libs) {
40
- usedPackageNames.add(pckg);
41
- }
42
- const fullFileName = path.join(workspaceRoot, fileName);
43
- const mappings = config.sharedMappings.filter(sm => fullFileName.startsWith(sm.path));
44
- for (const mapping of mappings) {
45
- usedMappings.add(mapping);
46
- }
47
- }
48
- return { usedPackageNames, usedMappings };
49
- }
50
- function addTransientDeps(packages, workspaceRoot, projectName) {
51
- const packagesAndPeers = new Set([...packages]);
52
- const discovered = new Set(packagesAndPeers);
53
- const stack = [...packagesAndPeers];
54
- while (stack.length > 0) {
55
- const dep = stack.pop();
56
- if (!dep) {
57
- continue;
58
- }
59
- const pInfo = getPackageInfo(dep, workspaceRoot);
60
- if (!pInfo) {
61
- continue;
62
- }
63
- const peerDeps = getExternalImports(pInfo, workspaceRoot, projectName);
64
- for (const peerDep of peerDeps) {
65
- if (!discovered.has(peerDep)) {
66
- discovered.add(peerDep);
67
- stack.push(peerDep);
68
- packagesAndPeers.add(peerDep);
69
- }
70
- }
71
- }
72
- return packagesAndPeers;
73
- }
74
- function getExternalImports(pInfo, workspaceRoot, projectName) {
75
- const encodedPackageName = normalizePackageName(pInfo.packageName);
76
- const cacheFileName = `${encodedPackageName}-${pInfo.version}.deps.json`;
77
- const cachePath = path.join(workspaceRoot, 'node_modules/.cache/native-federation/_externals-metadata', projectName);
78
- const cacheFilePath = path.join(cachePath, cacheFileName);
79
- const cacheHit = fs.existsSync(cacheFilePath);
80
- let peerDeps;
81
- if (cacheHit) {
82
- peerDeps = JSON.parse(fs.readFileSync(cacheFilePath, 'utf-8'));
83
- }
84
- else {
85
- peerDeps = extractExternalImports(pInfo.entryPoint);
86
- fs.mkdirSync(cachePath, { recursive: true });
87
- fs.writeFileSync(cacheFilePath, JSON.stringify(peerDeps, undefined, 2), 'utf-8');
88
- }
89
- return peerDeps;
90
- }
@@ -1,2 +0,0 @@
1
- import type { NormalizedFederationConfig } from '../domain/config/federation-config.contract.js';
2
- export declare function resolveProjectName(config: NormalizedFederationConfig): string;
@@ -1,9 +0,0 @@
1
- import { logger } from './logger.js';
2
- import { normalizePackageName } from './normalize.js';
3
- export function resolveProjectName(config) {
4
- const normalizedProjectName = normalizePackageName(config.name);
5
- if (normalizedProjectName.length < 1) {
6
- logger.warn("Project name in 'federation.config.js' is empty, defaulting to 'shell' cache folder (could collide with other projects in the workspace).");
7
- }
8
- return normalizedProjectName.length < 1 ? 'shell' : normalizedProjectName;
9
- }