Skip to content

Commit

Permalink
feat: migration schematics
Browse files Browse the repository at this point in the history
  • Loading branch information
kpanot committed May 16, 2024
1 parent 5fbacc5 commit a43848a
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 9 deletions.
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
### 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
* @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",
"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 */
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';
50 changes: 50 additions & 0 deletions packages/@o3r/schematics/src/utility/migration/migration.spec.ts
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 */
export interface MigrationRulesMap {
/** Rules to apply to a specific semver range */
[range: string]: Rule | Rule[];
}

/**
* 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))
);
};
}

0 comments on commit a43848a

Please sign in to comment.