Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/builders/build/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import {
import {
AbortedError,
createNfWatcher,
getDefaultCachePath,
linkedSharedDirs,
logger,
type NfFileWatcher,
Expand All @@ -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,
Expand Down Expand Up @@ -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(
{
Expand Down
1 change: 1 addition & 0 deletions src/builders/build/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface NfBuilderSchema extends JsonObject {
devServer?: boolean;
entryPoints?: string[];
cacheExternalArtifacts?: boolean;
cachePath?: string;
}

export type NfInternalOptions = {
Expand Down
4 changes: 4 additions & 0 deletions src/builders/build/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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",
"description": "Opt in to build completion notifications. Omitting this option leaves them off; the defaults below only apply once it is present.",
Expand Down
4 changes: 2 additions & 2 deletions src/builders/remote/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
setLogLevel,
RebuildQueue,
AbortedError,
getDefaultCachePath,
syncNfFileWatcher,
linkedSharedDirs,
sharedMappingDirs,
Expand All @@ -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';
Expand Down Expand Up @@ -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(
{
Expand Down
1 change: 1 addition & 0 deletions src/builders/remote/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
4 changes: 4 additions & 0 deletions src/builders/remote/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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",
"description": "Assets to copy into the output directory. Same shape as @angular/build:application 'assets'.",
Expand Down
48 changes: 48 additions & 0 deletions src/utils/resolve-cache-path.spec.ts
Original file line number Diff line number Diff line change
@@ -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 <cachePath>/<project> 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/);
}
);
});
24 changes: 24 additions & 0 deletions src/utils/resolve-cache-path.ts
Original file line number Diff line number Diff line change
@@ -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 `<cachePath>/<project>` 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;
}
Loading