Skip to content

Commit

Permalink
feat: schematics for 2 guides: 'modernize-app-migrated-from-6_8-to-22…
Browse files Browse the repository at this point in the history
…11_19' and 'modernize-app-migrated-from-2211_32-2211_35 (#19962)

This PR consists of 2 Parts:
1. Schematics automating the guide https://github.com/SAP/spartacus/blob/develop/docs/migration/2211_35/modernize-apps-migrated-from-6.8-to-2211.19.md
2. Schematics automating the guide https://github.com/SAP/spartacus/blob/develop/docs/migration/2211_35/modernize-apps-migrated-from-2211.32-to-2211.35.md

Fixes:  https://jira.tools.sap/browse/CXSPA-9304

------------------------------------------

**QA steps for Part 1:**
- create new ng15 app with Spa 6.8 WITH SSR

- migrate it to ng17 and Spa 2211.19 according to this doc https://help.sap.com/docs/SAP_COMMERCE_COMPOSABLE_STOREFRONT/10a8bc7f635b4e3db6f6bb7880e58a7d/7266f6f01edb4328b4e09df299ea09be.html?q=updating

- build and publish to verdaccio libraries from this branch

- run the migration schematics in the following way (it will be the recommended way for customers:

```bash
# 1. Create temp dir for isolated Schematics v35 installation
node -e "require('fs').mkdirSync('../temp-schematics-35')"

# 2. Install schematics in temp dir
npm install @spartacus/[email protected] --prefix ../temp-schematics-35

# 3. Execute migration schematic from isolated location
ng g ../temp-schematics-35/node_modules/@spartacus/schematics:modernize-app-migrated-from-6_8-to-2211_19

# 4. Clean up temp directory
node -e "require('fs').rmSync('../temp-schematics-35', { recursive: true, force: true })"
```

- check the diff of changed files in your app and compare against the changes mentioned in the manual guide: https://github.com/SAP/spartacus/blob/develop/docs/migration/2211_35/modernize-apps-migrated-from-6.8-to-2211.19.md

- repeat everything, but starting with the app with CSR ONLY (no SSR)

--------------------------------

QA steps for part 2:
- create new ng17 app with Spa 2211.19 WITH SSR

- migrate it to ng19 and Spa 2211.35.0-1 according to this doc draft https://github.com/SAP/spartacus/blob/develop/docs/migration/2211_35/migration.md

- build and publish to verdaccio libraries from this branch

- run the migration schematics in the following way (it will be the recommended way for customers:

```bash
# Execute separate migration schematic:
ng g @spartacus/schematics:modernize-app-migrated-from-2211_32-to-2211_35
```

- check the diff of changed files in your app and compare against the changes mentioned in the manual guide: https://github.com/SAP/spartacus/blob/develop/docs/migration/2211_35/modernize-apps-migrated-from-2211.32-to-2211.35.md

- repeat everything, but starting with the app with CSR ONLY (no SSR)
  • Loading branch information
Platonn authored Feb 7, 2025
1 parent b90fe3a commit f092426
Show file tree
Hide file tree
Showing 41 changed files with 2,536 additions and 52 deletions.
8 changes: 8 additions & 0 deletions projects/schematics/src/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
"description": "Generate a feature wrapper module",
"factory": "./wrapper-module/index#generateWrapperModule",
"aliases": ["wrapper"]
},
"modernize-app-migrated-from-6_8-to-2211_19": {
"description": "Modernize Angular application migrated from 6.8 to 2211.19",
"factory": "./modernize-app-migrated-from-6_8-to-2211_19/index#migrate"
},
"modernize-app-migrated-from-2211_32-to-2211_35": {
"description": "Modernize Angular application migrated from 2211.32 to 2211.35",
"factory": "./modernize-app-migrated-from-2211_32-to-2211_35/index#migrate"
}
}
}
4 changes: 2 additions & 2 deletions projects/schematics/src/migrations/2211_19/ssr/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
*/

import { noop, Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { checkIfSSRIsUsed } from '../../../shared/utils/package-utils';
import { isUsingLegacyServerBuilder as isOldSsrUsed } from '../../../shared/utils/package-utils';
import { updateServerFiles } from '../update-ssr/update-ssr-files';

export function migrate(): Rule {
return (tree: Tree, _context: SchematicContext) => {
return checkIfSSRIsUsed(tree) ? updateServerFiles() : noop();
return isOldSsrUsed(tree) ? updateServerFiles() : noop();
};
}
6 changes: 2 additions & 4 deletions projects/schematics/src/migrations/2211_35/ssr/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
*/

import { noop, Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { checkIfSSRIsUsedWithApplicationBuilder } from '../../../shared/utils/package-utils';
import { isSsrUsed } from '../../../shared/utils/package-utils';
import { updateServerFile } from './update-ssr/update-server-files';

export function migrate(): Rule {
return (tree: Tree, _context: SchematicContext) => {
return checkIfSSRIsUsedWithApplicationBuilder(tree)
? updateServerFile()
: noop();
return isSsrUsed(tree) ? updateServerFile() : noop();
};
}
34 changes: 9 additions & 25 deletions projects/schematics/src/migrations/2211_35/ssr/ssr_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as shared from '../../../shared/utils/package-utils';

jest.mock('../../../shared/utils/package-utils', () => ({
...jest.requireActual('../../../shared/utils/package-utils'),
checkIfSSRIsUsedWithApplicationBuilder: jest.fn(),
isSsrUsed: jest.fn(),
}));

const collectionPath = join(__dirname, '../../migrations.json');
Expand Down Expand Up @@ -68,9 +68,7 @@ describe('Update SSR Migration', () => {
it.each(['/server.ts', '/src/server.ts'])(
'should update %s when using application builder and SSR is used',
async (filePath) => {
(
shared.checkIfSSRIsUsedWithApplicationBuilder as jest.Mock
).mockReturnValue(true);
(shared.isSsrUsed as jest.Mock).mockReturnValue(true);
tree.create(filePath, serverFileContent);

const newTree = await runner.runSchematic(
Expand All @@ -83,16 +81,12 @@ describe('Update SSR Migration', () => {
expect(content).toContain('export function app()');
expect(content).toContain("join(serverDistFolder, 'index.server.html')");
expect(content).not.toContain("join(browserDistFolder, 'index.html')");
expect(
shared.checkIfSSRIsUsedWithApplicationBuilder
).toHaveBeenCalledWith(tree);
expect(shared.isSsrUsed).toHaveBeenCalledWith(tree);
}
);

it('should not update when SSR is not used', async () => {
(
shared.checkIfSSRIsUsedWithApplicationBuilder as jest.Mock
).mockReturnValue(false);
(shared.isSsrUsed as jest.Mock).mockReturnValue(false);
tree.create('/server.ts', serverFileContent);

const newTree = await runner.runSchematic(MIGRATION_SCRIPT_NAME, {}, tree);
Expand All @@ -102,29 +96,21 @@ describe('Update SSR Migration', () => {
expect(content).not.toContain(
"join(serverDistFolder, 'index.server.html')"
);
expect(shared.checkIfSSRIsUsedWithApplicationBuilder).toHaveBeenCalledWith(
tree
);
expect(shared.isSsrUsed).toHaveBeenCalledWith(tree);
});

it('should handle missing server.ts file', async () => {
(
shared.checkIfSSRIsUsedWithApplicationBuilder as jest.Mock
).mockReturnValue(true);
(shared.isSsrUsed as jest.Mock).mockReturnValue(true);

const newTree = await runner.runSchematic(MIGRATION_SCRIPT_NAME, {}, tree);

expect(newTree.exists('/server.ts')).toBe(false);
expect(newTree.exists('/src/server.ts')).toBe(false);
expect(shared.checkIfSSRIsUsedWithApplicationBuilder).toHaveBeenCalledWith(
tree
);
expect(shared.isSsrUsed).toHaveBeenCalledWith(tree);
});

it('should preserve other join statements when SSR is used', async () => {
(
shared.checkIfSSRIsUsedWithApplicationBuilder as jest.Mock
).mockReturnValue(true);
(shared.isSsrUsed as jest.Mock).mockReturnValue(true);

const contentWithMultipleJoins = `
const otherFile = join(process.cwd(), 'other.html');
Expand All @@ -140,8 +126,6 @@ describe('Update SSR Migration', () => {
expect(content).toContain("join(process.cwd(), 'other.html')");
expect(content).toContain('join(serverDistFolder, "index.server.html")');
expect(content).toContain("join(process.cwd(), 'another.html')");
expect(shared.checkIfSSRIsUsedWithApplicationBuilder).toHaveBeenCalledWith(
tree
);
expect(shared.isSsrUsed).toHaveBeenCalledWith(tree);
});
});
4 changes: 2 additions & 2 deletions projects/schematics/src/migrations/3_0/ssr/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
} from '../../../shared/utils/lib-utils';
import { createImportChange } from '../../../shared/utils/module-file-utils';
import {
checkIfSSRIsUsed,
isUsingLegacyServerBuilder as isOldSsrUsed,
getSpartacusSchematicsVersion,
readPackageJson,
} from '../../../shared/utils/package-utils';
Expand All @@ -43,7 +43,7 @@ export function migrate(): Rule {
return (tree: Tree, _context: SchematicContext) => {
const packageJson = readPackageJson(tree);

return checkIfSSRIsUsed(tree)
return isOldSsrUsed(tree)
? chain([
updateImport(),
addSetupPackageJsonDependencies(packageJson),
Expand Down
4 changes: 2 additions & 2 deletions projects/schematics/src/migrations/6_0/ssr/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

import { noop, Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { modifyAppServerModuleFile } from '../../../add-ssr/index';
import { checkIfSSRIsUsed } from '../../../shared/utils/package-utils';
import { isUsingLegacyServerBuilder as isOldSsrUsed } from '../../../shared/utils/package-utils';

export function migrate(): Rule {
return (tree: Tree, _context: SchematicContext) => {
return checkIfSSRIsUsed(tree) ? modifyAppServerModuleFile() : noop();
return isOldSsrUsed(tree) ? modifyAppServerModuleFile() : noop();
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { printErrorWithDocsForMigrated_2211_32_To_2211_35 as printErrorWithDocs } from '../fallback-advice-to-follow-docs';

/**
* Moves the `src/assets/` folder to the root and renames it to `public/`,
* to adapt to the new Angular v19 standards.
*/
export function moveAssetsToPublic(): Rule {
return (tree: Tree, context: SchematicContext) => {
enum AssetsDirs {
OLD = 'src/assets',
NEW = 'public',
}

context.logger.info(
`\n⏳ Moving assets folder from "${AssetsDirs.OLD}/" to "${AssetsDirs.NEW}/"...`
);

const sourceDir = tree.getDir(AssetsDirs.OLD);
if (!sourceDir.subfiles.length && !sourceDir.subdirs.length) {
printErrorWithDocs(
`Assets folder not found or empty at ${AssetsDirs.OLD}`,
context
);
return;
}

try {
tree.getDir(AssetsDirs.OLD).visit((filePath) => {
const relativeFilePath = filePath.replace(`${AssetsDirs.OLD}/`, '');
context.logger.info(` ↳ Moving file "${filePath}"`);

const content = tree.read(filePath);
if (content) {
tree.create(`${AssetsDirs.NEW}/${relativeFilePath}`, content);
} else {
printErrorWithDocs(`Failed to read ${filePath} file`, context);
}
});
} catch (error) {
printErrorWithDocs(
`Error moving assets file from "${AssetsDirs.OLD}" to "${AssetsDirs.NEW}". Error: ${error}`,
context
);
}

context.logger.info(` ↳ Deleting old "${AssetsDirs.OLD}/" directory`);
try {
tree.delete(AssetsDirs.OLD);
} catch (error) {
printErrorWithDocs(
`Error deleting old assets directory "${AssetsDirs.OLD}". Error: ${error}`,
context
);
}

context.logger.info(
`✅ Moved assets folder from "${AssetsDirs.OLD}/" to "${AssetsDirs.NEW}/"`
);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { printErrorWithDocsForMigrated_2211_32_To_2211_35 as printErrorWithDocs } from '../fallback-advice-to-follow-docs';

/**
* Moves the `favicon.ico` file from the `src/` folder to the `public/` folder,
* to adapt to the new Angular v19 standards.
*/
export function moveFaviconToPublic(): Rule {
return (tree: Tree, context: SchematicContext) => {
const fileName = 'favicon.ico';

const oldDir = 'src';
const oldPath = `${oldDir}/${fileName}`;

const newDir = 'public';
const newPath = `${newDir}/${fileName}`;

context.logger.info(
`\n⏳ Moving ${fileName} from "${oldDir}/" to "${newDir}/"...`
);

if (!tree.exists(oldPath)) {
printErrorWithDocs(`Favicon not found at ${oldPath}`, context);
return;
}

const content = tree.read(oldPath);
if (content) {
tree.create(newPath, content);
tree.delete(oldPath);
} else {
printErrorWithDocs(`Failed to read ${oldPath} file`, context);
return;
}

context.logger.info(
`✅ Moved ${fileName} from "${oldDir}/" to "${newDir}/"`
);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { getWorkspace } from '../../shared/utils/workspace-utils';
import { printErrorWithDocsForMigrated_2211_32_To_2211_35 as printErrorWithDocs } from '../fallback-advice-to-follow-docs';
import {
BrowserBuilderBaseOptions,
BrowserBuilderTarget,
} from '@schematics/angular/utility/workspace-models';

/**
* Updates the Angular configuration file to new Angular v19 standards.
*
* It updates the "assets" property for the "build" and "test" targets,
* to use the new path with the `public/` folder,
* instead of `src/assets` and `src/favicon.ico`.
*/
export function updateAngularJson(): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.info('\n⏳ Updating angular.json assets configuration...');

const { workspace, path } = getWorkspace(tree);
const project = workspace.projects[Object.keys(workspace.projects)[0]];

if (!project) {
printErrorWithDocs('No project found in workspace', context);
return;
}

const buildTarget = project.architect?.build as BrowserBuilderTarget;
const testTarget = project.architect?.test;

if (!buildTarget) {
printErrorWithDocs(
'Could not find "build" target in project configuration',
context
);
return;
}

const oldAssets: BrowserBuilderBaseOptions['assets'] = [
'src/favicon.ico',
'src/assets',
];
const newAssets: BrowserBuilderBaseOptions['assets'] = [
{ glob: '**/*', input: 'public' },
];

context.logger.info(
` ↳ Removing "assets" configuration for ${oldAssets
.map((x) => `"${x}"`)
.join(', ')}`
);
context.logger.info(
' ↳ Adding "assets" configuration: `{ glob: "**/*", input: "public" }`'
);

if (Array.isArray(buildTarget.options?.assets)) {
buildTarget.options.assets = buildTarget.options.assets.filter(
(asset: string | object) => !oldAssets.includes(asset)
);

// Add the new public assets config
buildTarget.options.assets = [
...newAssets,
...buildTarget.options.assets,
];
} else {
printErrorWithDocs(
'Could not find "assets" array in "build" target configuration',
context
);
}

// Update config for "test" target
if (Array.isArray(testTarget?.options?.assets)) {
testTarget.options.assets = testTarget.options.assets.filter(
(asset: string | object) => !oldAssets.includes(asset)
);

// Add the new public assets config
testTarget.options.assets = [...newAssets, ...testTarget.options.assets];
} else {
printErrorWithDocs(
'Could not find "assets" array in "test" target configuration',
context
);
}

const JSON_INDENT = 2;
tree.overwrite(path, JSON.stringify(workspace, null, JSON_INDENT));
context.logger.info('✅ Updated angular.json assets configuration');
};
}
Loading

0 comments on commit f092426

Please sign in to comment.