diff --git a/apps/showcase/src/components/showcase/component-replacement/component-replacement-pres.config.ts b/apps/showcase/src/components/showcase/component-replacement/component-replacement-pres.config.ts index 0bffce7e2d..ed1e3d20d5 100644 --- a/apps/showcase/src/components/showcase/component-replacement/component-replacement-pres.config.ts +++ b/apps/showcase/src/components/showcase/component-replacement/component-replacement-pres.config.ts @@ -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 = { datePickerCustomKey: '' -}; +} as const; /** Identifier for component replacement base component, used in the configuration store */ export const COMPONENT_REPLACEMENT_PRES_CONFIG_ID = computeItemIdentifier('ComponentReplacementPresConfig', 'showcase'); diff --git a/packages/@o3r/components/builders/component-extractor/helpers/component/component-config.extractor.ts b/packages/@o3r/components/builders/component-extractor/helpers/component/component-config.extractor.ts index bb1779cb95..daf9416ead 100644 --- a/packages/@o3r/components/builders/component-extractor/helpers/component/component-config.extractor.ts +++ b/packages/@o3r/components/builders/component-extractor/helpers/component/component-config.extractor.ts @@ -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) => { diff --git a/packages/@o3r/configuration/migration.json b/packages/@o3r/configuration/migration.json index 1d2bbe100a..069566a2d0 100644 --- a/packages/@o3r/configuration/migration.json +++ b/packages/@o3r/configuration/migration.json @@ -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" } } } diff --git a/packages/@o3r/configuration/schematics/configuration-to-component/templates/__name__.config.ts.template b/packages/@o3r/configuration/schematics/configuration-to-component/templates/__name__.config.ts.template index 5a9ee47740..bdc11f8852 100644 --- a/packages/@o3r/configuration/schematics/configuration-to-component/templates/__name__.config.ts.template +++ b/packages/@o3r/configuration/schematics/configuration-to-component/templates/__name__.config.ts.template @@ -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%>'); diff --git a/packages/@o3r/configuration/schematics/ng-update/v11-6/index.spec.ts b/packages/@o3r/configuration/schematics/ng-update/v11-6/index.spec.ts new file mode 100644 index 0000000000..e62d1f4fc8 --- /dev/null +++ b/packages/@o3r/configuration/schematics/ng-update/v11-6/index.spec.ts @@ -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 = { + 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); + }); + }); +}); diff --git a/packages/@o3r/configuration/schematics/ng-update/v11-6/index.ts b/packages/@o3r/configuration/schematics/ng-update/v11-6/index.ts new file mode 100644 index 0000000000..0eb4fffaa3 --- /dev/null +++ b/packages/@o3r/configuration/schematics/ng-update/v11-6/index.ts @@ -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); diff --git a/packages/@o3r/localization/migration.json b/packages/@o3r/localization/migration.json index 16affb1478..87fdc96d1e 100644 --- a/packages/@o3r/localization/migration.json +++ b/packages/@o3r/localization/migration.json @@ -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" } } } diff --git a/packages/@o3r/localization/schematics/add-localization-key/index.spec.ts b/packages/@o3r/localization/schematics/add-localization-key/index.spec.ts index 25819d8603..ebd8ddfd7c 100644 --- a/packages/@o3r/localization/schematics/add-localization-key/index.spec.ts +++ b/packages/@o3r/localization/schematics/add-localization-key/index.spec.ts @@ -90,7 +90,7 @@ describe('Add Localization', () => { export interface TestTranslation extends Translation {} - export const translations: TestTranslation = {} + export const translations: Readonly = {} as const; `); initialTree.create('.eslintrc.json', fs.readFileSync(path.resolve(__dirname, '..', '..', 'testing', 'mocks', '__dot__eslintrc.mocks.json'))); }); diff --git a/packages/@o3r/localization/schematics/add-localization-key/index.ts b/packages/@o3r/localization/schematics/add-localization-key/index.ts index 3dbe606a44..c3e13f1482 100644 --- a/packages/@o3r/localization/schematics/add-localization-key/index.ts +++ b/packages/@o3r/localization/schematics/add-localization-key/index.ts @@ -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); }; diff --git a/packages/@o3r/localization/schematics/localization-to-component/templates/__name__.translation.ts.template b/packages/@o3r/localization/schematics/localization-to-component/templates/__name__.translation.ts.template index ce98cdc963..c92275012f 100644 --- a/packages/@o3r/localization/schematics/localization-to-component/templates/__name__.translation.ts.template +++ b/packages/@o3r/localization/schematics/localization-to-component/templates/__name__.translation.ts.template @@ -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; diff --git a/packages/@o3r/localization/schematics/ng-update/v11-6/index.spec.ts b/packages/@o3r/localization/schematics/ng-update/v11-6/index.spec.ts new file mode 100644 index 0000000000..2688819878 --- /dev/null +++ b/packages/@o3r/localization/schematics/ng-update/v11-6/index.spec.ts @@ -0,0 +1,137 @@ +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 type { + Translation, +} from '@o3r/core'; + +export interface LocalizationPresTranslation extends Translation { + /** + * Key for the message in the speech bubble until a destination is selected + */ + welcome: string; + /** + * Key for the message in the speech bubble when a destination is selected, you can use \`{ cityName }\` to display the name of the city selected + */ + welcomeWithCityName: string; + /** + * Key for the question at the top of the form + */ + question: string; + /** + * Key for the label for the destination input + */ + destinationLabel: string; + /** + * Key for the label for the departure date input + */ + departureLabel: string; + /** + * Key for the placeholder for the destination input + */ + destinationPlaceholder: string; + /** + * Key for the city names' dictionary + */ + cityName: string; +} + +export const translations: LocalizationPresTranslation = { + welcome: 'o3r-localization-pres.welcome', + welcomeWithCityName: 'o3r-localization-pres.welcomeWithCityName', + question: 'o3r-localization-pres.question', + destinationLabel: 'o3r-localization-pres.destinationLabel', + departureLabel: 'o3r-localization-pres.departureLabel', + cityName: 'o3r-localization-pres.cityName', + destinationPlaceholder: 'o3r-localization-pres.destinationPlaceholder' +}; +`; + +const migrated = `import type { + Translation, +} from '@o3r/core'; + +export interface LocalizationPresTranslation extends Translation { + /** + * Key for the message in the speech bubble until a destination is selected + */ + welcome: string; + /** + * Key for the message in the speech bubble when a destination is selected, you can use \`{ cityName }\` to display the name of the city selected + */ + welcomeWithCityName: string; + /** + * Key for the question at the top of the form + */ + question: string; + /** + * Key for the label for the destination input + */ + destinationLabel: string; + /** + * Key for the label for the departure date input + */ + departureLabel: string; + /** + * Key for the placeholder for the destination input + */ + destinationPlaceholder: string; + /** + * Key for the city names' dictionary + */ + cityName: string; +} + +export const translations: Readonly = { + welcome: 'o3r-localization-pres.welcome', + welcomeWithCityName: 'o3r-localization-pres.welcomeWithCityName', + question: 'o3r-localization-pres.question', + destinationLabel: 'o3r-localization-pres.destinationLabel', + departureLabel: 'o3r-localization-pres.departureLabel', + cityName: 'o3r-localization-pres.cityName', + destinationPlaceholder: 'o3r-localization-pres.destinationPlaceholder' +} as const; +`; + +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.translation.ts'; + const migratedPath = 'src/components/migrated.translation.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); + }); + }); +}); diff --git a/packages/@o3r/localization/schematics/ng-update/v11-6/index.ts b/packages/@o3r/localization/schematics/ng-update/v11-6/index.ts new file mode 100644 index 0000000000..219bcf6d85 --- /dev/null +++ b/packages/@o3r/localization/schematics/ng-update/v11-6/index.ts @@ -0,0 +1,28 @@ +import type { + Rule, + Tree, +} from '@angular-devkit/schematics'; +import { + createSchematicWithMetricsIfInstalled, + findFilesInTree, +} from '@o3r/schematics'; + +const regexp = /translations\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) => /translation\.ts$/.test(filePath)); + files.forEach(({ content, path }) => { + const str = content.toString(); + const newContent = str.replaceAll(regexp, 'translations: Readonly<$1> = $2 as const;'); + if (newContent !== str) { + tree.overwrite(path, newContent); + } + }); + }; +} + +/** + * Update of Otter configuration V11.6 + */ +export const updateV116 = createSchematicWithMetricsIfInstalled(updateV116Fn);