-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add new builders for metadata migration check
- Loading branch information
Showing
28 changed files
with
776 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
...ages/@o3r/components/builders/metadata-check/helpers/config-metadata-comparison.helper.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { ComponentConfigOutput } from '@o3r/components'; | ||
import { MetadataComparator } from '@o3r/extractors'; | ||
|
||
/** | ||
* Interface describing a config migration element | ||
*/ | ||
export interface MigrationConfigData { | ||
libraryName: string; | ||
configName: string; | ||
propertyName: string; | ||
} | ||
|
||
/** | ||
* Comparator used to compare one version of config metadata with another | ||
*/ | ||
export class ConfigMetadataComparator implements MetadataComparator<ComponentConfigOutput> { | ||
/** | ||
* Returns an array of config metadata from a metadata file. | ||
* To be easily parseable, the properties will be split in separate items of the array. | ||
* e.g. : [{ library: '@o3r/demo', properties: [{name : 'property1', type: 'string'}, {name : 'property2', type: 'number'}] }] | ||
* will become : | ||
* [{ library: '@o3r/demo', properties: [{name : 'property1', type: 'string'}] }, { library: '@o3r/demo', properties: [{name : 'property2', type: 'number'}] }] | ||
* | ||
* @param content Content of a migration metadata files | ||
*/ | ||
public getArray(content: ComponentConfigOutput[]): ComponentConfigOutput[] { | ||
return content.reduce((acc, config) => { | ||
if (config.properties.length) { | ||
const propertiesConfigs = config.properties.map((property) => { | ||
return { | ||
...config, | ||
properties: [property] | ||
}; | ||
}); | ||
acc.push(...propertiesConfigs); | ||
} else { | ||
acc.push(config); | ||
} | ||
return acc; | ||
}, [] as ComponentConfigOutput[]); | ||
} | ||
|
||
/** | ||
* @inheritdoc | ||
*/ | ||
public getName(config: ComponentConfigOutput): string { | ||
return `${config.library}#${config.name}` + config.properties.length ? `-${config.properties[0].name}` : ''; | ||
} | ||
|
||
/** | ||
* @inheritdoc | ||
*/ | ||
public isSame(config1: ComponentConfigOutput, config2: ComponentConfigOutput): boolean { | ||
return config1.name === config2.name && config1.library === config2.library | ||
&& (config1.properties.length ? config1.properties[0].name === config2.properties[0].name : true); | ||
// TODO : second part seems not correct if no properties : config1.properties.length ? config1.properties[0].name === config2.properties[0].name : true | ||
} | ||
|
||
/** | ||
* @inheritdoc | ||
*/ | ||
public isMigrationDataMatch(config: ComponentConfigOutput, migrationData: MigrationConfigData): boolean { | ||
return migrationData.configName === config.name | ||
&& migrationData.libraryName === config.library && (config.properties.length ? migrationData.propertyName === config.properties[0].name : true); | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
packages/@o3r/components/builders/metadata-check/helpers/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './config-metadata-comparison.helper'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; | ||
import { checkMetadataBuilder, createBuilderWithMetricsIfInstalled } from '@o3r/extractors'; | ||
import { ConfigMetadataComparator } from './helpers/config-metadata-comparison.helper'; | ||
import type { ConfigMigrationMetadataCheckBuilderSchema } from './schema'; | ||
|
||
export default createBuilder<ConfigMigrationMetadataCheckBuilderSchema>(createBuilderWithMetricsIfInstalled((options, context): Promise<BuilderOutput> => { | ||
return checkMetadataBuilder(options, context, new ConfigMetadataComparator()); | ||
})); |
46 changes: 46 additions & 0 deletions
46
packages/@o3r/components/builders/metadata-check/schema.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
{ | ||
"$schema": "http://json-schema.org/draft-07/schema", | ||
"type": "object", | ||
"$id": "ConfigMigrationMetadataCheckBuilderSchema", | ||
"title": "Check config migration metadata builder", | ||
"description": "", | ||
"properties": { | ||
"migrationMetadataFolder": { | ||
"type": "string", | ||
"description": "Path to the file containing the migration metadata." | ||
}, | ||
"granularity": { | ||
"type": "string", | ||
"description": "Granularity of the migration check.", | ||
"default": "minor" | ||
}, | ||
"allowBreakingChanges": { | ||
"type": "boolean", | ||
"description": "Are breaking changes allowed.", | ||
"default": true | ||
}, | ||
"packageManager": { | ||
"type": "string", | ||
"description": "Override of the package manager, otherwise it will be determined from the project." | ||
}, | ||
"metadataPath": { | ||
"type": "string", | ||
"description": "Path of the config metadata file.", | ||
"default": "./component.config.metadata.json" | ||
}, | ||
"migrationMetadataName": { | ||
"type": "string", | ||
"description": "Migration metadata name override, if not provided will use the one with the highest version following the pattern." | ||
}, | ||
"toVersion": { | ||
"type": "string", | ||
"description": "Override the version to consider as the latest one for metadata comparison. If not provided, the latest migration metadata file will be considered as the latest version number." | ||
}, | ||
"fromVersion": { | ||
"type": "string", | ||
"description": "Override the previous version number to use. If not provided, it will be the latest package available from npm < toVersion and matching the granularity." | ||
} | ||
}, | ||
"additionalProperties": false, | ||
"required": ["migrationMetadataFolder"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { MigrationMetadataCheckBuilderOptions } from '@o3r/extractors'; | ||
|
||
/** Migration metadata check builder schema */ | ||
export interface ConfigMigrationMetadataCheckBuilderSchema extends MigrationMetadataCheckBuilderOptions { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './metadata-comparator.interface'; | ||
export * from './metadata-comparison.helper'; | ||
export * from './metadata-files.helper'; |
76 changes: 76 additions & 0 deletions
76
packages/@o3r/extractors/src/core/comparator/metadata-comparator.interface.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import type { JsonObject } from '@angular-devkit/core'; | ||
import type { SupportedPackageManagers } from '@o3r/schematics'; | ||
|
||
/** | ||
* Interface of the comparator used to compare 2 different versions of the same metadata file. | ||
*/ | ||
export interface MetadataComparator<T> { | ||
/** | ||
* Get an array of metadata items to parse a metadata file content. | ||
* @param content Content of a metadata file | ||
*/ | ||
getArray(content: any): T[]; | ||
|
||
/** | ||
* Get a description of a metadata item. | ||
* @param item Metadata item | ||
*/ | ||
getName(item: T): string; | ||
|
||
/** | ||
* Compares 2 metadata items. | ||
* @param item1 Metadata item | ||
* @param item2 Metadata item to compare with | ||
*/ | ||
isSame(item1: T, item2: T): boolean; | ||
|
||
/** | ||
* Returns true if a migration item matches a metadata item. | ||
* @param metadataItem Metadata item | ||
* @param migrationItem Migration item | ||
*/ | ||
isMigrationDataMatch(metadataItem: T, migrationItem: any): boolean; | ||
} | ||
|
||
/** | ||
* Migration item used to document a migration of a config, localization or styling metadata. | ||
*/ | ||
export interface MigrationData<T> { | ||
/** Metadata type */ | ||
contentType: 'CONFIG' | 'LOCALIZATION' | 'STYLING'; | ||
|
||
/** Previous metadata value */ | ||
before: T; | ||
|
||
/** New metadata value */ | ||
after: T; | ||
} | ||
|
||
/** | ||
* Generic metadata builder options | ||
*/ | ||
export interface MigrationMetadataCheckBuilderOptions extends JsonObject { | ||
/** Path to the folder containing the migration metadata. */ | ||
migrationMetadataFolder: string; | ||
|
||
/** Granularity of the migration check. */ | ||
granularity: 'major' | 'minor'; | ||
|
||
/** Whether breaking changes are allowed.*/ | ||
allowBreakingChanges: boolean; | ||
|
||
/** Override of the package manager, otherwise it will be determined from the project. */ | ||
packageManager: SupportedPackageManagers; | ||
|
||
/** Path of the metadata file to check */ | ||
metadataPath: string; | ||
|
||
/** Migration metadata name override, if not provided will use the one with the highest version following the pattern. */ | ||
migrationMetadataName: string; | ||
|
||
/** Override the version to consider as the latest one for metadata comparison. If not provided, the latest migration metadata file will be considered as the latest version number. */ | ||
toVersion: string; | ||
|
||
/** Override the previous version number to use. If not provided, it will be the latest package available from npm < toVersion and matching the granularity. */ | ||
fromVersion: string; | ||
} |
116 changes: 116 additions & 0 deletions
116
packages/@o3r/extractors/src/core/comparator/metadata-comparison.helper.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; | ||
import { getPackageManagerInfo, type PackageManagerOptions, type SupportedPackageManagers, type WorkspaceSchema } from '@o3r/schematics'; | ||
import { existsSync, readFileSync } from 'node:fs'; | ||
import { join } from 'node:path'; | ||
import type { MetadataComparator, MigrationData, MigrationMetadataCheckBuilderOptions } from './metadata-comparator.interface'; | ||
import { getFilesFromRegistry, getLatestMigrationMetadataFile, getLocalMetadataFile, getVersionFromFilename, getVersionRangeFromLatestVersion } from './metadata-files.helper'; | ||
|
||
// TODO : create a bespoke type for the errors? | ||
function checkMetadataFile(lastMetadataFile: any, newMetadataFile: any, migrationData: MigrationData<any>[], isBreakingChangeAllowed: boolean, comparator: MetadataComparator<any>): Error[] { | ||
const errors = [] as Error[]; | ||
const newMetadataArray = comparator.getArray(newMetadataFile); | ||
const lastMetadataArray = comparator.getArray(lastMetadataFile); | ||
for (const lastValue of lastMetadataArray) { | ||
const isInNewMetadata = newMetadataArray.some((newValue) => comparator.isSame(newValue, lastValue)); | ||
if (isInNewMetadata) { | ||
if (!isBreakingChangeAllowed) { | ||
errors.push(new Error(`Property ${comparator.getName(lastValue)} is not present in the new metadata and breaking changes are not allowed`)); | ||
break; | ||
} | ||
|
||
const migrationMetadataValue = migrationData.find((metadata) => comparator.isMigrationDataMatch(lastValue, metadata.after)); | ||
|
||
if (!migrationMetadataValue) { | ||
errors.push(new Error(`Property ${comparator.getName(lastValue)} has been modified but is not documented in the migration document`)); | ||
break; | ||
} | ||
|
||
if (migrationMetadataValue.after) { | ||
const isNewValueInNewMetadata = newMetadataArray.some((newValue) => comparator.isMigrationDataMatch(newValue, migrationMetadataValue.after)); | ||
if (!isNewValueInNewMetadata) { | ||
errors.push(new Error(`Property ${comparator.getName(lastValue)} has been modified but the new property is not present in the new metadata`)); | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
return errors; | ||
} | ||
|
||
/** | ||
* Gets the package manager to use to retrieve the previous package from npm. | ||
* If the project uses npm or yarn it will be npm. | ||
* If the project uses yarn 2+ it will be yarn. | ||
* This is especially important because npm and yarn 1 use the authentication from the .npmrc while yarn 2+ uses the .yarnrc. | ||
* @param options Option to determine the final package manager | ||
*/ | ||
function getPackageManagerForRegistry(options?: PackageManagerOptions): SupportedPackageManagers | undefined { | ||
const packageManagerInfo = getPackageManagerInfo(options); | ||
if (!packageManagerInfo.version) { | ||
return undefined; | ||
} | ||
return packageManagerInfo.name === 'yarn' && !packageManagerInfo.version.match(/^1\./) ? 'yarn' : 'npm'; | ||
} | ||
|
||
/** | ||
* Checks a type of metadata against a previous version of these metadata extracted from a npm package. | ||
* Will return errors if some changes are breaking and they are not allowed, of if the changes are not documented in the file | ||
* provided in options. | ||
* @param options Options for the buidler | ||
* @param context Builder context (from another builder) | ||
* @param comparator Comparator implementation, depends on the type of metadata to check | ||
*/ | ||
export async function checkMetadataBuilder<T>(options: MigrationMetadataCheckBuilderOptions, context: BuilderContext, comparator: MetadataComparator<T>): Promise<BuilderOutput> { | ||
context.reportRunning(); | ||
const angularJsonPath = join(context.workspaceRoot, 'angular.json'); | ||
const angularJson = existsSync(angularJsonPath) ? JSON.parse(readFileSync(angularJsonPath, { encoding: 'utf8' }).toString()) as WorkspaceSchema : undefined; | ||
if (!angularJson) { | ||
context.logger.warn(`angular.json file cannot be found by @o3r/core:${context.builder.builderName} builder. | ||
Detection of package manager runner will fallback on the one used to execute the actual command.`); | ||
} | ||
|
||
const packageManager = getPackageManagerForRegistry({ | ||
workspaceConfig: angularJson, | ||
enforcedNpmManager: options.packageManager | ||
}); | ||
|
||
if (!packageManager) { | ||
return { | ||
success: false, | ||
error: 'The package manager to use could not be determined. Try to override it using the packageManager option.' | ||
}; | ||
} | ||
|
||
// TODO: should be an input? | ||
const migrationFileNamePattern = /MIGRATION-(\d+\.\d+)\.json/; | ||
const migrationFileName = options.migrationMetadataName ?? getLatestMigrationMetadataFile(options.migrationMetadataFolder, migrationFileNamePattern); | ||
|
||
if (!migrationFileName) { | ||
throw new Error(`No migration data could be found in ${options.migrationMetadataFolder}, expected format: ${migrationFileNamePattern}`); | ||
} | ||
|
||
let previousVersion = options.fromVersion; | ||
if (!previousVersion) { | ||
const currentVersion = options.toVersion ?? getVersionFromFilename(migrationFileName, migrationFileNamePattern); | ||
previousVersion = getVersionRangeFromLatestVersion(currentVersion, options.granularity); | ||
} | ||
|
||
const migrationMetadata = getLocalMetadataFile<MigrationData<unknown>[]>(join(options.migrationMetadataFolder, migrationFileName)); | ||
|
||
const packageLocator = `${context.target?.project}@${previousVersion}`; | ||
const previousFile = await getFilesFromRegistry(packageLocator, [options.metadataPath], packageManager); | ||
const newFile = getLocalMetadataFile(options.metadataPath); | ||
|
||
const errors = checkMetadataFile(previousFile[options.metadataPath], newFile, migrationMetadata, options.allowBreakingChanges, comparator); | ||
|
||
if (errors.length) { | ||
return { | ||
success: false, | ||
error: errors.reduce(((message, error) => message.concat(error.message, '\n')), '') | ||
}; | ||
} else { | ||
return { | ||
success: true | ||
}; | ||
} | ||
} |
Oops, something went wrong.