Skip to content

Commit

Permalink
feat(config): generate default config with readonly and as const (#2542)
Browse files Browse the repository at this point in the history
## Related issues
 🐛 Fix #2530
  • Loading branch information
matthieu-crouzet authored Dec 3, 2024
2 parents 01ce403 + cea1241 commit 18786c6
Show file tree
Hide file tree
Showing 12 changed files with 462 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export interface ComponentReplacementPresConfig extends Configuration {
}

/** Default config of component replacement base component */
export const COMPONENT_REPLACEMENT_PRES_DEFAULT_CONFIG: ComponentReplacementPresConfig = {
export const COMPONENT_REPLACEMENT_PRES_DEFAULT_CONFIG: Readonly<ComponentReplacementPresConfig> = {
datePickerCustomKey: ''
};
} as const;

/** Identifier for component replacement base component, used in the configuration store */
export const COMPONENT_REPLACEMENT_PRES_CONFIG_ID = computeItemIdentifier('ComponentReplacementPresConfig', 'showcase');
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,36 @@ export class ComponentConfigExtractor {
if (ts.isVariableDeclaration(declarationNode)) {
let isConfigImplementation = false;
declarationNode.forEachChild((vNode) => {
if (ts.isTypeReferenceNode(vNode) && configurationInformationWrapper.configurationInformation!.name === vNode.getText(this.source)) {
if (
ts.isTypeReferenceNode(vNode)
&& ts.isIdentifier(vNode.typeName)
&& (
vNode.typeName.escapedText.toString() === configurationInformationWrapper.configurationInformation!.name
|| (
vNode.typeName.escapedText.toString() === 'Readonly'
&& vNode.typeArguments?.[0]
&& ts.isTypeReferenceNode(vNode.typeArguments?.[0])
&& ts.isIdentifier(vNode.typeArguments?.[0].typeName)
&& vNode.typeArguments[0].typeName.escapedText.toString() === configurationInformationWrapper.configurationInformation!.name
)
)
) {
isConfigImplementation = true;
} else if (isConfigImplementation && ts.isObjectLiteralExpression(vNode)) {
vNode.forEachChild((propertyNode) => {
} else if (
isConfigImplementation
&& (
ts.isObjectLiteralExpression(vNode)
|| (
ts.isAsExpression(vNode)
&& ts.isTypeReferenceNode(vNode.type)
&& ts.isIdentifier(vNode.type.typeName)
&& vNode.type.typeName.escapedText.toString() === 'const'
&& ts.isObjectLiteralExpression(vNode.expression)
)
)
) {
const objectExpression = ts.isObjectLiteralExpression(vNode) ? vNode : vNode.expression;
objectExpression.forEachChild((propertyNode) => {
if (ts.isPropertyAssignment(propertyNode)) {
let identifier: string | undefined;
propertyNode.forEachChild((fieldNode) => {
Expand Down
5 changes: 5 additions & 0 deletions packages/@o3r/configuration/migration.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
"version": "10.3.0-alpha.0",
"description": "Updates of @o3r/configuration to v10.3.*",
"factory": "./schematics/ng-update/v10-3/index#updateV10_3"
},
"migration-v11_6": {
"version": "11.6.0-prerelease.0",
"description": "Updates of @o3r/configuration to v11.6.*",
"factory": "./schematics/ng-update/v11-6/index#updateV116"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import {computeItemIdentifier} from '@o3r/core';

export interface <%=componentConfig%> extends Configuration {}

export const <%=configKey%>_DEFAULT_CONFIG: <%=componentConfig%> = {};
export const <%=configKey%>_DEFAULT_CONFIG: Readonly<<%=componentConfig%>> = {} as const;

export const <%=configKey%>_CONFIG_ID = computeItemIdentifier('<%=componentConfig%>', '<%=projectName%>');
167 changes: 167 additions & 0 deletions packages/@o3r/configuration/schematics/ng-update/v11-6/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
Tree,
} from '@angular-devkit/schematics';
import {
SchematicTestRunner,
type UnitTestTree,
} from '@angular-devkit/schematics/testing';

const migrationPath = path.join(__dirname, '..', '..', '..', 'migration.json');

const notMigrated = `
import {
computeItemIdentifier,
} from '@o3r/core';
import type {
Configuration,
NestedConfiguration,
} from '@o3r/core';
/** Configuration of a destination */
export interface DestinationConfiguration extends NestedConfiguration {
/**
* Name of the destination's city
* @o3rRequired
*/
cityName: string;
/**
* Is the destination available
*/
available: boolean;
}
/**
* Component configuration example
* @o3rCategories localCategory Local category
*/
export interface ConfigurationPresConfig extends Configuration {
/**
* Default date selected compare to today
* @o3rCategory localCategory
*/
inXDays: number;
/**
* Proposed destinations
* @o3rWidget DESTINATION_ARRAY
* @o3rWidgetParam minItems 1
* @o3rWidgetParam allDestinationsDifferent true
* @o3rWidgetParam atLeastOneDestinationAvailable true
* @o3rWidgetParam destinationPattern "[A-Z][a-zA-Z-' ]+"
*/
destinations: DestinationConfiguration[];
/**
* Propose round trip
* @o3rCategory localCategory
*/
shouldProposeRoundTrip: boolean;
}
export const CONFIGURATION_PRES_DEFAULT_CONFIG: ConfigurationPresConfig = {
inXDays: 7,
destinations: [
{ cityName: 'London', available: true },
{ cityName: 'Paris', available: true },
{ cityName: 'New-York', available: false }
],
shouldProposeRoundTrip: false
};
export const CONFIGURATION_PRES_CONFIG_ID = computeItemIdentifier('ConfigurationPresConfig', 'showcase');
`;

const migrated = `
import {
computeItemIdentifier,
} from '@o3r/core';
import type {
Configuration,
NestedConfiguration,
} from '@o3r/core';
/** Configuration of a destination */
export interface DestinationConfiguration extends NestedConfiguration {
/**
* Name of the destination's city
* @o3rRequired
*/
cityName: string;
/**
* Is the destination available
*/
available: boolean;
}
/**
* Component configuration example
* @o3rCategories localCategory Local category
*/
export interface ConfigurationPresConfig extends Configuration {
/**
* Default date selected compare to today
* @o3rCategory localCategory
*/
inXDays: number;
/**
* Proposed destinations
* @o3rWidget DESTINATION_ARRAY
* @o3rWidgetParam minItems 1
* @o3rWidgetParam allDestinationsDifferent true
* @o3rWidgetParam atLeastOneDestinationAvailable true
* @o3rWidgetParam destinationPattern "[A-Z][a-zA-Z-' ]+"
*/
destinations: DestinationConfiguration[];
/**
* Propose round trip
* @o3rCategory localCategory
*/
shouldProposeRoundTrip: boolean;
}
export const CONFIGURATION_PRES_DEFAULT_CONFIG: Readonly<ConfigurationPresConfig> = {
inXDays: 7,
destinations: [
{ cityName: 'London', available: true },
{ cityName: 'Paris', available: true },
{ cityName: 'New-York', available: false }
],
shouldProposeRoundTrip: false
} as const;
export const CONFIGURATION_PRES_CONFIG_ID = computeItemIdentifier('ConfigurationPresConfig', 'showcase');
`;

describe('Update', () => {
let initialTree: Tree;
let runner: SchematicTestRunner;
beforeEach(() => {
initialTree = Tree.empty();
initialTree.create('angular.json', fs.readFileSync(path.resolve(__dirname, '..', '..', '..', 'testing', 'mocks', 'angular.mocks.json')));
initialTree.create('package.json', fs.readFileSync(path.resolve(__dirname, '..', '..', '..', 'testing', 'mocks', 'package.mocks.json')));
initialTree.create('.eslintrc.json', fs.readFileSync(path.resolve(__dirname, '..', '..', '..', 'testing', 'mocks', '__dot__eslintrc.mocks.json')));
runner = new SchematicTestRunner('schematics', migrationPath);
});

describe('Update v11.6', () => {
let tree: UnitTestTree;
const notMigratedPath = 'src/components/not-migrated.config.ts';
const migratedPath = 'src/components/migrated.config.ts';

beforeEach(async () => {
initialTree.create(notMigratedPath, notMigrated);
initialTree.create(migratedPath, migrated);
tree = await runner.runSchematic('migration-v11_6', {}, initialTree);
});

it('should migrate the not migrated file', () => {
const newText = tree.readText(notMigratedPath);
expect(newText).not.toEqual(notMigrated);
expect(newText).toEqual(migrated);
});

it('should not change the file already migrated', () => {
expect(tree.readText(migratedPath)).toEqual(migrated);
});
});
});
28 changes: 28 additions & 0 deletions packages/@o3r/configuration/schematics/ng-update/v11-6/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type {
Rule,
Tree,
} from '@angular-devkit/schematics';
import {
createSchematicWithMetricsIfInstalled,
findFilesInTree,
} from '@o3r/schematics';

const regexp = /DEFAULT_CONFIG\s*:\s*(?:Readonly<)?([A-Z][\w]*)(?:>)?\s*=\s*({[^;]+})\s*(as\s*const)?\s*;/g;

function updateV116Fn(): Rule {
return (tree: Tree) => {
const files = findFilesInTree(tree.getDir(''), (filePath) => /config\.ts$/.test(filePath));
files.forEach(({ content, path }) => {
const str = content.toString();
const newContent = str.replaceAll(regexp, 'DEFAULT_CONFIG: Readonly<$1> = $2 as const;');
if (newContent !== str) {
tree.overwrite(path, newContent);
}
});
};
}

/**
* Update of Otter configuration V11.6
*/
export const updateV116 = createSchematicWithMetricsIfInstalled(updateV116Fn);
5 changes: 5 additions & 0 deletions packages/@o3r/localization/migration.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
"version": "10.0.0-alpha.0",
"description": "Updates of @o3r/localization to v10.0.*",
"factory": "./schematics/ng-update/v10-0/index#updateV10_0"
},
"migration-v11_6": {
"version": "11.6.0-prerelease.0",
"description": "Updates of @o3r/localization to v11.6.*",
"factory": "./schematics/ng-update/v11-6/index#updateV116"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe('Add Localization', () => {
export interface TestTranslation extends Translation {}
export const translations: TestTranslation = {}
export const translations: Readonly<TestTranslation> = {} as const;
`);
initialTree.create('.eslintrc.json', fs.readFileSync(path.resolve(__dirname, '..', '..', 'testing', 'mocks', '__dot__eslintrc.mocks.json')));
});
Expand Down
75 changes: 58 additions & 17 deletions packages/@o3r/localization/schematics/add-localization-key/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,27 +195,68 @@ export function ngAddLocalizationKeyFn(options: NgAddLocalizationKeySchematicsSc
}
if (
ts.isVariableDeclaration(node)
&& node.type && ts.isTypeReferenceNode(node.type)
&& ts.isIdentifier(node.type.typeName)
&& node.type.typeName.escapedText.toString() === translationsVariableType
&& node.type
&& (
(
ts.isTypeReferenceNode(node.type)
&& ts.isIdentifier(node.type.typeName)
&& node.type.typeName.escapedText.toString() === translationsVariableType
)
|| (
ts.isTypeReferenceNode(node.type)
&& ts.isIdentifier(node.type.typeName)
&& node.type.typeName.escapedText.toString() === 'Readonly'
&& node.type.typeArguments?.[0]
&& ts.isTypeReferenceNode(node.type.typeArguments[0])
&& ts.isIdentifier(node.type.typeArguments[0].typeName)
&& node.type.typeArguments[0].typeName.escapedText.toString() === translationsVariableType
)
)
&& node.initializer
&& ts.isObjectLiteralExpression(node.initializer)
) {
return factory.updateVariableDeclaration(
node,
node.name,
node.exclamationToken,
node.type,
factory.updateObjectLiteralExpression(
node.initializer,
node.initializer.properties.concat(
factory.createPropertyAssignment(
factory.createIdentifier(properties.keyName),
factory.createStringLiteral(properties.keyValue, true)
if (ts.isObjectLiteralExpression(node.initializer)) {
return factory.updateVariableDeclaration(
node,
node.name,
node.exclamationToken,
node.type,
factory.updateObjectLiteralExpression(
node.initializer,
node.initializer.properties.concat(
factory.createPropertyAssignment(
factory.createIdentifier(properties.keyName),
factory.createStringLiteral(properties.keyValue, true)
)
)
)
)
);
);
} else if (
ts.isAsExpression(node.initializer)
&& ts.isTypeReferenceNode(node.initializer.type)
&& ts.isIdentifier(node.initializer.type.typeName)
&& node.initializer.type.typeName.escapedText.toString() === 'const'
&& ts.isObjectLiteralExpression(node.initializer.expression)
) {
return factory.updateVariableDeclaration(
node,
node.name,
node.exclamationToken,
node.type,
factory.updateAsExpression(
node.initializer,
factory.updateObjectLiteralExpression(
node.initializer.expression,
node.initializer.expression.properties.concat(
factory.createPropertyAssignment(
factory.createIdentifier(properties.keyName),
factory.createStringLiteral(properties.keyValue, true)
)
)
),
node.initializer.type
)
);
}
}
return ts.visitEachChild(node, visit, ctx);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import type {Translation} from '@o3r/core';

export interface <%= componentTranslation %> extends Translation {}

export const translations: <%= componentTranslation %> = {};
export const translations: Readonly<<%= componentTranslation %>> = {} as const;
Loading

0 comments on commit 18786c6

Please sign in to comment.