Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migration schematics #1744

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/@ama-sdk/schematics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,18 @@ yarn schematics @ama-sdk/schematics:typescript-core --generator-key example-sdk
> The values provided by the parameter `--global-property` will actually be merged with the values of `globalProperty` from
> `openapitools.json` (rather than override them like the other properties).

### Migration

To help to apply changes on the Shell part of the SDK repository, a `migrate` schematic is exposed:

```shell
yarn schematics @ama-sdk/schematics:migrate --from 10.0.0 [--to 11.0.0]
```

> [!NOTE]
> - The `--from` parameter is mandatory to provide the version of the original `@ama-sdk/schematics` package from which the rules should be run.
> - The *optional* `--to` parameter allows to indicate a version until which the rules should be run
kpanot marked this conversation as resolved.
Show resolved Hide resolved

### Debug

The OpenApi generator extracts an enhanced JSON data model from the specification YAML and uses this data model to feed the templates to generate the code.
Expand Down
5 changes: 5 additions & 0 deletions packages/@ama-sdk/schematics/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
"schema": "./schematics/ng-add/schema.json",
"aliases": ["install", "i"]
},
"migrate": {
"description": "Execute migration scripts between 2 versions.",
"factory": "./schematics/migrate/index#migrate",
"schema": "./schematics/migrate/schema.json"
},
"typescript-mock": {
"description": "Generate an api mock into the project.",
"factory": "./schematics/typescript/mock/index#ngGenerateMock",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Tree } from '@angular-devkit/schematics';

/**
* Determine of the SDK targeted is typescript based
kpanot marked this conversation as resolved.
Show resolved Hide resolved
* @param tree Schematic Tree
* @param pathInTree Path to the SDK in the tree
**/
export const isTypescriptSdk = (tree: Tree, pathInTree = '/') => {
return tree.getDir(pathInTree)
.subfiles
.some((filePath) => /tsconfig[^/\\]*\.json$/.test(filePath));
};
39 changes: 39 additions & 0 deletions packages/@ama-sdk/schematics/schematics/migrate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Rule } from '@angular-devkit/schematics';
import { MigrateSchematicsSchemaOptions } from './schema';
import { getMigrationRuleRunner, getWorkspaceConfig, type MigrationRulesMap } from '@o3r/schematics';
import { resolve } from 'node:path';
import { gt, minVersion } from 'semver';
import { isTypescriptSdk } from '../helpers/is-typescript-project';

const tsMigrationMap: MigrationRulesMap = {

};
/**
* Facilitate the migration of a version to another by the run of migration rules
* @param options
*/
function migrateFn(options: MigrateSchematicsSchemaOptions): Rule {

const currentVersion = JSON.parse(require(resolve(__dirname, '..', '..', 'package.json'))).version;
const to: string = options.to || currentVersion;
const minimumVersion = minVersion(to);

return (tree, context) => {
if (minimumVersion && gt(minimumVersion, currentVersion)) {
context.logger.warn(`The specified range "${to}" has a minimum supported version higher than the current version of @ama-sdk/schematics (${currentVersion}).` +
' The migration may not have any effect.');
}
const workingDirectory = options?.projectName && getWorkspaceConfig(tree)?.projects[options.projectName]?.root || '/';
const runMigrateSchematic = isTypescriptSdk(tree, workingDirectory) ? getMigrationRuleRunner(tsMigrationMap, { logger: context.logger }) : undefined;
return runMigrateSchematic?.({from: options.from, to});
};
}

/**
* Facilitate the migration of a version to another by the run of migration rules
* @param options
*/
export const migrate = (options: MigrateSchematicsSchemaOptions) => async () => {
const { createSchematicWithMetricsIfInstalled } = await import('@o3r/schematics');
return createSchematicWithMetricsIfInstalled(migrateFn)(options);
};
28 changes: 28 additions & 0 deletions packages/@ama-sdk/schematics/schematics/migrate/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "MigrateSchematicsSchema",
"title": "Execute migration scripts between 2 versions",
"description": "Schematics to migrate from a specific version to another one",
"properties": {
"from": {
"type": "string",
"description": "Starting version from which executing the migration scripts",
kpanot marked this conversation as resolved.
Show resolved Hide resolved
"x-prompt": "What was the original version before migration?"
},
"to": {
"type": "string",
"description": "Version of the package to migrate to (will use the current version if not specified)"
},
"projectName": {
"type": "string",
"description": "Project name (in case it is applied to a module from an Angular Project)",
"$default": {
"$source": "projectName"
}
}
},
"additionalProperties": true,
"required": [
"from"
]
}
9 changes: 9 additions & 0 deletions packages/@ama-sdk/schematics/schematics/migrate/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** Schematic Option */
export interface MigrateSchematicsSchemaOptions {
/** Starting version from which executing the migration scripts */
kpanot marked this conversation as resolved.
Show resolved Hide resolved
from: string;
/** Version of the package to migrate to (will use the current version if not specified) */
to?: string;
/** Project name */
projectName?: string | undefined;
}
11 changes: 2 additions & 9 deletions packages/@ama-sdk/schematics/schematics/ng-update/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable camelcase */

import { Rule, Tree } from '@angular-devkit/schematics';
import type { Rule } from '@angular-devkit/schematics';
import { updateV10_0 as tsUpdateV10_0 } from './typescript';

/**
* Determine if the script is run in a Typescript SDK
* @param tree
*/
const isTypescriptSdk = (tree: Tree) => {
return tree.exists('/tsconfig.json');
};
import { isTypescriptSdk } from '../helpers/is-typescript-project';

/**
* update of Otter library V10.0
Expand Down
1 change: 1 addition & 0 deletions packages/@o3r/schematics/src/utility/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export * from './update-imports';
export * from './update-pipes';
export * from './builder';
export * from './wrapper';
export * from './migration/migration';
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { callRule, Tree } from '@angular-devkit/schematics';
import { getMigrationRuleRunner } from './migration';
import { firstValueFrom } from 'rxjs';

describe('getMigrationRuleRunner', () => {

it('should execute rule when in the range', async () => {
const spy = jest.fn();
const runnner = getMigrationRuleRunner({
// eslint-disable-next-line @typescript-eslint/naming-convention
'10.0.*': spy
});

const rules = runnner({ from: '9.0.0', to: '10.1.0' });

await firstValueFrom(callRule(rules, Tree.empty(), {} as any));

expect(spy).toHaveBeenCalled();
});

it('should not execute rule when not in the range', async () => {
const spy = jest.fn();
const spy2 = jest.fn();
const runnner = getMigrationRuleRunner({
// eslint-disable-next-line @typescript-eslint/naming-convention
'8.*': spy2,
// eslint-disable-next-line @typescript-eslint/naming-convention
'10.0.*': spy
});

const rules = runnner({ from: '8.0.0', to: '9.1.0' });
await firstValueFrom(callRule(rules, Tree.empty(), {} as any));

expect(spy).not.toHaveBeenCalled();
expect(spy2).toHaveBeenCalled();
});

it('should execute rule when in the range without limit', async () => {
const spy = jest.fn();
const runnner = getMigrationRuleRunner({
// eslint-disable-next-line @typescript-eslint/naming-convention
'10.0.*': spy
});

const rules = runnner({ from: '9.0.0' });
await firstValueFrom(callRule(rules, Tree.empty(), {} as any));

expect(spy).toHaveBeenCalled();
});
});
60 changes: 60 additions & 0 deletions packages/@o3r/schematics/src/utility/migration/migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { LoggerApi } from '@angular-devkit/core/src/logger';
import { chain, type Rule } from '@angular-devkit/schematics';
import { intersects, Range, validRange } from 'semver';

/** Create the migration */
interface MigrateRuleRunnerOptions {
/** The original version from which to execute the rules */
from: string;

/**
* The last version for which to execute the rule.
* If not specified, all the versions from the "from" parameter will be considered.
*/
to?: string;
}

/** Mapping of rules to apply to it's specific range */
kpanot marked this conversation as resolved.
Show resolved Hide resolved
export interface MigrationRulesMap {
/** Rules to apply to a specific semver range */
[range: string]: Rule | Rule[];
cpaulve-1A marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Option for migration rule runner factory
*/
interface MigrationRuleRunnerOptions {
/** Logger */
logger?: LoggerApi;
}

/**
* Generate the Migration Rule Schematic runner to execute rules according to the range
* @param rulesMapping Mapping of rules to execute based on its semver range
* @param options Additional options
*/
export function getMigrationRuleRunner(rulesMapping: MigrationRulesMap, options?: MigrationRuleRunnerOptions) {
const rangeMapping = Object.entries(rulesMapping)
.reduce((acc, [range, rule]) => {
const checkedRange = validRange(range);
if (!checkedRange) {
options?.logger?.warn(`The range "${range}" is invalid and will be ignored in the Migration rule`);
} else {
acc.push([checkedRange, Array.isArray(rule) ? rule : [rule]]);
}
return acc;
}, [] as [string, Rule[]][]);

/**
* Migration rule runner
* @param config Provide information regarding the range to match
*/
return (config: MigrateRuleRunnerOptions): Rule => {
const fromToRange = new Range(config.to ? `>${config.from} <=${config.to}` : `>${config.from}`);
return chain(
rangeMapping
.filter(([range]) => intersects(range, fromToRange))
.map(([_, rules]) => chain(rules))
);
};
}