Skip to content

Commit

Permalink
feat: add new builders for metadata migration check
Browse files Browse the repository at this point in the history
  • Loading branch information
cpourcel committed May 31, 2024
1 parent b963e18 commit f234d6b
Show file tree
Hide file tree
Showing 30 changed files with 1,041 additions and 1 deletion.
5 changes: 5 additions & 0 deletions packages/@o3r/components/builders.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
"implementation": "./builders/component-extractor/",
"schema": "./builders/component-extractor/schema.json",
"description": "Extract the component metadata (configuration and class) from an Otter project"
},
"check-config-migration-metadata": {
"implementation": "./builders/metadata-check/",
"schema": "./builders/metadata-check/schema.json",
"description": "Check for component metadata breaking changes"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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[0]?.name === config2.properties[0]?.name;
}

/**
* @inheritdoc
*/
public isMigrationDataMatch(config: ComponentConfigOutput, migrationData: MigrationConfigData): boolean {
return migrationData.configName === config.name && migrationData.libraryName === config.library
&& (!migrationData.propertyName || config.properties[0]?.name === migrationData.propertyName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './config-metadata-comparison.helper';
196 changes: 196 additions & 0 deletions packages/@o3r/components/builders/metadata-check/index.it.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/**
* Test environment exported by O3rEnvironment, must be first line of the file
* @jest-environment @o3r/test-helpers/jest-environment
* @jest-environment-o3r-app-folder test-app-components-metadata-check
*/
const o3rEnvironment = globalThis.o3rEnvironment;

import { getPackageManager } from '@o3r/schematics';
import {
getDefaultExecSyncOptions,
packageManagerAdd,
packageManagerExec,
packageManagerInfo,
packageManagerPublish,
packageManagerVersion
} from '@o3r/test-helpers';
import type { ExecSyncOptions } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { coerce, inc } from 'semver';

const migrationDataFileName = 'MIGRATION-1.3.json';
const metadataFileName = 'component.config.metadata.json';
const baseVersion = '1.2.0';

const migrationData = {
version: '1.3.0',
changes: [
{
contentType: 'CONFIG',
before: {
libraryName: '@o3r/test',
configName: 'RenamedTestConfig',
propertyName: 'Property1'
},
after: {
libraryName: '@o3r/test',
configName: 'NewTestConfig',
propertyName: 'Property3'
}
},
{
contentType: 'CONFIG',
before: {
libraryName: '@o3r/test',
configName: 'RemovedTestConfig'
}
}
]
};

const previousConfigurationMetadata = [
{
library: '@o3r/test',
description: 'Test renamed config',
name: 'RenamedTestConfig',
properties: [
{
description: '',
name: 'Property1',
type: 'string',
value: 'test'
},
{
description: '',
name: 'Property2',
type: 'boolean',
value: false
}
]
},
{
library: '@o3r/test',
description: 'Test removed config',
name: 'RemovedTestConfig',
properties: [
{
description: '',
name: 'Property',
type: 'boolean',
value: true
}
]
}
];

const newConfigurationMetadata = [
{
library: '@o3r/test',
description: 'Test renamed config',
name: 'NewTestConfig',
properties: [
{
description: '',
name: 'Property1',
type: 'string',
value: 'test'
},
{
description: '',
name: 'Property2',
type: 'boolean',
value: false
}
]
}
];

function writeFileAsJSON(path: string, content: object): void {
writeFileSync(path, JSON.stringify(content), { encoding: 'utf8' });
}

function getLatestPackageVersion(packageRef: string, execOptions: ExecSyncOptions): string | undefined {
try {
const result = packageManagerInfo(packageRef, ['--json'], execOptions);
const latestVersion = JSON.parse(result)['dist-tags']?.latest;
return latestVersion && coerce(latestVersion).toString() || undefined;
} catch (error) {
return undefined;
}
}

describe('check metadata migration', () => {
test('check metadata changes', () => {
const { workspacePath, projectName, projectPath, o3rVersion } = o3rEnvironment.testEnvironment;
const execAppOptions = { ...getDefaultExecSyncOptions(), cwd: projectPath };
const execAppOptionsWorkspace = { ...getDefaultExecSyncOptions(), cwd: workspacePath };
packageManagerAdd(`@o3r/components@${o3rVersion}`, execAppOptionsWorkspace);
packageManagerAdd(`@o3r/extractors@${o3rVersion}`, execAppOptionsWorkspace);
const packageJsonPath = join(projectPath, 'package.json');
const angularJsonPath = join(workspacePath, 'angular.json');
const metadataPath = join(projectPath, metadataFileName);
const migrationDataPath = join(projectPath, migrationDataFileName);

// Add builder options
const angularJson = existsSync(angularJsonPath) ? JSON.parse(readFileSync(angularJsonPath, { encoding: 'utf8' }).toString()) : undefined;
const builderConfig = {
builder: '@o3r/components:check-config-migration-metadata',
options: {
granularity: 'minor',
metadataPath: './component.config.metadata.json',
migrationDataPath: './apps/test-app/MIGRATION*.json'
}
};
angularJson.projects[projectName].architect['check-metadata'] = builderConfig;
writeFileAsJSON(angularJsonPath, angularJson);

// Add scope to project for registry management
let packageJson = existsSync(packageJsonPath) ? JSON.parse(readFileSync(packageJsonPath, { encoding: 'utf8' }).toString()) : undefined;
const packageName = `@o3r/${packageJson.name}`;
packageJson = {
...packageJson,
name: packageName,
private: false
};
writeFileAsJSON(packageJsonPath, packageJson);

// Set old metadata and publish to registry
writeFileAsJSON(metadataPath, previousConfigurationMetadata);

const bumpedVersion = inc(getLatestPackageVersion(packageName, execAppOptionsWorkspace) || baseVersion, 'patch');

const args = getPackageManager() === 'yarn' ? [] : ['--no-git-tag-version', '-f'];
packageManagerVersion(bumpedVersion, args, execAppOptions);
packageManagerPublish(execAppOptions);

// Override with new metadata for comparison
writeFileAsJSON(metadataPath, newConfigurationMetadata);

// Add migration data file
writeFileAsJSON(migrationDataPath, migrationData);

try {
packageManagerExec({ script: 'ng', args: ['run', `${projectName}:check-metadata`] }, execAppOptionsWorkspace);
throw new Error('Should have thrown');
} catch (error: any) {
/* eslint-disable jest/no-conditional-expect */
expect(error.message).not.toBe('Should have thrown');

// eslint-disable-next-line no-control-regex
const ansiFormatting = /\[(\d+;)?\d+m|\x1B/g;
const errorLine = /^Property.*/;
const errors = (error.message.split('\n') as string[])
.map((s) => s.replaceAll(ansiFormatting, ''))
.filter((s) => s.match(errorLine));

// Errors are duplicated in the output so we need to unify them
const uniqueErrorsNumber = new Set(errors).size;

expect(uniqueErrorsNumber).toBe(2);
expect(errors).toContain('Property @o3r/test#RenamedTestConfig Property1 has been modified but the new property is not present in the new metadata');
expect(errors).toContain('Property @o3r/test#RenamedTestConfig Property2 has been modified but is not documented in the migration document');
/* eslint-enable jest/no-conditional-expect */
}
});
});
8 changes: 8 additions & 0 deletions packages/@o3r/components/builders/metadata-check/index.ts
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());
}));
37 changes: 37 additions & 0 deletions packages/@o3r/components/builders/metadata-check/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"$id": "ConfigMigrationMetadataCheckBuilderSchema",
"title": "Check config migration metadata builder",
"description": "",
"properties": {
"migrationDataPath": {
"type": ["string", "array"],
"items": {
"type": "string"
},
"description": "Glob of the migration files to use."
},
"granularity": {
"type": "string",
"description": "Granularity of the migration check.",
"default": "minor"
},
"allowBreakingChanges": {
"type": "boolean",
"description": "Are breaking changes allowed.",
"default": false
},
"packageManager": {
"type": "string",
"description": "Override of the package manager, otherwise it will be computed from the project setup."
},
"metadataPath": {
"type": "string",
"description": "Path of the config metadata file.",
"default": "./component.config.metadata.json"
}
},
"additionalProperties": false,
"required": ["migrationDataPath"]
}
5 changes: 5 additions & 0 deletions packages/@o3r/components/builders/metadata-check/schema.ts
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 {
}
2 changes: 2 additions & 0 deletions packages/@o3r/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
"@stylistic/eslint-plugin-ts": "^1.5.4",
"@types/jest": "~29.5.2",
"@types/node": "^20.0.0",
"@types/semver": "^7.3.13",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"chokidar": "^3.5.2",
Expand All @@ -166,6 +167,7 @@
"nx": "~18.3.0",
"pid-from-port": "^1.1.3",
"rxjs": "^7.8.1",
"semver": "^7.5.2",
"ts-jest": "~29.1.2",
"ts-node": "~10.9.2",
"typescript": "~5.4.2",
Expand Down
Loading

0 comments on commit f234d6b

Please sign in to comment.