Skip to content
Open
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
37 changes: 35 additions & 2 deletions merge-schemes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { writeFileSync } from 'fs';
import { mergeWith } from 'lodash';
import { resolvePackagePath } from './packages/common/src';

interface CustomSchema {
Expand All @@ -9,6 +8,39 @@ interface CustomSchema {
newSchemaPath: string;
}

/**
* Deep merge two objects, invoking a customizer for each key.
* If the customizer returns `undefined`, the default deep-merge behavior applies.
*/
function deepMergeWith(
target: any,
source: any,
customizer: (targetVal: any, sourceVal: any) => any
): any {
if (source === undefined || source === null) {
return target;
}
const result = Array.isArray(target) ? [...target] : { ...target };
for (const key of Object.keys(source)) {
const customResult = customizer(result[key], source[key]);
if (customResult !== undefined) {
result[key] = customResult;
} else if (
typeof result[key] === 'object' &&
result[key] !== null &&
!Array.isArray(result[key]) &&
typeof source[key] === 'object' &&
source[key] !== null &&
!Array.isArray(source[key])
) {
result[key] = deepMergeWith(result[key], source[key], customizer);
} else {
result[key] = source[key];
}
}
return result;
}

const wd = process.cwd();
const schemesToMerge: CustomSchema[] = require(`${wd}/src/schemes`);

Expand All @@ -27,7 +59,7 @@ for (const {
const schemaExtensions = schemaExtensionPaths.map((path: string) => require(path));
const newSchema = schemaExtensions.reduce(
(extendedSchema: any, currentExtension: any) =>
mergeWith(extendedSchema, currentExtension, schemaMerger),
deepMergeWith(extendedSchema, currentExtension, schemaMerger),
originalSchema
);
writeFileSync(newSchemaPath, JSON.stringify(newSchema, schemaValueReplacer, 2), 'utf-8');
Expand All @@ -37,6 +69,7 @@ function schemaMerger(resultSchemaValue: unknown, extensionSchemaValue: unknown)
if (Array.isArray(extensionSchemaValue) && extensionSchemaValue[0] === '__REPLACE__') {
return extensionSchemaValue.slice(1);
}
return undefined;
}

function schemaValueReplacer(key: unknown, value: unknown) {
Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,9 @@
"@commitlint/config-conventional": "^20.0.0",
"@lerna-lite/cli": "^4.10.5",
"@lerna-lite/publish": "^4.10.5",
"@types/lodash": "^4.14.118",
"@types/node": "^24.0.0",
"husky": "^9.0.0",
"lint-staged": "^16.0.0",
"lodash": "^4.17.15",
"prettier": "^3.0.0",
"ts-jest": "29.4.6",
"turbo": "^2.4.0",
Expand Down
5 changes: 3 additions & 2 deletions packages/custom-esbuild/e2e/custom-esbuild-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { resolvePackagePath } from '@angular-builders/common';
import { remove } from 'lodash';

describe('Custom ESBuild schema tests', () => {
let customEsbuildApplicationSchema: any;
Expand Down Expand Up @@ -32,7 +31,9 @@ describe('Custom ESBuild schema tests', () => {
const path = resolvePackagePath('@angular/build', 'src/builders/unit-test/schema.json');
const originalUnitTestSchema = require(path);
originalUnitTestSchema.properties['runner'] = undefined;
remove(originalUnitTestSchema.required, prop => prop === 'runner');
originalUnitTestSchema.required = originalUnitTestSchema.required.filter(
(prop: string) => prop !== 'runner'
);
customEsbuildUnitTestSchema.properties['plugins'] = undefined;
expect(originalUnitTestSchema.properties).toEqual(customEsbuildUnitTestSchema.properties);
expect(originalUnitTestSchema.required).toEqual(customEsbuildUnitTestSchema.required);
Expand Down
1 change: 0 additions & 1 deletion packages/custom-webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
"@angular-devkit/build-angular": "^21.0.0",
"@angular-devkit/core": "^21.0.0",
"@angular/build": "^21.0.0",
"lodash": "^4.17.15",
"webpack-merge": "^6.0.0"
},
"peerDependencies": {
Expand Down
15 changes: 13 additions & 2 deletions packages/custom-webpack/src/custom-webpack-builder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as path from 'node:path';
import { inspect } from 'util';
import { getSystemPath, logging, Path } from '@angular-devkit/core';
import { get } from 'lodash';
import { Configuration } from 'webpack';
import { loadModule } from '@angular-builders/common';

Expand All @@ -12,6 +11,18 @@ import { mergeConfigs } from './webpack-config-merger';

export const defaultWebpackConfigPath = 'webpack.config.js';

/**
* Accesses a nested property by dot/bracket path (e.g. 'output.enabledChunkLoadingTypes[0]').
*/
function getByPath(obj: any, path: string): any {
const keys = path.replace(/\[(\d+)]/g, '.$1').split('.');
let result = obj;
for (const key of keys) {
result = result?.[key];
}
return result;
}

type CustomWebpackConfig =
| Configuration
| Promise<Configuration>
Expand Down Expand Up @@ -93,7 +104,7 @@ function logConfigProperties(
// entirely. Users can provide a list of properties they want to be logged.
if (config.verbose?.properties) {
for (const property of config.verbose.properties) {
const value = get(webpackConfig, property);
const value = getByPath(webpackConfig, property);
if (value) {
const message = inspect(value, /* showHidden */ false, config.verbose.serializationDepth);
logger.info(message);
Expand Down
44 changes: 35 additions & 9 deletions packages/custom-webpack/src/webpack-config-merger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
import { MergeRules } from './custom-webpack-builder-config';
import { CustomizeRule, mergeWithRules } from 'webpack-merge';
import { Configuration } from 'webpack';
import { differenceWith, keyBy, merge } from 'lodash';

function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
value !== null && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype
);
}

/**
* Recursively deep-merges source into target, mutating target.
* Arrays are replaced entirely (unlike lodash.merge which merges by index).
* This is intentional for webpack plugin options where full replacement is the expected behavior.
*/
function deepMerge<T extends Record<string, any>>(target: T, source: Record<string, any>): T {
for (const key of Object.keys(source)) {
const targetVal = (target as any)[key];
const sourceVal = source[key];
if (isPlainObject(targetVal) && isPlainObject(sourceVal)) {
deepMerge(targetVal, sourceVal);
} else {
(target as any)[key] = sourceVal;
}
}
return target;
}

const DEFAULT_MERGE_RULES: MergeRules = {
module: {
Expand All @@ -24,16 +47,19 @@ export function mergeConfigs(
const mergedConfig: Configuration = mergeWithRules(mergeRules)(webpackConfig1, webpackConfig2);

if (webpackConfig1.plugins && webpackConfig2.plugins) {
const conf1ExceptConf2 = differenceWith(
webpackConfig1.plugins,
webpackConfig2.plugins,
(item1, item2) => item1.constructor.name === item2.constructor.name
const conf1ExceptConf2 = webpackConfig1.plugins.filter(
item1 =>
!webpackConfig2.plugins!.some(item2 => item1.constructor.name === item2.constructor.name)
);
if (!replacePlugins) {
const conf1ByName = keyBy(webpackConfig1.plugins, 'constructor.name');
webpackConfig2.plugins = webpackConfig2.plugins.map(p =>
conf1ByName[p.constructor.name] ? merge(conf1ByName[p.constructor.name], p) : p
);
const conf1ByName: Record<string, (typeof webpackConfig1.plugins)[number]> = {};
for (const p of webpackConfig1.plugins) {
conf1ByName[p.constructor.name] = p;
}
webpackConfig2.plugins = webpackConfig2.plugins.map(p => {
const match = conf1ByName[p.constructor.name];
return match ? deepMerge(match as any, p as any) : p;
}) as typeof webpackConfig2.plugins;
}
mergedConfig.plugins = [...conf1ExceptConf2, ...webpackConfig2.plugins];
}
Expand Down
3 changes: 1 addition & 2 deletions packages/jest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@
"@angular-builders/common": "workspace:*",
"@angular-devkit/architect": ">=0.2100.0 < 0.2200.0",
"@angular-devkit/core": "^21.0.0",
"jest-preset-angular": "^16.0.0",
"lodash": "^4.17.15"
"jest-preset-angular": "^16.0.0"
},
"peerDependencies": {
"@angular-devkit/build-angular": "^21.0.0",
Expand Down
11 changes: 7 additions & 4 deletions packages/jest/src/default-config.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { pick } from 'lodash';
import { getSystemPath, normalize, Path } from '@angular-devkit/core';

import { JestConfig } from './types';
Expand All @@ -13,9 +12,13 @@ const globalMocks = {
};

const getMockFiles = (enabledMocks: string[] = []): string[] =>
Object.values(pick(globalMocks, enabledMocks)).map(fileName =>
getSystemPath(normalize(`${__dirname}/global-mocks/${fileName}`))
);
Object.values(
Object.fromEntries(
enabledMocks
.filter(k => k in globalMocks)
.map(k => [k, globalMocks[k as keyof typeof globalMocks]])
)
).map(fileName => getSystemPath(normalize(`${__dirname}/global-mocks/${fileName}`)));

const getSetupFile = (zoneless: boolean = true): string => {
const setupFileName = zoneless ? 'setup-zoneless.js' : 'setup-zone.js';
Expand Down
59 changes: 45 additions & 14 deletions packages/jest/src/jest-configuration-builder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Path, resolve } from '@angular-devkit/core';
import { isArray, mergeWith } from 'lodash';

import { JestConfig } from './types';
import { CustomConfigResolver } from './custom-config.resolver';
Expand All @@ -15,20 +14,55 @@ const ARRAY_PROPERTIES_TO_CONCAT = [
'astTransformers',
];

type MergeCustomizer = (objValue: any, srcValue: any, key: string) => any;

/**
* Deep merge two objects, invoking a customizer for each key.
* If the customizer returns `undefined`, the default deep-merge behavior applies.
*/
function deepMergeWith(target: any, source: any, customizer: MergeCustomizer): any {
if (source === undefined || source === null) {
return target;
}
if (target === undefined || target === null) {
return source;
}
const result = { ...target };
for (const key of Object.keys(source)) {
const customResult = customizer(result[key], source[key], key);
if (customResult !== undefined) {
result[key] = customResult;
} else if (
typeof result[key] === 'object' &&
result[key] !== null &&
!Array.isArray(result[key]) &&
typeof source[key] === 'object' &&
source[key] !== null &&
!Array.isArray(source[key])
) {
result[key] = deepMergeWith(result[key], source[key], customizer);
} else {
result[key] = source[key];
}
}
return result;
}

/**
* This function checks witch properties should be concat. Early return will
* merge the data as lodash#merge would do it.
* This function checks which properties should be concat. Returning `undefined`
* falls through to the default deep-merge behavior.
*/
function concatArrayProperties(objValue: any[], srcValue: unknown, property: string) {
function concatArrayProperties(objValue: any, srcValue: unknown, property: string): any {
if (!ARRAY_PROPERTIES_TO_CONCAT.includes(property)) {
return;
return undefined;
}

if (!isArray(objValue)) {
return mergeWith(objValue, srcValue, (obj, src) => {
if (isArray(obj)) {
if (!Array.isArray(objValue)) {
return deepMergeWith(objValue, srcValue, (obj, src) => {
if (Array.isArray(obj)) {
return obj.concat(src);
}
return undefined;
});
}

Expand All @@ -47,12 +81,9 @@ const buildConfiguration = async (
const globalCustomConfig = await customConfigResolver.resolveGlobal(workspaceRoot);
const projectCustomConfig = await customConfigResolver.resolveForProject(projectRoot, config);

return mergeWith(
globalDefaultConfig,
projectDefaultConfig,
globalCustomConfig,
projectCustomConfig,
concatArrayProperties
return [projectDefaultConfig, globalCustomConfig, projectCustomConfig].reduce(
(acc, cfg) => deepMergeWith(acc, cfg, concatArrayProperties),
globalDefaultConfig
);
};

Expand Down
13 changes: 1 addition & 12 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,6 @@ __metadata:
"@angular-devkit/core": ^21.0.0
"@angular/build": ^21.0.0
jest: 30.2.0
lodash: ^4.17.15
rimraf: ^6.0.0
ts-node: ^10.0.0
typescript: 5.9.3
Expand All @@ -253,7 +252,6 @@ __metadata:
cpy-cli: ^7.0.0
jest: 30.2.0
jest-preset-angular: ^16.0.0
lodash: ^4.17.15
quicktype: ^15.0.260
rimraf: ^6.0.0
typescript: 5.9.3
Expand Down Expand Up @@ -7494,13 +7492,6 @@ __metadata:
languageName: node
linkType: hard

"@types/lodash@npm:^4.14.118":
version: 4.17.24
resolution: "@types/lodash@npm:4.17.24"
checksum: 2b254973145ecdf9b052d83f00ebaa111245f319d45b217c78f81cea8892fda81180fc749bd50a99c08f4e5efceb834c774d3f74153a50a9c4a28648343dc9f3
languageName: node
linkType: hard

"@types/mime@npm:^1":
version: 1.3.5
resolution: "@types/mime@npm:1.3.5"
Expand Down Expand Up @@ -8547,11 +8538,9 @@ __metadata:
"@commitlint/config-conventional": ^20.0.0
"@lerna-lite/cli": ^4.10.5
"@lerna-lite/publish": ^4.10.5
"@types/lodash": ^4.14.118
"@types/node": ^24.0.0
husky: ^9.0.0
lint-staged: ^16.0.0
lodash: ^4.17.15
prettier: ^3.0.0
ts-jest: 29.4.6
turbo: ^2.4.0
Expand Down Expand Up @@ -15767,7 +15756,7 @@ __metadata:
languageName: node
linkType: hard

"lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.23":
"lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.23":
version: 4.17.23
resolution: "lodash@npm:4.17.23"
checksum: 7daad39758a72872e94651630fbb54ba76868f904211089721a64516ce865506a759d9ad3d8ff22a2a49a50a09db5d27c36f22762d21766e47e3ba918d6d7bab
Expand Down