Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: strf-10437 Autofix script: conditional file import #1064

Merged
merged 4 commits into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pull_request_review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
build:
strategy:
matrix:
node: [14.x]
node: [16.x, 18.x]
os: ['ubuntu-latest', 'windows-2019', 'macos-latest']

runs-on: ${{ matrix.os }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14.x'
node-version: '18.x'
- run: npm i
- name: Check Git Commit name
run: git log -1 --pretty=format:"%s" | npx commitlint
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v14.19
v18.13
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:14
FROM node:18

WORKDIR /usr/src/app

Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ The BigCommerce server emulator for local theme development.

## Install

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

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

Expand Down
23 changes: 23 additions & 0 deletions bin/stencil-scss-autofix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env node

require('colors');
const program = require('../lib/commander');

const ThemeConfig = require('../lib/theme-config');
const NodeSassAutoFixer = require('../lib/NodeSassAutoFixer');
const { THEME_PATH, PACKAGE_INFO } = require('../constants');
const { printCliResultErrorAndExit } = require('../lib/cliCommon');

program
.version(PACKAGE_INFO.version)
.option(
'-d, --dry',
'will not write any changes to the file system, instead it will print the changes to the console',
)
.parse(process.argv);

const cliOptions = program.opts();

const themeConfig = ThemeConfig.getInstance(THEME_PATH);

new NodeSassAutoFixer(THEME_PATH, themeConfig, cliOptions).run().catch(printCliResultErrorAndExit);
1 change: 1 addition & 0 deletions bin/stencil.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ program
.command('pull', 'Pulls currently active theme config files and overwrites local copy')
.command('download', 'Downloads all the theme files')
.command('debug', 'Prints environment and theme settings for debug purposes')
.command('scss autofix', 'Prints environment and theme settings for debug purposes')
.parse(process.argv);
253 changes: 253 additions & 0 deletions lib/NodeSassAutoFixer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
/* eslint-disable no-param-reassign, operator-assignment */
require('colors');

const fs = require('fs');
const path = require('path');
const postcss = require('postcss');
const postcssScss = require('postcss-scss');

const ScssValidator = require('./ScssValidator');
const cssCompiler = require('./css/compile');

const CONDITIONAL_IMPORT = 'conditional-import';

class NodeSassAutoFixer {
/**
*
* @param themePath
* @param themeConfig
* @param cliOptions
*/
constructor(themePath, themeConfig, cliOptions) {
this.themePath = themePath;
this.themeConfig = themeConfig;
this.cliOptions = cliOptions;

this.validator = new ScssValidator(themePath, themeConfig);
}

async run() {
const assetsPath = path.join(this.themePath, 'assets');
const rawConfig = await this.themeConfig.getConfig();
const cssFiles = await this.validator.getCssFiles();

let issuesDetected = false;
/* eslint-disable-next-line no-useless-catch */
try {
for await (const file of cssFiles) {
try {
/* eslint-disable-next-line no-await-in-loop */
await cssCompiler.compile(
rawConfig,
assetsPath,
file,
cssCompiler.SASS_ENGINE_NAME,
);
} catch (e) {
issuesDetected = true;
await this.tryToFix(e, file);
}
}
if (!issuesDetected) {
console.log('No issues deteted');
}
} catch (e) {
throw e;
}
}

async tryToFix(err, file) {
const problem = this.detectProblem(err);
if (problem) {
const dirname = path.join(this.themePath, 'assets/scss');
const filePath = this.resolveScssFileName(dirname, err.file);
if (problem === CONDITIONAL_IMPORT) {
await this.fixConditionalImport(filePath);
}
} else {
const filePath = path.join(this.themePath, 'assets/scss', file + '.scss');
console.log("Couldn't determine and autofix the problem. Please fix it manually.".red);
console.log('Found trying compile file:'.red, filePath);
throw new Error(err);
}
}

detectProblem(err) {
if (err.formatted) {
if (
err.formatted.includes(
'Error: Import directives may not be used within control directives or mixins',
)
) {
return CONDITIONAL_IMPORT;
}
}

return null;
}

async fixConditionalImport(filePath) {
const scss = fs.readFileSync(filePath, 'utf8');
const condImportFile = await this.processCss(scss, this.transformConditionalImport());

this.overrideFile(filePath, condImportFile.css);
for await (const message of condImportFile.messages) {
if (message.type === 'import') {
const importFile = this.findImportedFile(message.filename, filePath);
const importFileScss = fs.readFileSync(importFile);
const mixinFile = await this.processCss(
importFileScss,
this.transformRootToMixin(message),
);
this.overrideFile(importFile, mixinFile.css);
}
}
}

transformConditionalImport() {
return {
postcssPlugin: 'Transform Conditional Import',
AtRule: (rule, { AtRule, result }) => {
if (rule.name === 'if') {
rule.walkAtRules('import', (decl) => {
const newRule = new AtRule({
name: 'import',
params: decl.params,
source: decl.source,
});
const root = decl.root();
root.prepend(newRule);
decl.name = 'include';
decl.params = decl.params.replace(/['"]+/g, '');
result.messages.push({
type: 'import',
filename: decl.params,
});
});
}
},
};
}

transformRootToMixin(data) {
const self = this;
return {
postcssPlugin: 'Transform Root to Mixin',
Once(root, { AtRule, result }) {
// already wrapped in mixin
if (
root.nodes.length === 1 &&
root.nodes[0].type === 'atrule' &&
root.nodes[0].name === 'mixin'
) {
return;
}
const nodes = root.nodes.map((node) => {
const cloned = node.clone();
cloned.raws.before = '\n';
return cloned;
});
self.formatNodes(nodes);
const newRoot = new AtRule({
name: 'mixin',
params: data.filename,
source: root.source,
nodes,
});
result.root.nodes = [newRoot];
},
};
}

formatNodes(nodes) {
const spacer = this.getSpacer(nodes[0]);
this.addTabsToNodes(nodes, spacer);
}

addTabsToNodes(nodes, spacer) {
nodes.forEach((node) => {
if (node.nodes) {
node.raws.before = node.raws.before + spacer;
node.raws.after = node.raws.after + spacer;
this.addTabsToNodes(node.nodes, spacer);
} else {
if (node.raws.before) {
node.raws.before = node.raws.before + spacer;
}
if (node.prop === 'src') {
node.value = node.value.replace(/\n/g, '\n' + spacer);
}
}
});
}

getSpacer(node) {
const hasTabSpace = this.hasTabSpace(node.raws.before);
if (hasTabSpace) {
return '\t';
}

return ' ';
}

hasTabSpace(string) {
return /\t/g.test(string);
}

async processCss(data, plugin) {
const processor = postcss([plugin]);
return processor.process(data, { from: undefined, parser: postcssScss });
}

findImportedFile(file, originalFilePath) {
const originalDirname = path.dirname(originalFilePath);
return this.resolveScssFileName(originalDirname, file);
}

resolveScssFileName(dirname, fileName) {
if (!fileName.includes('.scss')) {
fileName += '.scss';
}
const filePath = path.join(dirname, fileName);
if (!fs.existsSync(filePath)) {
// try with underscore
const withUnderscoreFileName = this.getFileNameWithUnderscore(fileName);
const filePathWithUnderscore = path.join(dirname, withUnderscoreFileName);
if (!fs.existsSync(filePathWithUnderscore)) {
throw new Error(
`Import ${fileName} wasn't resolved in ${filePath} or ${filePathWithUnderscore}`,
);
}
return filePathWithUnderscore;
}

return filePath;
}

getFileNameWithUnderscore(fileName) {
const fileNameParts = fileName.split('/');
const withUnderscoreFileName = fileNameParts
.map((part, i) => {
if (i === fileNameParts.length - 1) {
return '_' + part;
}
return part;
})
.join('/');
return withUnderscoreFileName;
}

overrideFile(filePath, data) {
const phrase = this.cliOptions.dry ? 'Would override' : 'Overriding';
console.log(phrase.green + ' file:'.green, filePath);
if (this.cliOptions.dry) {
console.log('---Content---'.yellow);
console.log(data);
console.log('---END of Content---'.yellow);
} else {
fs.writeFileSync(filePath, data);
}
}
}

module.exports = NodeSassAutoFixer;
Loading