From 8c4f5b45f4eed148cc332f3a5937bce89e2cc005 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Sun, 18 Oct 2026 13:32:40 +0200 Subject: [PATCH 1/2] feat(builder): Add a cachePath option for the federation cache Both builders take a cachePath, resolved against the workspace root, and default to node_modules/.cache/native-federation as before. A path at or above the workspace root is rejected, since core purges / on an adapter minor bump. Refs #179 --- src/builders/build/builder.ts | 4 +-- src/builders/build/schema.d.ts | 1 + src/builders/build/schema.json | 4 +++ src/builders/remote/builder.ts | 4 +-- src/builders/remote/schema.d.ts | 1 + src/builders/remote/schema.json | 4 +++ src/utils/resolve-cache-path.spec.ts | 48 ++++++++++++++++++++++++++++ src/utils/resolve-cache-path.ts | 24 ++++++++++++++ 8 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 src/utils/resolve-cache-path.spec.ts create mode 100644 src/utils/resolve-cache-path.ts diff --git a/src/builders/build/builder.ts b/src/builders/build/builder.ts index 3d6aa63..ea65911 100644 --- a/src/builders/build/builder.ts +++ b/src/builders/build/builder.ts @@ -36,7 +36,6 @@ import { import { AbortedError, createNfWatcher, - getDefaultCachePath, linkedSharedDirs, logger, type NfFileWatcher, @@ -52,6 +51,7 @@ import { checkForInvalidImports } from "./../../utils/check-for-invalid-imports. import { warnFlatExternals } from "./../../utils/warn-flat-externals.js"; import { federationSourceFiles } from "./../../utils/federation-source-files.js"; import { watchpackWatch } from "./../../utils/watchpack-watch.js"; +import { resolveCachePath } from "./../../utils/resolve-cache-path.js"; import { federationBuildNotifier } from "./federation-build-notifier.js"; import { createFederationFreshness, @@ -256,7 +256,7 @@ export async function* runBuilder( i18n ? getLocaleSubPath(i18n, localeFilter[0]!) : localeFilter[0]!, ); - const cachePath = getDefaultCachePath(context.workspaceRoot); + const cachePath = resolveCachePath(context.workspaceRoot, nfBuilderOptions.cachePath); const normalized = await normalizeFederationOptions( { diff --git a/src/builders/build/schema.d.ts b/src/builders/build/schema.d.ts index d9811d5..900bada 100644 --- a/src/builders/build/schema.d.ts +++ b/src/builders/build/schema.d.ts @@ -23,6 +23,7 @@ export interface NfBuilderSchema extends JsonObject { devServer?: boolean; entryPoints?: string[]; cacheExternalArtifacts?: boolean; + cachePath?: string; } export type NfInternalOptions = { diff --git a/src/builders/build/schema.json b/src/builders/build/schema.json index e3fc570..54fcf4c 100644 --- a/src/builders/build/schema.json +++ b/src/builders/build/schema.json @@ -81,6 +81,10 @@ "description": "Will cache the shared externals so they can be re-used between builds", "alias": "cache" }, + "cachePath": { + "type": "string", + "description": "Folder for the federation cache: shared externals, the generated tsconfigs and the compiler cache. Relative paths resolve against the workspace root. Defaults to 'node_modules/.cache/native-federation'. Don't place it directly under Angular's cache folder ('.angular/cache'): Angular deletes every folder there except its current version folder, so use e.g. '.angular/cache//native-federation' or '.angular/nf-cache'." + }, "buildNotifications": { "type": "object", "description": "Opt in to build completion notifications. Omitting this option leaves them off; the defaults below only apply once it is present.", diff --git a/src/builders/remote/builder.ts b/src/builders/remote/builder.ts index 954a6eb..bcda1f7 100644 --- a/src/builders/remote/builder.ts +++ b/src/builders/remote/builder.ts @@ -22,7 +22,6 @@ import { setLogLevel, RebuildQueue, AbortedError, - getDefaultCachePath, syncNfFileWatcher, linkedSharedDirs, sharedMappingDirs, @@ -33,6 +32,7 @@ import { withDiskCaseWorkspaceRoot } from '../../utils/disk-case.js'; import { checkForInvalidImports } from '../../utils/check-for-invalid-imports.js'; import { warnFlatExternals } from '../../utils/warn-flat-externals.js'; import { federationSourceFiles } from '../../utils/federation-source-files.js'; +import { resolveCachePath } from '../../utils/resolve-cache-path.js'; import type { NfRemoteBuilderSchema, NfRemoteInternalOptions } from './schema.js'; import { resolveNgBuilderOptions } from './resolve-ng-options.js'; @@ -89,7 +89,7 @@ export async function* runRemoteBuilder( setBuildAdapter(adapter); setLogLevel(nfBuilderOptions.verbose ? 'verbose' : 'info'); - const cachePath = getDefaultCachePath(context.workspaceRoot); + const cachePath = resolveCachePath(context.workspaceRoot, nfBuilderOptions.cachePath); const normalized = await normalizeFederationOptions( { diff --git a/src/builders/remote/schema.d.ts b/src/builders/remote/schema.d.ts index 6a9e2e2..96acca8 100644 --- a/src/builders/remote/schema.d.ts +++ b/src/builders/remote/schema.d.ts @@ -13,6 +13,7 @@ export interface NfRemoteBuilderSchema extends JsonObject { verbose?: boolean; entryPoints?: string[]; cacheExternalArtifacts?: boolean; + cachePath?: string; // Passthroughs to the Angular esbuild pipeline / asset copier. assets?: ApplicationBuilderOptions['assets']; diff --git a/src/builders/remote/schema.json b/src/builders/remote/schema.json index 9587c4f..6df86c8 100644 --- a/src/builders/remote/schema.json +++ b/src/builders/remote/schema.json @@ -50,6 +50,10 @@ "description": "Will cache the shared externals so they can be re-used between builds", "alias": "cache" }, + "cachePath": { + "type": "string", + "description": "Folder for the federation cache: shared externals, the generated tsconfigs and the compiler cache. Relative paths resolve against the workspace root. Defaults to 'node_modules/.cache/native-federation'. Don't place it directly under Angular's cache folder ('.angular/cache'): Angular deletes every folder there except its current version folder, so use e.g. '.angular/cache//native-federation' or '.angular/nf-cache'." + }, "assets": { "type": "array", "description": "Assets to copy into the output directory. Same shape as @angular/build:application 'assets'.", diff --git a/src/utils/resolve-cache-path.spec.ts b/src/utils/resolve-cache-path.spec.ts new file mode 100644 index 0000000..ffb627a --- /dev/null +++ b/src/utils/resolve-cache-path.spec.ts @@ -0,0 +1,48 @@ +import * as path from 'path'; + +import { resolveCachePath } from './resolve-cache-path.js'; + +const root = path.resolve('/workspace/app'); + +describe('resolveCachePath', () => { + it('falls back to core default when unset or empty', () => { + expect(resolveCachePath(root)).toBe(path.join(root, 'node_modules/.cache/native-federation')); + expect(resolveCachePath(root, '')).toBe( + path.join(root, 'node_modules/.cache/native-federation') + ); + }); + + it('resolves a relative path against the workspace root', () => { + expect(resolveCachePath(root, '.angular/nf-cache')).toBe(path.join(root, '.angular/nf-cache')); + }); + + it('keeps an absolute path as is', () => { + const abs = path.resolve('/tmp/nf-cache'); + expect(resolveCachePath(root, abs)).toBe(abs); + }); + + it('normalizes ".." segments, allowing a folder outside the workspace', () => { + expect(resolveCachePath(root, '../shared-cache/nf')).toBe( + path.resolve('/workspace/shared-cache/nf') + ); + }); + + it('accepts a sibling whose name merely starts with ".."', () => { + // path.relative(root/..foo, root) is "..": a plain prefix check on ".." would misread + // names like this, so the helper compares whole segments. + expect(resolveCachePath(root, '..foo')).toBe(path.join(root, '..foo')); + }); + + it('rejects an ancestor whose child folder name starts with ".."', () => { + const odd = path.resolve('/workspace/..hidden/app'); + expect(() => resolveCachePath(odd, '..')).toThrow(/contains the workspace root/); + }); + + // Core rm -rf's / on an adapter minor bump; these would hit sources. + it.each(['.', './', '..', '../..', root, path.resolve('/')])( + 'rejects %s, which contains the workspace root', + cachePath => { + expect(() => resolveCachePath(root, cachePath)).toThrow(/contains the workspace root/); + } + ); +}); diff --git a/src/utils/resolve-cache-path.ts b/src/utils/resolve-cache-path.ts new file mode 100644 index 0000000..9385e76 --- /dev/null +++ b/src/utils/resolve-cache-path.ts @@ -0,0 +1,24 @@ +import * as path from 'path'; + +import { getDefaultCachePath } from '@softarc/native-federation/internal'; + +export function resolveCachePath(workspaceRoot: string, cachePath?: string): string { + if (!cachePath) { + return getDefaultCachePath(workspaceRoot); + } + + const resolved = path.resolve(workspaceRoot, cachePath); + + // Core purges `/` whenever the adapter's minor changes, so a cachePath + // at or above the workspace root could delete a source folder named like the project. + const up = path.relative(resolved, workspaceRoot); + const isOutside = up === '..' || up.startsWith(`..${path.sep}`) || path.isAbsolute(up); + if (!isOutside) { + throw new Error( + `The cachePath "${cachePath}" resolves to "${resolved}", which contains the workspace ` + + `root. Point it at a dedicated folder, e.g. ".angular/nf-cache".` + ); + } + + return resolved; +} From a6bb0b6f3e7f4bf595598bf5c6aa5021c289f909 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Sun, 18 Oct 2026 13:45:30 +0200 Subject: [PATCH 2/2] docs(builder): Tighten the cachePath description Drop the '.angular/cache/' suggestion, which Angular purges on upgrade, and note that a custom cache folder must be gitignored. --- src/builders/build/schema.json | 2 +- src/builders/remote/schema.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/builders/build/schema.json b/src/builders/build/schema.json index 54fcf4c..1bc15a8 100644 --- a/src/builders/build/schema.json +++ b/src/builders/build/schema.json @@ -83,7 +83,7 @@ }, "cachePath": { "type": "string", - "description": "Folder for the federation cache: shared externals, the generated tsconfigs and the compiler cache. Relative paths resolve against the workspace root. Defaults to 'node_modules/.cache/native-federation'. Don't place it directly under Angular's cache folder ('.angular/cache'): Angular deletes every folder there except its current version folder, so use e.g. '.angular/cache//native-federation' or '.angular/nf-cache'." + "description": "Folder for the federation cache, relative to the workspace root. Defaults to 'node_modules/.cache/native-federation'. Avoid '.angular/cache' (Angular purges it); use e.g. '.angular/nf-cache' and gitignore it." }, "buildNotifications": { "type": "object", diff --git a/src/builders/remote/schema.json b/src/builders/remote/schema.json index 6df86c8..2c3d29d 100644 --- a/src/builders/remote/schema.json +++ b/src/builders/remote/schema.json @@ -52,7 +52,7 @@ }, "cachePath": { "type": "string", - "description": "Folder for the federation cache: shared externals, the generated tsconfigs and the compiler cache. Relative paths resolve against the workspace root. Defaults to 'node_modules/.cache/native-federation'. Don't place it directly under Angular's cache folder ('.angular/cache'): Angular deletes every folder there except its current version folder, so use e.g. '.angular/cache//native-federation' or '.angular/nf-cache'." + "description": "Folder for the federation cache, relative to the workspace root. Defaults to 'node_modules/.cache/native-federation'. Avoid '.angular/cache' (Angular purges it); use e.g. '.angular/nf-cache' and gitignore it." }, "assets": { "type": "array",