Skip to content

Commit

Permalink
chore: add migration scripts for bootstrap dependency removal (#19926)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Krzysztof Platis <[email protected]>
Co-authored-by: Paweł Fraś <[email protected]>
  • Loading branch information
4 people authored Feb 4, 2025
1 parent 397395f commit d8eaa8b
Show file tree
Hide file tree
Showing 16 changed files with 361 additions and 79 deletions.
38 changes: 6 additions & 32 deletions docs/migration/2211_35/bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@
2. Update `styles.scss`
Modify the `styles.scss` file to integrate Spartacus styles along with Bootstrap. Proper import order is critical for
styles to be applied correctly.

### Steps to Update:

1. Place the following import for styles-config at the top of the file:
```@import 'styles-config';```
```@import 'styles-config';```
2. Add Spartacus core styles first. Importing Spartacus styles before Bootstrap ensures core styles load as a
priority.
3. Follow this by importing Bootstrap styles using the Bootstrap copy provided by Spartacus. Ensure the order of
Bootstrap imports matches the sequence below for consistency.
4. Conclude with the Spartacus index styles.


Final file structure should look like this:
Final file structure should look like this:

```styles.scss
// ORDER IMPORTANT: Spartacus core first
Expand All @@ -42,41 +43,14 @@

@import '@spartacus/styles/index';
```

3. Individual imports.
If your application directly imports specific Bootstrap classes in any of your stylesheets, replace those imports with the corresponding Spartacus imports. For example:

```
// Original import
@import '~bootstrap/scss/reboot';
// Replace with
@import '@spartacus/styles/vendor/bootstrap/scss/reboot';
```

4. Some libraries have stopped importing Bootstrap-related styles. Instead, these styles should now be imported directly within the application. For example, the `cart.scss` file should include the following imports:
```scss
// original imports
@import '../styles-config';
@import '@spartacus/cart';
```

```scss
// new imports
@import '../styles-config';
@import '@spartacus/cart';

@import '@spartacus/styles/vendor/bootstrap/scss/functions';
@import '@spartacus/styles/vendor/bootstrap/scss/variables';
@import '@spartacus/styles/vendor/bootstrap/scss/_mixins';
```
Affected libraries:
- cart
- checkout
- organization
- pick-up-in-store
- product
- product-multi-dimensional
- qualtrics
- quote
- storefinder
- epd-visualization
- opf
131 changes: 131 additions & 0 deletions projects/schematics/src/migrations/2211_35/bootstrap/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import {
chain,
Rule,
SchematicContext,
Tree,
} from '@angular-devkit/schematics';
import { replaceBootstrapImports } from './replace-bootstrap-imports';
import { spawn } from 'node:child_process';

export function migrate(): Rule {
return (tree: Tree, context: SchematicContext) => {
return chain([
uninstallBootstrap(),
updateMainStylesFileImports(),
replaceBootstrapImports(),
])(tree, context);
};
}

function uninstallBootstrap(): Rule {
return (tree: Tree, context: SchematicContext) => {
return async () => {
// Detect the package manager
let packageManager = '';
if (tree.exists('yarn.lock')) {
packageManager = 'yarn';
} else if (tree.exists('pnpm-lock.yaml')) {
packageManager = 'pnpm';
} else if (tree.exists('package-lock.json')) {
packageManager = 'npm';
}

let uninstallCommand = '';
if (packageManager === 'yarn') {
uninstallCommand = 'yarn remove bootstrap';
} else if (packageManager === 'pnpm') {
uninstallCommand = 'pnpm remove bootstrap';
} else if (packageManager === 'npm') {
uninstallCommand = 'npm uninstall bootstrap';
} else {
context.logger.warn(
'Could not detect a package manager. Please uninstall Bootstrap manually.'
);
return;
}

context.logger.info(`Running uninstall command: ${uninstallCommand}`);

await new Promise<void>((resolve) => {
const child = spawn(uninstallCommand, { shell: true });

child.on('close', (code) => {
if (code === 0) {
context.logger.info('Bootstrap uninstalled successfully.');
resolve();
} else {
context.logger.error(
`Bootstrap uninstall failed with exit code ${code}. Please uninstall Bootstrap manually.`
);
resolve();
}
});
});
};
};
}

function updateMainStylesFileImports(): Rule {
return (tree: Tree, context: SchematicContext) => {
const filePath = 'src/styles.scss';

if (!tree.exists(filePath)) {
context.logger.warn(`File ${filePath} does not exist.`);
return tree;
}

const fileContent = tree.read(filePath)?.toString('utf-8');

if (!fileContent) {
context.logger.warn(`File ${filePath} is empty or could not be read.`);
return tree;
}

context.logger.info(`Updating Bootstrap imports in '${filePath}'...`);

const styleImportsToInsert =
`@import 'styles-config';\n` +
`\n// ORDER IMPORTANT: Spartacus core first\n` +
`@import '@spartacus/styles/scss/core';\n\n` +
`// ORDER IMPORTANT: Copy of Bootstrap files next\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/reboot';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/type';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/grid';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/utilities';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/transitions';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/dropdown';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/card';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/nav';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/buttons';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/forms';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/custom-forms';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/modal';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/close';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/alert';\n` +
`@import '@spartacus/styles/vendor/bootstrap/scss/tooltip';\n\n` +
`// ORDER IMPORTANT: Spartacus styles last\n` +
`@import '@spartacus/styles/index';\n`;

const updatedContent = fileContent
.replace(
/\/\* You can add global styles to this file, and also import other style files \*\//g,
''
)
.replace(/@import\s+['"]@spartacus\/styles\/index['"];/g, '')
.replace(/@import ['"]styles-config['"];/g, styleImportsToInsert);

tree.overwrite(filePath, updatedContent);

context.logger.info(
`Bootstrap imports updated successfully in '${filePath}'.`
);

return tree;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';

const bootstrapImportsToReplace = [
{
find: 'bootstrap/scss/alert',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/alert',
},
{
find: 'bootstrap/scss/badge',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/badge',
},
{
find: 'bootstrap/scss/breadcrumb',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/breadcrumb',
},
{
find: 'bootstrap/scss/button-group',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/button-group',
},
{
find: 'bootstrap/scss/buttons',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/buttons',
},
{
find: 'bootstrap/scss/card',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/card',
},
{
find: 'bootstrap/scss/carousel',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/carousel',
},
{
find: 'bootstrap/scss/close',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/close',
},
{
find: 'bootstrap/scss/code',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/code',
},
{
find: 'bootstrap/scss/custom-forms',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/custom-forms',
},
{
find: 'bootstrap/scss/dropdown',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/dropdown',
},
{
find: 'bootstrap/scss/forms',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/forms',
},
{
find: 'bootstrap/scss/functions',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/functions',
},
{
find: 'bootstrap/scss/grid',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/grid',
},
{
find: 'bootstrap/scss/images',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/images',
},
{
find: 'bootstrap/scss/input-group',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/input-group',
},
{
find: 'bootstrap/scss/jumbotron',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/jumbotron',
},
{
find: 'bootstrap/scss/list-group',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/list-group',
},
{
find: 'bootstrap/scss/media',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/media',
},
{
find: 'bootstrap/scss/mixins',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/mixins',
},
{
find: 'bootstrap/scss/modal',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/modal',
},
{
find: 'bootstrap/scss/nav',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/nav',
},
{
find: 'bootstrap/scss/navbar',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/navbar',
},
{
find: 'bootstrap/scss/pagination',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/pagination',
},
{
find: 'bootstrap/scss/popover',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/popover',
},
{
find: 'bootstrap/scss/print',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/print',
},
{
find: 'bootstrap/scss/progress',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/progress',
},
{
find: 'bootstrap/scss/reboot',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/reboot',
},
{
find: 'bootstrap/scss/root',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/root',
},
{
find: 'bootstrap/scss/tables',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/tables',
},
{
find: 'bootstrap/scss/toasts',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/toasts',
},
{
find: 'bootstrap/scss/tooltip',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/tooltip',
},
{
find: 'bootstrap/scss/transitions',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/transitions',
},
{
find: 'bootstrap/scss/type',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/type',
},
{
find: 'bootstrap/scss/utilities',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/utilities',
},
{
find: 'bootstrap/scss/variables',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/variables',
},
{
find: 'bootstrap/scss/spinners',
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/spinners',
},
];

export function replaceBootstrapImports(): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.info(
'Scanning files for Bootstrap imports. ' +
'Imports will be updated to use `@spartacus/styles/vendor/bootstrap/scss/`.'
);

tree.visit((filePath) => {
if (filePath.endsWith('.scss')) {
const fileContent = tree.read(filePath)?.toString('utf-8');
if (fileContent) {
let updatedContent = fileContent;
let hasChanges = false;

bootstrapImportsToReplace.forEach(({ find, replaceWith }) => {
const regex = new RegExp(`@import\\s+['"]${find}['"];`, 'g');
if (regex.test(updatedContent)) {
updatedContent = updatedContent.replace(
regex,
`@import '${replaceWith}';`
);
hasChanges = true;
}
});

if (hasChanges) {
tree.overwrite(filePath, updatedContent);
context.logger.info(
`Updated imports of Bootstrap in file '${filePath}'`
);
}
}
}
});

context.logger.info('Bootstrap import replacement process completed.');
return tree;
};
}
Loading

0 comments on commit d8eaa8b

Please sign in to comment.