diff --git a/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts index 35c854a9821..05ef220f871 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts @@ -7,7 +7,7 @@ */ import {AbsoluteFsPath, ReadonlyFileSystem} from '../../../src/ngtsc/file_system'; import {PathMappings} from '../path_mappings'; -import {isRelativePath, resolveFileWithPostfixes} from '../utils'; +import {isRelativePath, loadJson, loadSecondaryEntryPointInfoForApfV14, resolveFileWithPostfixes} from '../utils'; /** * This is a very cut-down implementation of the TypeScript module resolution strategy. @@ -110,8 +110,8 @@ export class ModuleResolver { * Try to resolve the `moduleName` as an external entry-point by searching the `node_modules` * folders up the tree for a matching `.../node_modules/${moduleName}`. * - * If a folder is found but the path does not contain a `package.json` then it is marked as a - * "deep-import". + * If a folder is found but the path is not considered an entry-point (see `isEntryPoint()`) then + * it is marked as a "deep-import". */ private resolveAsEntryPoint(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null { let folder = fromPath; @@ -136,9 +136,25 @@ export class ModuleResolver { * Can we consider the given path as an entry-point to a package? * * This is achieved by checking for the existence of `${modulePath}/package.json`. + * If there is no `package.json`, we check whether this is an APF v14+ secondary entry-point, + * which does not have its own `package.json` but has an `exports` entry in the package's primary + * `package.json`. */ private isEntryPoint(modulePath: AbsoluteFsPath): boolean { - return this.fs.exists(this.fs.join(modulePath, 'package.json')); + if (this.fs.exists(this.fs.join(modulePath, 'package.json'))) { + return true; + } + + const packagePath = this.findPackagePath(modulePath); + if (packagePath === null) { + return false; + } + + const packagePackageJson = loadJson(this.fs, this.fs.join(packagePath, 'package.json')); + const entryPointInfoForApfV14 = + loadSecondaryEntryPointInfoForApfV14(this.fs, packagePackageJson, packagePath, modulePath); + + return entryPointInfoForApfV14 !== null; } /** diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point.ts b/packages/compiler-cli/ngcc/src/packages/entry_point.ts index bf04119ad69..95ea1b4b358 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point.ts @@ -10,7 +10,7 @@ import ts from 'typescript'; import {AbsoluteFsPath, PathManipulation, ReadonlyFileSystem} from '../../../src/ngtsc/file_system'; import {Logger} from '../../../src/ngtsc/logging'; import {parseStatementForUmdModule} from '../host/umd_host'; -import {JsonObject, loadJson, resolveFileWithPostfixes} from '../utils'; +import {JsonObject, loadJson, loadSecondaryEntryPointInfoForApfV14, resolveFileWithPostfixes} from '../utils'; import {NgccConfiguration, NgccEntryPointConfig} from './configuration'; @@ -131,7 +131,8 @@ export function getEntryPointInfo( const loadedPackagePackageJson = loadJson(fs, packagePackageJsonPath); const loadedEntryPointPackageJson = (packagePackageJsonPath === entryPointPackageJsonPath) ? loadedPackagePackageJson : - loadJson(fs, entryPointPackageJsonPath); + loadOrSynthesizeSecondaryPackageJson( + fs, packagePath, entryPointPath, entryPointPackageJsonPath, loadedPackagePackageJson); const {packageName, packageVersion} = getPackageNameAndVersion( fs, packagePath, loadedPackagePackageJson, loadedEntryPointPackageJson); const repositoryUrl = getRepositoryUrl(loadedPackagePackageJson); @@ -141,17 +142,17 @@ export function getEntryPointInfo( let entryPointPackageJson: EntryPointPackageJson; if (entryPointConfig === undefined) { - if (!fs.exists(entryPointPackageJsonPath)) { - // No `package.json` and no config. + if (loadedEntryPointPackageJson !== null) { + entryPointPackageJson = loadedEntryPointPackageJson; + } else if (!fs.exists(entryPointPackageJsonPath)) { + // No entry-point `package.json` or package `package.json` with exports and no config. return NO_ENTRY_POINT; - } else if (loadedEntryPointPackageJson === null) { + } else { // `package.json` exists but could not be parsed and there is no redeeming config. logger.warn(`Failed to read entry point info from invalid 'package.json' file: ${ entryPointPackageJsonPath}`); return INCOMPATIBLE_ENTRY_POINT; - } else { - entryPointPackageJson = loadedEntryPointPackageJson; } } else if (entryPointConfig.ignore === true) { // Explicitly ignored entry-point. @@ -246,6 +247,69 @@ export function getEntryPointFormat( } } +/** + * Parse the JSON from a secondary `package.json` file. If no such file exists, look for a + * corresponding entry in the primary `package.json` file's `exports` property (if any) and + * synthesize the JSON from that. + * + * @param packagePath The absolute path to the containing npm package. + * @param entryPointPath The absolute path to the secondary entry-point. + * @param secondaryPackageJsonPath The absolute path to the secondary `package.json` file. + * @param primaryPackageJson The parsed JSON of the primary `package.json` (or `null` if it failed + * to be loaded). + * @returns Parsed JSON (either loaded from a secondary `package.json` file or synthesized from a + * primary one) if it is valid, `null` otherwise. + */ +function loadOrSynthesizeSecondaryPackageJson( + fs: ReadonlyFileSystem, packagePath: AbsoluteFsPath, entryPointPath: AbsoluteFsPath, + secondaryPackageJsonPath: AbsoluteFsPath, + primaryPackageJson: EntryPointPackageJson|null): EntryPointPackageJson|null { + // If a secondary `package.json` exists and is valid, load and return that. + const loadedPackageJson = loadJson(fs, secondaryPackageJsonPath); + if (loadedPackageJson !== null) { + return loadedPackageJson; + } + + // Try to load the entry-point info from the primary `package.json` data. + const entryPointInfo = + loadSecondaryEntryPointInfoForApfV14(fs, primaryPackageJson, packagePath, entryPointPath); + if (entryPointInfo === null) { + return null; + } + + // Create a synthesized `package.json`. + // + // NOTE: + // We do not care about being able to update the synthesized `package.json` (for example, updating + // its `__processed_by_ivy_ngcc__` property), because these packages are generated with Angular + // v14+ (following the Angular Package Format v14+) and thus are already in Ivy format and do not + // require processing by `ngcc`. + const synthesizedPackageJson: EntryPointPackageJson = { + synthesized: true, + name: `${primaryPackageJson!.name}/${fs.relative(packagePath, entryPointPath)}`, + }; + + // Update the synthesized `package.json` with any of the supported format and types properties, + // changing paths to make them relative to the entry-point directory. This makes the synthesized + // `package.json` similar to how a `package.json` inside the entry-point directory would look + // like. + for (const prop of [...SUPPORTED_FORMAT_PROPERTIES, 'types', 'typings']) { + const packageRelativePath = entryPointInfo[prop]; + + if (typeof packageRelativePath === 'string') { + const absolutePath = fs.resolve(packagePath, packageRelativePath); + const entryPointRelativePath = fs.relative(entryPointPath, absolutePath); + synthesizedPackageJson[prop] = + (fs.isRooted(entryPointRelativePath) || entryPointRelativePath.startsWith('.')) ? + entryPointRelativePath : + `./${entryPointRelativePath}`; + } + } + + // Return the synthesized JSON. + return synthesizedPackageJson; +} + function sniffModuleFormat( fs: ReadonlyFileSystem, sourceFilePath: AbsoluteFsPath): EntryPointFormat|undefined { const resolvedPath = resolveFileWithPostfixes(fs, sourceFilePath, ['', '.js', '/index.js']); diff --git a/packages/compiler-cli/ngcc/src/utils.ts b/packages/compiler-cli/ngcc/src/utils.ts index 025221b8678..78794c0e180 100644 --- a/packages/compiler-cli/ngcc/src/utils.ts +++ b/packages/compiler-cli/ngcc/src/utils.ts @@ -186,3 +186,57 @@ export function loadJson( return null; } } + +/** + * Given the parsed JSON of a `package.json` file, try to extract info for a secondary entry-point + * from the `exports` property. Such info will only be present for packages following Angular + * Package Format v14+. + * + * @param primaryPackageJson The parsed JSON of the primary `package.json` (or `null` if it failed + * to be loaded). + * @param packagePath The absolute path to the containing npm package. + * @param entryPointPath The absolute path to the secondary entry-point. + * @returns The `exports` info for the specified entry-point if it exists, `null` otherwise. + */ +export function loadSecondaryEntryPointInfoForApfV14( + fs: ReadonlyFileSystem, primaryPackageJson: JsonObject|null, packagePath: AbsoluteFsPath, + entryPointPath: AbsoluteFsPath): JsonObject|null { + // Check if primary `package.json` has been loaded and has an `exports` property that is an + // object. + const exportMap = primaryPackageJson?.exports; + if (!isExportObject(exportMap)) { + return null; + } + + // Find the `exports` key for the secondary entry-point. + const relativeEntryPointPath = fs.relative(packagePath, entryPointPath); + const entryPointExportKey = `./${relativeEntryPointPath}`; + + // Read the info for the entry-point. + const entryPointInfo = exportMap[entryPointExportKey]; + + // Check whether the entry-point info exists and is an export map. + return isExportObject(entryPointInfo) ? entryPointInfo : null; +} + +/** + * Check whether a value read from a JSON file is a Node.js export map (either the top-level one or + * one for a subpath). + * + * In `package.json` files, the `exports` field can be of type `Object | string | string[]`, but APF + * v14+ uses an object with subpath exports for each entry-point, which in turn are conditional + * exports (see references below). This function verifies that a value read from the top-level + * `exports` field or a subpath is of type `Object` (and not `string` or `string[]`). + * + * References: + * - https://nodejs.org/api/packages.html#exports + * - https://nodejs.org/api/packages.html#subpath-exports + * - https://nodejs.org/api/packages.html#conditional-exports + * - https://v14.angular.io/guide/angular-package-format#exports + * + * @param thing The value read from the JSON file + * @returns True if the value is an `Object` (and not an `Array`). + */ +function isExportObject(thing: JsonValue): thing is JsonObject { + return (typeof thing === 'object') && (thing !== null) && !Array.isArray(thing); +} diff --git a/packages/compiler-cli/ngcc/src/writing/package_json_updater.ts b/packages/compiler-cli/ngcc/src/writing/package_json_updater.ts index d50fc589f27..4ced59f580d 100644 --- a/packages/compiler-cli/ngcc/src/writing/package_json_updater.ts +++ b/packages/compiler-cli/ngcc/src/writing/package_json_updater.ts @@ -101,6 +101,7 @@ export class PackageJsonUpdate { */ writeChanges(packageJsonPath: AbsoluteFsPath, parsedJson?: JsonObject): void { this.ensureNotApplied(); + this.ensureNotSynthesized(parsedJson); this.writeChangesImpl(this.changes, packageJsonPath, parsedJson); this.applied = true; } @@ -110,6 +111,15 @@ export class PackageJsonUpdate { throw new Error('Trying to apply a `PackageJsonUpdate` that has already been applied.'); } } + + private ensureNotSynthesized(parsedJson?: JsonObject) { + if (parsedJson?.synthesized) { + // Theoretically, this should never happen, because synthesized `package.json` files should + // only be created for libraries following the Angular Package Format v14+, which means they + // should already be in Ivy format and not require processing by `ngcc`. + throw new Error('Trying to update a non-existent (synthesized) `package.json` file.'); + } + } } /** A `PackageJsonUpdater` that writes directly to the file-system. */ diff --git a/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts index c308949d2de..70673179716 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/module_resolver_spec.ts @@ -64,6 +64,25 @@ runInEachFileSystem(() => { name: _('/node_modules/top-package/package.json'), contents: 'PACKAGE.JSON for top-package' }, + { + name: _('/node_modules/apf-v14-package/package.json'), + contents: `{ + "name": "apf-v14-package", + "exports": { + "./second/ary": { + "main": "./second/ary/index.js" + } + } + }`, + }, + { + name: _('/node_modules/apf-v14-package/index.js'), + contents: `export const type = 'primary';`, + }, + { + name: _('/node_modules/apf-v14-package/second/ary/index.js'), + contents: `export const type = 'secondary';`, + }, ]); }); @@ -144,6 +163,17 @@ runInEachFileSystem(() => { .toEqual(new ResolvedDeepImport( _('/libs/local-package/node_modules/package-1/sub-folder'))); }); + + it('should resolve to APF v14+ secondary entry-points', () => { + const resolver = new ModuleResolver(getFileSystem()); + + expect(resolver.resolveModuleImport( + 'apf-v14-package/second/ary', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/node_modules/apf-v14-package/second/ary'))); + expect(resolver.resolveModuleImport( + 'apf-v14-package/second/ary', _('/libs/local-package/sub-folder/index.js'))) + .toEqual(new ResolvedExternalModule(_('/node_modules/apf-v14-package/second/ary'))); + }); }); describe('with mapped path external modules', () => { @@ -256,6 +286,20 @@ runInEachFileSystem(() => { 'package-4/secondary-entry-point', _('/dist/package-4/index.js'))) .toEqual(new ResolvedExternalModule(_('/dist/package-4/secondary-entry-point'))); }); + + it('should resolve APF v14+ secondary entry-points', () => { + const resolver = new ModuleResolver(getFileSystem(), { + baseUrl: '/node_modules', + paths: {'package-42': ['apf-v14-package'], 'package-42/*': ['apf-v14-package/*']}, + }); + + expect(resolver.resolveModuleImport( + 'package-42/second/ary', _('/libs/local-package/index.js'))) + .toEqual(new ResolvedExternalModule(_('/node_modules/apf-v14-package/second/ary'))); + expect(resolver.resolveModuleImport( + 'package-42/second/ary', _('/libs/local-package/sub-folder/index.js'))) + .toEqual(new ResolvedExternalModule(_('/node_modules/apf-v14-package/second/ary'))); + }); }); describe('with mapped path relative paths', () => { diff --git a/packages/compiler-cli/ngcc/test/execution/cluster/package_json_updater_spec.ts b/packages/compiler-cli/ngcc/test/execution/cluster/package_json_updater_spec.ts index bf8658057f9..03fad5bd7f1 100644 --- a/packages/compiler-cli/ngcc/test/execution/cluster/package_json_updater_spec.ts +++ b/packages/compiler-cli/ngcc/test/execution/cluster/package_json_updater_spec.ts @@ -197,6 +197,14 @@ runInEachFileSystem(() => { expect(() => update.writeChanges(_('/bar/package.json'))) .toThrowError('Trying to apply a `PackageJsonUpdate` that has already been applied.'); }); + + it('should throw, if trying to update a synthesized `package.json` file', () => { + const update = updater.createUpdate().addChange(['foo'], 'updated'); + + expect(() => update.writeChanges(_('/foo/package.json'), { + synthesized: true + })).toThrowError('Trying to update a non-existent (synthesized) `package.json` file.'); + }); }); }); diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts index 1970a8ddcc8..25b92279bc9 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts @@ -707,6 +707,125 @@ runInEachFileSystem(() => { _('/project/node_modules/some_package/unexpected_symbols')); expect(entryPoint).toBe(INCOMPATIBLE_ENTRY_POINT); }); + + describe('when processing APF v14+ secondary entry-points', () => { + it('should retrieve info from primary `package.json`', () => { + const packagePath = _('/project/node_modules/primary'); + const entryPointPath = fs.join(packagePath, 'second/ary'); + + loadTestFiles([ + { + name: fs.join(packagePath, 'package.json'), + contents: `{ + "name": "primary-package", + "exports": { + ".": { + "types": "/something/primary.d.ts" + }, + "./second/ary": { + "types": "/something/secondary.d.ts" + } + } + }`, + }, + ]); + + const config = new NgccConfiguration(fs, _('/project')); + const entryPoint = + getEntryPointInfo(fs, config, new MockLogger(), packagePath, entryPointPath); + + expect(entryPoint).toEqual({ + name: 'primary-package/second/ary', + path: entryPointPath, + packageName: 'primary-package', + packagePath, + repositoryUrl: '', + packageJson: { + synthesized: true, + name: 'primary-package/second/ary', + types: jasmine.any(String), + }, + typings: _('/something/secondary.d.ts'), + compiledByAngular: false, + ignoreMissingDependencies: false, + generateDeepReexports: false, + }); + }); + + it('should only keep supported format properties', () => { + const packagePath = _('/project/node_modules/primary'); + const entryPointPath = fs.join(packagePath, 'second/ary'); + + loadTestFiles([ + { + name: fs.join(packagePath, 'package.json'), + contents: `{ + "name": "primary-package", + "exports": { + "./second/ary": { + "types": "/types/index.d.ts", + "typings": "/typings/index.d.ts", + "default": "/default/index.js", + "other": "/other/index.js", + ${ + SUPPORTED_FORMAT_PROPERTIES.map(prop => `"${prop}": "/${prop}/index.js"`) + .join(', ')} + } + } + }`, + }, + ]); + + const config = new NgccConfiguration(fs, _('/project')); + const entryPoint = + getEntryPointInfo(fs, config, new MockLogger(), packagePath, entryPointPath) as + EntryPoint; + const packageJsonProps = Object.keys(entryPoint.packageJson); + + expect(packageJsonProps).not.toContain('default'); + expect(packageJsonProps).not.toContain('other'); + expect(packageJsonProps).toEqual(jasmine.arrayWithExactContents([ + 'synthesized', + 'name', + 'types', + 'typings', + ...SUPPORTED_FORMAT_PROPERTIES, + ])); + }); + + it('should convert format paths to be relative to entry-point directory', () => { + const packagePath = _('/project/node_modules/primary'); + const entryPointPath = fs.join(packagePath, 'second/ary'); + + loadTestFiles([ + { + name: fs.join(packagePath, 'package.json'), + contents: `{ + "name": "primary-package", + "main": "./bundles/index.js", + "exports": { + "./second/ary": { + "fesm2015": "./fesm2015/secondary.js", + "main": "./bundles/secondary.js", + "types": "./second/ary/secondary.d.ts" + } + } + }`, + }, + ]); + + const config = new NgccConfiguration(fs, _('/project')); + const entryPoint = + getEntryPointInfo(fs, config, new MockLogger(), packagePath, entryPointPath) as + EntryPoint; + + expect(entryPoint.packageJson).toEqual(jasmine.objectContaining({ + fesm2015: '../../fesm2015/secondary.js', + main: '../../bundles/secondary.js', + types: './secondary.d.ts', + })); + }); + }); }); describe('getEntryPointFormat()', () => { diff --git a/packages/compiler-cli/ngcc/test/utils_spec.ts b/packages/compiler-cli/ngcc/test/utils_spec.ts index 1a46c5564bb..baedd52b1bd 100644 --- a/packages/compiler-cli/ngcc/test/utils_spec.ts +++ b/packages/compiler-cli/ngcc/test/utils_spec.ts @@ -12,7 +12,7 @@ import {absoluteFrom as _abs, FileSystem, getFileSystem} from '../../src/ngtsc/f import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing'; import {KnownDeclaration} from '../../src/ngtsc/reflection'; import {loadTestFiles} from '../../src/ngtsc/testing'; -import {FactoryMap, getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, isRelativePath, loadJson, stripExtension} from '../src/utils'; +import {FactoryMap, getTsHelperFnFromDeclaration, getTsHelperFnFromIdentifier, isRelativePath, loadJson, loadSecondaryEntryPointInfoForApfV14, stripExtension} from '../src/utils'; describe('FactoryMap', () => { it('should return an existing value', () => { @@ -276,3 +276,114 @@ runInEachFileSystem(() => { }); }); }); + +runInEachFileSystem(() => { + let fs: FileSystem; + + beforeEach(() => fs = getFileSystem()); + + describe('loadSecondaryEntryPointInfoForApfV14()', () => { + it('should return `null` if the primary `package.json` failed to be loaded', () => { + expect(loadSecondaryEntryPointInfoForApfV14(fs, null, _abs('/foo'), _abs('/foo/bar'))) + .toBe(null); + }); + + it('should return `null` if the primary `package.json` has no `exports` property', () => { + const primaryPackageJson = { + name: 'some-package', + version: '1.33.7', + main: './index.js', + }; + + expect(loadSecondaryEntryPointInfoForApfV14( + fs, primaryPackageJson, _abs('/foo'), _abs('/foo/bar'))) + .toBe(null); + }); + + it('should return `null` if the primary `package.json`\'s `exports` property is a string', + () => { + const primaryPackageJson = { + name: 'some-package', + exports: './index.js', + }; + + expect(loadSecondaryEntryPointInfoForApfV14( + fs, primaryPackageJson, _abs('/foo'), _abs('/foo/bar'))) + .toBe(null); + }); + + it('should return `null` if the primary `package.json`\'s `exports` property is a string array', + () => { + const primaryPackageJson = { + name: 'some-package', + exports: [ + './foo.js', + './bar.js', + ], + }; + + expect(loadSecondaryEntryPointInfoForApfV14( + fs, primaryPackageJson, _abs('/foo'), _abs('/foo/bar'))) + .toBe(null); + }); + + it('should return `null` if there is no info for the specified entry-point', () => { + const primaryPackageJson = { + name: 'some-package', + exports: { + './baz': { + main: './baz/index.js', + }, + }, + }; + + expect(loadSecondaryEntryPointInfoForApfV14( + fs, primaryPackageJson, _abs('/foo'), _abs('/foo/bar'))) + .toBe(null); + }); + + it('should return `null` if the entry-point info is a string', () => { + const primaryPackageJson = { + name: 'some-package', + exports: { + './bar': './bar/index.js', + }, + }; + + expect(loadSecondaryEntryPointInfoForApfV14( + fs, primaryPackageJson, _abs('/foo'), _abs('/foo/bar'))) + .toBe(null); + }); + + it('should return `null` if the entry-point info is a string array', () => { + const primaryPackageJson = { + name: 'some-package', + exports: { + './bar': [ + './bar/a.js', + './bar/b.js', + ], + }, + }; + + expect(loadSecondaryEntryPointInfoForApfV14( + fs, primaryPackageJson, _abs('/foo'), _abs('/foo/bar'))) + .toBe(null); + }); + + it('should return the entry-point info if it exists and is an object', () => { + const primaryPackageJson = { + name: 'some-package', + exports: { + './bar': { + main: './bar/index.js', + }, + }, + }; + + expect(loadSecondaryEntryPointInfoForApfV14( + fs, primaryPackageJson, _abs('/foo'), _abs('/foo/bar'))) + .toEqual({main: './bar/index.js'}); + }); + }); +}); diff --git a/packages/compiler-cli/ngcc/test/writing/package_json_updater_spec.ts b/packages/compiler-cli/ngcc/test/writing/package_json_updater_spec.ts index 4cdc14d68a1..058efde8144 100644 --- a/packages/compiler-cli/ngcc/test/writing/package_json_updater_spec.ts +++ b/packages/compiler-cli/ngcc/test/writing/package_json_updater_spec.ts @@ -153,6 +153,14 @@ runInEachFileSystem(() => { .toThrowError('Trying to apply a `PackageJsonUpdate` that has already been applied.'); }); + it('should throw, if trying to update a synthesized `package.json` file', () => { + const update = updater.createUpdate().addChange(['foo'], 'updated'); + + expect(() => update.writeChanges(_('/foo/package.json'), { + synthesized: true + })).toThrowError('Trying to update a non-existent (synthesized) `package.json` file.'); + }); + describe('(property positioning)', () => { // Helpers const createJsonFile = (jsonObj: JsonObject) => {