Skip to content

Commit d8eaa8b

Browse files
kpawelczakgithub-actions[bot]Platonnpawelfras
authored
chore: add migration scripts for bootstrap dependency removal (#19926)
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]>
1 parent 397395f commit d8eaa8b

16 files changed

+361
-79
lines changed

docs/migration/2211_35/bootstrap.md

+6-32
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@
77
2. Update `styles.scss`
88
Modify the `styles.scss` file to integrate Spartacus styles along with Bootstrap. Proper import order is critical for
99
styles to be applied correctly.
10+
1011
### Steps to Update:
12+
1113
1. Place the following import for styles-config at the top of the file:
12-
```@import 'styles-config';```
14+
```@import 'styles-config';```
1315
2. Add Spartacus core styles first. Importing Spartacus styles before Bootstrap ensures core styles load as a
1416
priority.
1517
3. Follow this by importing Bootstrap styles using the Bootstrap copy provided by Spartacus. Ensure the order of
1618
Bootstrap imports matches the sequence below for consistency.
1719
4. Conclude with the Spartacus index styles.
1820

19-
20-
Final file structure should look like this:
21+
Final file structure should look like this:
2122

2223
```styles.scss
2324
// ORDER IMPORTANT: Spartacus core first
@@ -42,41 +43,14 @@
4243

4344
@import '@spartacus/styles/index';
4445
```
46+
4547
3. Individual imports.
4648
If your application directly imports specific Bootstrap classes in any of your stylesheets, replace those imports with the corresponding Spartacus imports. For example:
49+
4750
```
4851
// Original import
4952
@import '~bootstrap/scss/reboot';
5053
5154
// Replace with
5255
@import '@spartacus/styles/vendor/bootstrap/scss/reboot';
5356
```
54-
55-
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:
56-
```scss
57-
// original imports
58-
@import '../styles-config';
59-
@import '@spartacus/cart';
60-
```
61-
62-
```scss
63-
// new imports
64-
@import '../styles-config';
65-
@import '@spartacus/cart';
66-
67-
@import '@spartacus/styles/vendor/bootstrap/scss/functions';
68-
@import '@spartacus/styles/vendor/bootstrap/scss/variables';
69-
@import '@spartacus/styles/vendor/bootstrap/scss/_mixins';
70-
```
71-
Affected libraries:
72-
- cart
73-
- checkout
74-
- organization
75-
- pick-up-in-store
76-
- product
77-
- product-multi-dimensional
78-
- qualtrics
79-
- quote
80-
- storefinder
81-
- epd-visualization
82-
- opf
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {
8+
chain,
9+
Rule,
10+
SchematicContext,
11+
Tree,
12+
} from '@angular-devkit/schematics';
13+
import { replaceBootstrapImports } from './replace-bootstrap-imports';
14+
import { spawn } from 'node:child_process';
15+
16+
export function migrate(): Rule {
17+
return (tree: Tree, context: SchematicContext) => {
18+
return chain([
19+
uninstallBootstrap(),
20+
updateMainStylesFileImports(),
21+
replaceBootstrapImports(),
22+
])(tree, context);
23+
};
24+
}
25+
26+
function uninstallBootstrap(): Rule {
27+
return (tree: Tree, context: SchematicContext) => {
28+
return async () => {
29+
// Detect the package manager
30+
let packageManager = '';
31+
if (tree.exists('yarn.lock')) {
32+
packageManager = 'yarn';
33+
} else if (tree.exists('pnpm-lock.yaml')) {
34+
packageManager = 'pnpm';
35+
} else if (tree.exists('package-lock.json')) {
36+
packageManager = 'npm';
37+
}
38+
39+
let uninstallCommand = '';
40+
if (packageManager === 'yarn') {
41+
uninstallCommand = 'yarn remove bootstrap';
42+
} else if (packageManager === 'pnpm') {
43+
uninstallCommand = 'pnpm remove bootstrap';
44+
} else if (packageManager === 'npm') {
45+
uninstallCommand = 'npm uninstall bootstrap';
46+
} else {
47+
context.logger.warn(
48+
'Could not detect a package manager. Please uninstall Bootstrap manually.'
49+
);
50+
return;
51+
}
52+
53+
context.logger.info(`Running uninstall command: ${uninstallCommand}`);
54+
55+
await new Promise<void>((resolve) => {
56+
const child = spawn(uninstallCommand, { shell: true });
57+
58+
child.on('close', (code) => {
59+
if (code === 0) {
60+
context.logger.info('Bootstrap uninstalled successfully.');
61+
resolve();
62+
} else {
63+
context.logger.error(
64+
`Bootstrap uninstall failed with exit code ${code}. Please uninstall Bootstrap manually.`
65+
);
66+
resolve();
67+
}
68+
});
69+
});
70+
};
71+
};
72+
}
73+
74+
function updateMainStylesFileImports(): Rule {
75+
return (tree: Tree, context: SchematicContext) => {
76+
const filePath = 'src/styles.scss';
77+
78+
if (!tree.exists(filePath)) {
79+
context.logger.warn(`File ${filePath} does not exist.`);
80+
return tree;
81+
}
82+
83+
const fileContent = tree.read(filePath)?.toString('utf-8');
84+
85+
if (!fileContent) {
86+
context.logger.warn(`File ${filePath} is empty or could not be read.`);
87+
return tree;
88+
}
89+
90+
context.logger.info(`Updating Bootstrap imports in '${filePath}'...`);
91+
92+
const styleImportsToInsert =
93+
`@import 'styles-config';\n` +
94+
`\n// ORDER IMPORTANT: Spartacus core first\n` +
95+
`@import '@spartacus/styles/scss/core';\n\n` +
96+
`// ORDER IMPORTANT: Copy of Bootstrap files next\n` +
97+
`@import '@spartacus/styles/vendor/bootstrap/scss/reboot';\n` +
98+
`@import '@spartacus/styles/vendor/bootstrap/scss/type';\n` +
99+
`@import '@spartacus/styles/vendor/bootstrap/scss/grid';\n` +
100+
`@import '@spartacus/styles/vendor/bootstrap/scss/utilities';\n` +
101+
`@import '@spartacus/styles/vendor/bootstrap/scss/transitions';\n` +
102+
`@import '@spartacus/styles/vendor/bootstrap/scss/dropdown';\n` +
103+
`@import '@spartacus/styles/vendor/bootstrap/scss/card';\n` +
104+
`@import '@spartacus/styles/vendor/bootstrap/scss/nav';\n` +
105+
`@import '@spartacus/styles/vendor/bootstrap/scss/buttons';\n` +
106+
`@import '@spartacus/styles/vendor/bootstrap/scss/forms';\n` +
107+
`@import '@spartacus/styles/vendor/bootstrap/scss/custom-forms';\n` +
108+
`@import '@spartacus/styles/vendor/bootstrap/scss/modal';\n` +
109+
`@import '@spartacus/styles/vendor/bootstrap/scss/close';\n` +
110+
`@import '@spartacus/styles/vendor/bootstrap/scss/alert';\n` +
111+
`@import '@spartacus/styles/vendor/bootstrap/scss/tooltip';\n\n` +
112+
`// ORDER IMPORTANT: Spartacus styles last\n` +
113+
`@import '@spartacus/styles/index';\n`;
114+
115+
const updatedContent = fileContent
116+
.replace(
117+
/\/\* You can add global styles to this file, and also import other style files \*\//g,
118+
''
119+
)
120+
.replace(/@import\s+['"]@spartacus\/styles\/index['"];/g, '')
121+
.replace(/@import ['"]styles-config['"];/g, styleImportsToInsert);
122+
123+
tree.overwrite(filePath, updatedContent);
124+
125+
context.logger.info(
126+
`Bootstrap imports updated successfully in '${filePath}'.`
127+
);
128+
129+
return tree;
130+
};
131+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
8+
9+
const bootstrapImportsToReplace = [
10+
{
11+
find: 'bootstrap/scss/alert',
12+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/alert',
13+
},
14+
{
15+
find: 'bootstrap/scss/badge',
16+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/badge',
17+
},
18+
{
19+
find: 'bootstrap/scss/breadcrumb',
20+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/breadcrumb',
21+
},
22+
{
23+
find: 'bootstrap/scss/button-group',
24+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/button-group',
25+
},
26+
{
27+
find: 'bootstrap/scss/buttons',
28+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/buttons',
29+
},
30+
{
31+
find: 'bootstrap/scss/card',
32+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/card',
33+
},
34+
{
35+
find: 'bootstrap/scss/carousel',
36+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/carousel',
37+
},
38+
{
39+
find: 'bootstrap/scss/close',
40+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/close',
41+
},
42+
{
43+
find: 'bootstrap/scss/code',
44+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/code',
45+
},
46+
{
47+
find: 'bootstrap/scss/custom-forms',
48+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/custom-forms',
49+
},
50+
{
51+
find: 'bootstrap/scss/dropdown',
52+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/dropdown',
53+
},
54+
{
55+
find: 'bootstrap/scss/forms',
56+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/forms',
57+
},
58+
{
59+
find: 'bootstrap/scss/functions',
60+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/functions',
61+
},
62+
{
63+
find: 'bootstrap/scss/grid',
64+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/grid',
65+
},
66+
{
67+
find: 'bootstrap/scss/images',
68+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/images',
69+
},
70+
{
71+
find: 'bootstrap/scss/input-group',
72+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/input-group',
73+
},
74+
{
75+
find: 'bootstrap/scss/jumbotron',
76+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/jumbotron',
77+
},
78+
{
79+
find: 'bootstrap/scss/list-group',
80+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/list-group',
81+
},
82+
{
83+
find: 'bootstrap/scss/media',
84+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/media',
85+
},
86+
{
87+
find: 'bootstrap/scss/mixins',
88+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/mixins',
89+
},
90+
{
91+
find: 'bootstrap/scss/modal',
92+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/modal',
93+
},
94+
{
95+
find: 'bootstrap/scss/nav',
96+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/nav',
97+
},
98+
{
99+
find: 'bootstrap/scss/navbar',
100+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/navbar',
101+
},
102+
{
103+
find: 'bootstrap/scss/pagination',
104+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/pagination',
105+
},
106+
{
107+
find: 'bootstrap/scss/popover',
108+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/popover',
109+
},
110+
{
111+
find: 'bootstrap/scss/print',
112+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/print',
113+
},
114+
{
115+
find: 'bootstrap/scss/progress',
116+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/progress',
117+
},
118+
{
119+
find: 'bootstrap/scss/reboot',
120+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/reboot',
121+
},
122+
{
123+
find: 'bootstrap/scss/root',
124+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/root',
125+
},
126+
{
127+
find: 'bootstrap/scss/tables',
128+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/tables',
129+
},
130+
{
131+
find: 'bootstrap/scss/toasts',
132+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/toasts',
133+
},
134+
{
135+
find: 'bootstrap/scss/tooltip',
136+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/tooltip',
137+
},
138+
{
139+
find: 'bootstrap/scss/transitions',
140+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/transitions',
141+
},
142+
{
143+
find: 'bootstrap/scss/type',
144+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/type',
145+
},
146+
{
147+
find: 'bootstrap/scss/utilities',
148+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/utilities',
149+
},
150+
{
151+
find: 'bootstrap/scss/variables',
152+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/variables',
153+
},
154+
{
155+
find: 'bootstrap/scss/spinners',
156+
replaceWith: '@spartacus/styles/vendor/bootstrap/scss/spinners',
157+
},
158+
];
159+
160+
export function replaceBootstrapImports(): Rule {
161+
return (tree: Tree, context: SchematicContext) => {
162+
context.logger.info(
163+
'Scanning files for Bootstrap imports. ' +
164+
'Imports will be updated to use `@spartacus/styles/vendor/bootstrap/scss/`.'
165+
);
166+
167+
tree.visit((filePath) => {
168+
if (filePath.endsWith('.scss')) {
169+
const fileContent = tree.read(filePath)?.toString('utf-8');
170+
if (fileContent) {
171+
let updatedContent = fileContent;
172+
let hasChanges = false;
173+
174+
bootstrapImportsToReplace.forEach(({ find, replaceWith }) => {
175+
const regex = new RegExp(`@import\\s+['"]${find}['"];`, 'g');
176+
if (regex.test(updatedContent)) {
177+
updatedContent = updatedContent.replace(
178+
regex,
179+
`@import '${replaceWith}';`
180+
);
181+
hasChanges = true;
182+
}
183+
});
184+
185+
if (hasChanges) {
186+
tree.overwrite(filePath, updatedContent);
187+
context.logger.info(
188+
`Updated imports of Bootstrap in file '${filePath}'`
189+
);
190+
}
191+
}
192+
}
193+
});
194+
195+
context.logger.info('Bootstrap import replacement process completed.');
196+
return tree;
197+
};
198+
}

0 commit comments

Comments
 (0)