Skip to content

Commit d00f35c

Browse files
authored
feat: strf-10437 Autofix script: conditional file import (#1064)
1 parent aa0919b commit d00f35c

38 files changed

+30114
-11736
lines changed

.github/workflows/pull_request_review.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
build:
1212
strategy:
1313
matrix:
14-
node: [14.x]
14+
node: [16.x, 18.x]
1515
os: ['ubuntu-latest', 'windows-2019', 'macos-latest']
1616

1717
runs-on: ${{ matrix.os }}

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
- uses: actions/checkout@v3
1212
- uses: actions/setup-node@v3
1313
with:
14-
node-version: '14.x'
14+
node-version: '18.x'
1515
- run: npm i
1616
- name: Check Git Commit name
1717
run: git log -1 --pretty=format:"%s" | npx commitlint

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v14.19
1+
v18.13

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:14
1+
FROM node:18
22

33
WORKDIR /usr/src/app
44

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ The BigCommerce server emulator for local theme development.
77

88
## Install
99

10-
_Note: Stencil requires the Node.js runtime environment,
11-
version 14.x is supported.
12-
We do not yet have support for versions greater than Node 14._
10+
Note: Stencil requires the Node.js runtime environment,
11+
versions 16.x, 18.x are supported.
1312

1413
Run `npm install -g @bigcommerce/stencil-cli`.
1514

bin/stencil-scss-autofix.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env node
2+
3+
require('colors');
4+
const program = require('../lib/commander');
5+
6+
const ThemeConfig = require('../lib/theme-config');
7+
const NodeSassAutoFixer = require('../lib/NodeSassAutoFixer');
8+
const { THEME_PATH, PACKAGE_INFO } = require('../constants');
9+
const { printCliResultErrorAndExit } = require('../lib/cliCommon');
10+
11+
program
12+
.version(PACKAGE_INFO.version)
13+
.option(
14+
'-d, --dry',
15+
'will not write any changes to the file system, instead it will print the changes to the console',
16+
)
17+
.parse(process.argv);
18+
19+
const cliOptions = program.opts();
20+
21+
const themeConfig = ThemeConfig.getInstance(THEME_PATH);
22+
23+
new NodeSassAutoFixer(THEME_PATH, themeConfig, cliOptions).run().catch(printCliResultErrorAndExit);

bin/stencil.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ program
1616
.command('pull', 'Pulls currently active theme config files and overwrites local copy')
1717
.command('download', 'Downloads all the theme files')
1818
.command('debug', 'Prints environment and theme settings for debug purposes')
19+
.command('scss autofix', 'Prints environment and theme settings for debug purposes')
1920
.parse(process.argv);

lib/NodeSassAutoFixer.js

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
/* eslint-disable no-param-reassign, operator-assignment */
2+
require('colors');
3+
4+
const fs = require('fs');
5+
const path = require('path');
6+
const postcss = require('postcss');
7+
const postcssScss = require('postcss-scss');
8+
9+
const ScssValidator = require('./ScssValidator');
10+
const cssCompiler = require('./css/compile');
11+
12+
const CONDITIONAL_IMPORT = 'conditional-import';
13+
14+
class NodeSassAutoFixer {
15+
/**
16+
*
17+
* @param themePath
18+
* @param themeConfig
19+
* @param cliOptions
20+
*/
21+
constructor(themePath, themeConfig, cliOptions) {
22+
this.themePath = themePath;
23+
this.themeConfig = themeConfig;
24+
this.cliOptions = cliOptions;
25+
26+
this.validator = new ScssValidator(themePath, themeConfig);
27+
}
28+
29+
async run() {
30+
const assetsPath = path.join(this.themePath, 'assets');
31+
const rawConfig = await this.themeConfig.getConfig();
32+
const cssFiles = await this.validator.getCssFiles();
33+
34+
let issuesDetected = false;
35+
/* eslint-disable-next-line no-useless-catch */
36+
try {
37+
for await (const file of cssFiles) {
38+
try {
39+
/* eslint-disable-next-line no-await-in-loop */
40+
await cssCompiler.compile(
41+
rawConfig,
42+
assetsPath,
43+
file,
44+
cssCompiler.SASS_ENGINE_NAME,
45+
);
46+
} catch (e) {
47+
issuesDetected = true;
48+
await this.tryToFix(e, file);
49+
}
50+
}
51+
if (!issuesDetected) {
52+
console.log('No issues deteted');
53+
}
54+
} catch (e) {
55+
throw e;
56+
}
57+
}
58+
59+
async tryToFix(err, file) {
60+
const problem = this.detectProblem(err);
61+
if (problem) {
62+
const dirname = path.join(this.themePath, 'assets/scss');
63+
const filePath = this.resolveScssFileName(dirname, err.file);
64+
if (problem === CONDITIONAL_IMPORT) {
65+
await this.fixConditionalImport(filePath);
66+
}
67+
} else {
68+
const filePath = path.join(this.themePath, 'assets/scss', file + '.scss');
69+
console.log("Couldn't determine and autofix the problem. Please fix it manually.".red);
70+
console.log('Found trying compile file:'.red, filePath);
71+
throw new Error(err);
72+
}
73+
}
74+
75+
detectProblem(err) {
76+
if (err.formatted) {
77+
if (
78+
err.formatted.includes(
79+
'Error: Import directives may not be used within control directives or mixins',
80+
)
81+
) {
82+
return CONDITIONAL_IMPORT;
83+
}
84+
}
85+
86+
return null;
87+
}
88+
89+
async fixConditionalImport(filePath) {
90+
const scss = fs.readFileSync(filePath, 'utf8');
91+
const condImportFile = await this.processCss(scss, this.transformConditionalImport());
92+
93+
this.overrideFile(filePath, condImportFile.css);
94+
for await (const message of condImportFile.messages) {
95+
if (message.type === 'import') {
96+
const importFile = this.findImportedFile(message.filename, filePath);
97+
const importFileScss = fs.readFileSync(importFile);
98+
const mixinFile = await this.processCss(
99+
importFileScss,
100+
this.transformRootToMixin(message),
101+
);
102+
this.overrideFile(importFile, mixinFile.css);
103+
}
104+
}
105+
}
106+
107+
transformConditionalImport() {
108+
return {
109+
postcssPlugin: 'Transform Conditional Import',
110+
AtRule: (rule, { AtRule, result }) => {
111+
if (rule.name === 'if') {
112+
rule.walkAtRules('import', (decl) => {
113+
const newRule = new AtRule({
114+
name: 'import',
115+
params: decl.params,
116+
source: decl.source,
117+
});
118+
const root = decl.root();
119+
root.prepend(newRule);
120+
decl.name = 'include';
121+
decl.params = decl.params.replace(/['"]+/g, '');
122+
result.messages.push({
123+
type: 'import',
124+
filename: decl.params,
125+
});
126+
});
127+
}
128+
},
129+
};
130+
}
131+
132+
transformRootToMixin(data) {
133+
const self = this;
134+
return {
135+
postcssPlugin: 'Transform Root to Mixin',
136+
Once(root, { AtRule, result }) {
137+
// already wrapped in mixin
138+
if (
139+
root.nodes.length === 1 &&
140+
root.nodes[0].type === 'atrule' &&
141+
root.nodes[0].name === 'mixin'
142+
) {
143+
return;
144+
}
145+
const nodes = root.nodes.map((node) => {
146+
const cloned = node.clone();
147+
cloned.raws.before = '\n';
148+
return cloned;
149+
});
150+
self.formatNodes(nodes);
151+
const newRoot = new AtRule({
152+
name: 'mixin',
153+
params: data.filename,
154+
source: root.source,
155+
nodes,
156+
});
157+
result.root.nodes = [newRoot];
158+
},
159+
};
160+
}
161+
162+
formatNodes(nodes) {
163+
const spacer = this.getSpacer(nodes[0]);
164+
this.addTabsToNodes(nodes, spacer);
165+
}
166+
167+
addTabsToNodes(nodes, spacer) {
168+
nodes.forEach((node) => {
169+
if (node.nodes) {
170+
node.raws.before = node.raws.before + spacer;
171+
node.raws.after = node.raws.after + spacer;
172+
this.addTabsToNodes(node.nodes, spacer);
173+
} else {
174+
if (node.raws.before) {
175+
node.raws.before = node.raws.before + spacer;
176+
}
177+
if (node.prop === 'src') {
178+
node.value = node.value.replace(/\n/g, '\n' + spacer);
179+
}
180+
}
181+
});
182+
}
183+
184+
getSpacer(node) {
185+
const hasTabSpace = this.hasTabSpace(node.raws.before);
186+
if (hasTabSpace) {
187+
return '\t';
188+
}
189+
190+
return ' ';
191+
}
192+
193+
hasTabSpace(string) {
194+
return /\t/g.test(string);
195+
}
196+
197+
async processCss(data, plugin) {
198+
const processor = postcss([plugin]);
199+
return processor.process(data, { from: undefined, parser: postcssScss });
200+
}
201+
202+
findImportedFile(file, originalFilePath) {
203+
const originalDirname = path.dirname(originalFilePath);
204+
return this.resolveScssFileName(originalDirname, file);
205+
}
206+
207+
resolveScssFileName(dirname, fileName) {
208+
if (!fileName.includes('.scss')) {
209+
fileName += '.scss';
210+
}
211+
const filePath = path.join(dirname, fileName);
212+
if (!fs.existsSync(filePath)) {
213+
// try with underscore
214+
const withUnderscoreFileName = this.getFileNameWithUnderscore(fileName);
215+
const filePathWithUnderscore = path.join(dirname, withUnderscoreFileName);
216+
if (!fs.existsSync(filePathWithUnderscore)) {
217+
throw new Error(
218+
`Import ${fileName} wasn't resolved in ${filePath} or ${filePathWithUnderscore}`,
219+
);
220+
}
221+
return filePathWithUnderscore;
222+
}
223+
224+
return filePath;
225+
}
226+
227+
getFileNameWithUnderscore(fileName) {
228+
const fileNameParts = fileName.split('/');
229+
const withUnderscoreFileName = fileNameParts
230+
.map((part, i) => {
231+
if (i === fileNameParts.length - 1) {
232+
return '_' + part;
233+
}
234+
return part;
235+
})
236+
.join('/');
237+
return withUnderscoreFileName;
238+
}
239+
240+
overrideFile(filePath, data) {
241+
const phrase = this.cliOptions.dry ? 'Would override' : 'Overriding';
242+
console.log(phrase.green + ' file:'.green, filePath);
243+
if (this.cliOptions.dry) {
244+
console.log('---Content---'.yellow);
245+
console.log(data);
246+
console.log('---END of Content---'.yellow);
247+
} else {
248+
fs.writeFileSync(filePath, data);
249+
}
250+
}
251+
}
252+
253+
module.exports = NodeSassAutoFixer;

0 commit comments

Comments
 (0)