Development Workflow and Tooling
1. Sass Linting with stylelint Configuration
| Configuration | Plugin/Rule | Purpose | Example Rule |
|---|---|---|---|
| stylelint-scss | SCSS-specific rules | Enforce SCSS syntax | at-rule-no-unknown |
| Standard Config | stylelint-config-standard-scss | Recommended rules | Comprehensive base |
| Order Plugin | stylelint-order | Property ordering | Consistent structure |
| BEM Plugin | stylelint-selector-bem-pattern | BEM validation | Naming conventions |
| Custom Rules | Project-specific config | Team standards | Brand compliance |
Example: Complete stylelint configuration for SCSS
// .stylelintrc.json
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-prettier-scss"
],
"plugins": [
"stylelint-scss",
"stylelint-order"
],
"rules": {
// SCSS-specific rules
"scss/at-rule-no-unknown": true,
"scss/at-import-partial-extension": "never",
"scss/dollar-variable-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
"scss/percent-placeholder-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
"scss/at-mixin-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
"scss/at-function-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
"scss/selector-no-redundant-nesting-selector": true,
"scss/no-duplicate-dollar-variables": true,
"scss/no-duplicate-mixins": true,
"scss/operator-no-newline-after": true,
"scss/operator-no-unspaced": true,
"scss/dimension-no-non-numeric-values": true,
// Property ordering
"order/properties-order": [
"position",
"top",
"right",
"bottom",
"left",
"z-index",
"display",
"flex",
"flex-direction",
"justify-content",
"align-items",
"width",
"height",
"margin",
"padding",
"border",
"background",
"color",
"font",
"text-align",
"transition",
"transform"
],
// General rules
"color-hex-length": "short",
"color-named": "never",
"declaration-no-important": true,
"max-nesting-depth": 3,
"selector-max-id": 0,
"selector-max-compound-selectors": 4,
"selector-max-specificity": "0,4,0",
"selector-class-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*(__[a-z0-9]+(-[a-z0-9]+)*)?(--[a-z0-9]+(-[a-z0-9]+)*)?$",
// Comments
"comment-empty-line-before": [
"always",
{
"except": ["first-nested"],
"ignore": ["stylelint-commands"]
}
],
// Disabled rules
"no-descending-specificity": null,
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global", "local"]
}
]
},
"ignoreFiles": [
"node_modules/**",
"dist/**",
"build/**",
"*.min.css"
]
}
// package.json scripts
{
"scripts": {
"lint:scss": "stylelint '**/*.scss'",
"lint:scss:fix": "stylelint '**/*.scss' --fix",
"lint:scss:report": "stylelint '**/*.scss' --formatter json --output-file stylelint-report.json"
}
}
Example: Custom stylelint rules for team standards
// .stylelintrc.js - Advanced configuration
module.exports = {
extends: ['stylelint-config-standard-scss'],
plugins: [
'stylelint-scss',
'stylelint-order',
'stylelint-selector-bem-pattern'
],
rules: {
// BEM pattern enforcement
'plugin/selector-bem-pattern': {
preset: 'bem',
componentName: '[A-Z]+',
componentSelectors: {
initial: "^\\.{componentName}(?:-[a-z]+)*$",
combined: "^\\.combined-{componentName}-[a-z]+$"
},
utilitySelectors: "^\\.util-[a-z]+$"
},
// Custom SCSS variable naming
"scss/dollar-variable-pattern": [
"^(_)?[a-z][a-z0-9]*(-[a-z0-9]+)*$",
{
message: "Expected variable to be kebab-case (use _ prefix for private)",
ignore: ["global"]
}
],
// Enforce @use over @import
'scss/at-import-no-partial-leading-underscore': true,
'scss/load-no-partial-leading-underscore': true,
'at-rule-disallowed-list': ['import'],
// Mixin and function standards
'scss/at-mixin-argumentless-call-parentheses': 'always',
'scss/at-else-closing-brace-newline-after': 'always-last-in-chain',
'scss/at-else-closing-brace-space-after': 'always-intermediate',
'scss/at-else-empty-line-before': 'never',
'scss/at-if-closing-brace-newline-after': 'always-last-in-chain',
'scss/at-if-closing-brace-space-after': 'always-intermediate',
// Color management
'color-function-notation': 'modern',
'color-hex-case': 'lower',
'scss/dollar-variable-colon-space-after': 'always',
'scss/dollar-variable-colon-space-before': 'never',
// Property ordering with groups
'order/properties-order': [
{
groupName: 'positioning',
properties: ['position', 'top', 'right', 'bottom', 'left', 'z-index']
},
{
groupName: 'box-model',
properties: ['display', 'flex', 'grid', 'width', 'height', 'margin', 'padding']
},
{
groupName: 'typography',
properties: ['font-family', 'font-size', 'line-height', 'color', 'text-align']
},
{
groupName: 'visual',
properties: ['background', 'border', 'border-radius', 'box-shadow', 'opacity']
},
{
groupName: 'animation',
properties: ['transition', 'animation', 'transform']
}
],
// Limit complexity
'max-nesting-depth': [
3,
{
ignore: ['blockless-at-rules', 'pseudo-classes']
}
],
// Project-specific rules
'declaration-property-value-disallowed-list': {
'/^border/': ['none'],
'transition': ['/all/']
},
// Disable for SCSS features
'at-rule-no-unknown': null,
'function-no-unknown': null
}
};
// VS Code settings.json integration
{
"stylelint.enable": true,
"stylelint.validate": ["css", "scss"],
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": true
}
}
Example: Stylelint ignore patterns and overrides
// .stylelintrc.json with overrides
{
"extends": "stylelint-config-standard-scss",
"rules": {
"max-nesting-depth": 3,
"selector-max-id": 0
},
"overrides": [
{
// Stricter rules for components
"files": ["src/components/**/*.scss"],
"rules": {
"max-nesting-depth": 2,
"selector-class-pattern": "^[a-z][a-z0-9]*(__[a-z0-9]+)?(--[a-z0-9]+)?$"
}
},
{
// Relaxed rules for utilities
"files": ["src/utilities/**/*.scss"],
"rules": {
"declaration-no-important": null,
"max-nesting-depth": 1
}
},
{
// Legacy code (gradual migration)
"files": ["src/legacy/**/*.scss"],
"rules": {
"max-nesting-depth": null,
"selector-max-id": null,
"at-rule-disallowed-list": null
}
}
],
"ignoreFiles": [
"**/*.min.css",
"**/vendor/**",
"**/node_modules/**"
]
}
// .stylelintignore
node_modules/
dist/
build/
*.min.css
vendor/
coverage/
# Inline ignore comments in SCSS
.legacy-component {
/* stylelint-disable-next-line selector-max-id */
#legacy-id {
color: red;
}
}
.exception {
/* stylelint-disable declaration-no-important */
color: blue !important;
/* stylelint-enable declaration-no-important */
}
2. Prettier Formatting and Code Style
| Setting | Option | Recommendation | Reason |
|---|---|---|---|
| printWidth | 80-120 characters | 100 | Readability |
| singleQuote | true/false | true | Consistency |
| tabWidth | 2/4 spaces | 2 | Standard SCSS |
| trailingComma | none/es5/all | es5 | Git diffs |
| bracketSpacing | true/false | true | Readability |
Example: Prettier configuration for SCSS
// .prettierrc.json
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf",
"overrides": [
{
"files": "*.scss",
"options": {
"singleQuote": false,
"parser": "scss"
}
},
{
"files": "*.json",
"options": {
"printWidth": 80,
"tabWidth": 2
}
}
]
}
// .prettierignore
node_modules/
dist/
build/
coverage/
*.min.css
*.min.js
package-lock.json
yarn.lock
pnpm-lock.yaml
// package.json scripts
{
"scripts": {
"format": "prettier --write '**/*.{scss,css,js,jsx,ts,tsx,json,md}'",
"format:check": "prettier --check '**/*.{scss,css,js,jsx,ts,tsx,json,md}'",
"format:scss": "prettier --write '**/*.scss'"
},
"devDependencies": {
"prettier": "^3.1.0"
}
}
Example: EditorConfig for consistent formatting
// .editorconfig - Works with Prettier and most editors
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{js,jsx,ts,tsx,json}]
indent_style = space
indent_size = 2
[*.{scss,css}]
indent_style = space
indent_size = 2
quote_type = double
[*.md]
max_line_length = off
trim_trailing_whitespace = false
[{package.json,.prettierrc,.stylelintrc}]
indent_style = space
indent_size = 2
[*.yml]
indent_style = space
indent_size = 2
// Example SCSS before Prettier
.button{background-color:#0066cc;color:white;padding:10px 20px;&:hover{background-color:darken(#0066cc,10%);}}
// After Prettier
.button {
background-color: #0066cc;
color: white;
padding: 10px 20px;
&:hover {
background-color: darken(#0066cc, 10%);
}
}
Example: Integration with stylelint and Prettier
// Combine stylelint + Prettier for best results
// 1. Install dependencies
npm install -D prettier stylelint stylelint-config-prettier-scss
// 2. .stylelintrc.json
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-prettier-scss" // Disables conflicting rules
],
"rules": {
// Your custom rules
}
}
// 3. package.json scripts
{
"scripts": {
"lint": "npm run lint:scss && npm run format:check",
"lint:scss": "stylelint '**/*.scss'",
"lint:fix": "npm run lint:scss:fix && npm run format",
"lint:scss:fix": "stylelint '**/*.scss' --fix",
"format": "prettier --write '**/*.{scss,css,js,json}'",
"format:check": "prettier --check '**/*.{scss,css,js,json}'"
}
}
// 4. VS Code settings.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": true
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"stylelint.enable": true,
"stylelint.validate": ["css", "scss"],
"prettier.requireConfig": true
}
// 5. Workflow
// 1. Stylelint fixes structural/logical issues
// 2. Prettier formats the code aesthetically
// 3. Both run automatically on save in VS Code
// 4. Pre-commit hooks enforce on all commits
3. VS Code Extensions and IntelliSense
| Extension | ID | Features | Priority |
|---|---|---|---|
| SCSS IntelliSense | mrmlnc.vscode-scss | Autocomplete, go-to-def | Essential |
| Stylelint | stylelint.vscode-stylelint | Linting errors/warnings | Essential |
| Prettier | esbenp.prettier-vscode | Code formatting | Essential |
| Color Highlight | naumovs.color-highlight | Visual color preview | Recommended |
| CSS Peek | pranaygp.vscode-css-peek | Class definition lookup | Recommended |
Example: VS Code workspace settings for SCSS
// .vscode/settings.json
{
// Editor
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"editor.formatOnPaste": false,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": true,
"source.organizeImports": false
},
// SCSS specific
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.suggest.insertMode": "replace",
"editor.quickSuggestions": {
"other": true,
"comments": false,
"strings": true
}
},
// Stylelint
"stylelint.enable": true,
"stylelint.validate": ["css", "scss", "sass"],
"stylelint.snippet": ["css", "scss"],
"css.validate": false, // Disable default CSS validation
"scss.validate": false, // Let stylelint handle it
// Prettier
"prettier.requireConfig": true,
"prettier.useEditorConfig": true,
// SCSS IntelliSense
"scss.scannerDepth": 30,
"scss.scannerExclude": [
"**/.git",
"**/node_modules",
"**/bower_components"
],
"scss.implicitlyLabel": "(implicitly)",
// Color decorators
"editor.colorDecorators": true,
// Emmet
"emmet.includeLanguages": {
"scss": "css"
},
"emmet.syntaxProfiles": {
"scss": "css"
},
// Files
"files.associations": {
"*.scss": "scss"
},
"files.exclude": {
"**/.git": true,
"**/node_modules": true,
"**/.DS_Store": true,
"**/dist": true,
"**/build": true
},
// Search
"search.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/*.min.css": true
}
}
Example: Recommended VS Code extensions list
// .vscode/extensions.json
{
"recommendations": [
// Essential for SCSS development
"stylelint.vscode-stylelint",
"esbenp.prettier-vscode",
"mrmlnc.vscode-scss",
// Enhanced developer experience
"naumovs.color-highlight",
"pranaygp.vscode-css-peek",
"csstools.postcss",
// General productivity
"editorconfig.editorconfig",
"usernamehw.errorlens",
"christian-kohler.path-intellisense",
// Git integration
"eamodio.gitlens",
"mhutchie.git-graph"
],
"unwantedRecommendations": [
"hookyqr.beautify", // Use Prettier instead
"HookyQR.beautify"
]
}
// .vscode/tasks.json - Build tasks
{
"version": "2.0.0",
"tasks": [
{
"label": "Watch SCSS",
"type": "shell",
"command": "npm run watch:scss",
"group": {
"kind": "build",
"isDefault": true
},
"isBackground": true,
"problemMatcher": {
"owner": "scss",
"fileLocation": ["relative", "${workspaceFolder}"],
"pattern": {
"regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
},
"background": {
"activeOnStart": true,
"beginsPattern": "^Compiling",
"endsPattern": "^Compiled"
}
}
},
{
"label": "Lint SCSS",
"type": "shell",
"command": "npm run lint:scss",
"group": "test",
"presentation": {
"reveal": "always",
"panel": "new"
}
},
{
"label": "Format SCSS",
"type": "shell",
"command": "npm run format:scss",
"group": "none"
}
]
}
Example: Custom VS Code snippets for SCSS
// .vscode/scss.code-snippets
{
"SCSS Mixin": {
"prefix": "mixin",
"body": [
"@mixin ${1:name}($2) {",
" $3",
"}"
],
"description": "Create a SCSS mixin"
},
"SCSS Function": {
"prefix": "function",
"body": [
"@function ${1:name}($2) {",
" @return $3;",
"}"
],
"description": "Create a SCSS function"
},
"SCSS Use Module": {
"prefix": "use",
"body": "@use '${1:module}' as ${2:alias};",
"description": "Import SCSS module with @use"
},
"SCSS Forward Module": {
"prefix": "forward",
"body": "@forward '${1:module}';",
"description": "Forward SCSS module"
},
"SCSS Media Query": {
"prefix": "media",
"body": [
"@media (min-width: ${1:768px}) {",
" $2",
"}"
],
"description": "Create a media query"
},
"SCSS For Loop": {
"prefix": "for",
"body": [
"@for \\${1:i} from ${2:1} through ${3:10} {",
" $4",
"}"
],
"description": "Create a @for loop"
},
"SCSS Each Loop": {
"prefix": "each",
"body": [
"@each \\${1:item} in ${2:list} {",
" $3",
"}"
],
"description": "Create an @each loop"
},
"BEM Block": {
"prefix": "bem-block",
"body": [
".${1:block} {",
" $2",
" ",
" &__${3:element} {",
" $4",
" }",
" ",
" &--${5:modifier} {",
" $6",
" }",
"}"
],
"description": "Create a BEM block structure"
},
"Responsive Mixin": {
"prefix": "responsive",
"body": [
"@mixin responsive(\\$breakpoint) {",
" @if \\$breakpoint == mobile {",
" @media (max-width: 767px) { @content; }",
" } @else if \\$breakpoint == tablet {",
" @media (min-width: 768px) and (max-width: 1023px) { @content; }",
" } @else if \\$breakpoint == desktop {",
" @media (min-width: 1024px) { @content; }",
" }",
"}"
],
"description": "Create a responsive mixin"
}
}
4. Git Hooks and Pre-commit Validation
| Tool | Hook Type | Action | Purpose |
|---|---|---|---|
| Husky | pre-commit | Run linters | Quality gate |
| lint-staged | pre-commit | Lint staged files only | Fast validation |
| commitlint | commit-msg | Validate commit messages | Clean history |
| pre-push | pre-push | Run full test suite | Prevent broken pushes |
| prepare-commit-msg | commit template | Auto-add ticket number | Traceability |
Example: Husky and lint-staged setup
// 1. Install dependencies
npm install -D husky lint-staged
// 2. Initialize husky
npx husky install
npm pkg set scripts.prepare="husky install"
// 3. package.json configuration
{
"scripts": {
"prepare": "husky install",
"lint:scss": "stylelint '**/*.scss'",
"format": "prettier --write"
},
"lint-staged": {
"*.scss": [
"stylelint --fix",
"prettier --write"
],
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml}": [
"prettier --write"
]
},
"devDependencies": {
"husky": "^8.0.0",
"lint-staged": "^15.0.0",
"stylelint": "^16.0.0",
"prettier": "^3.0.0"
}
}
// 4. Create pre-commit hook
npx husky add .husky/pre-commit "npx lint-staged"
// .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🔍 Running pre-commit checks..."
npx lint-staged
// 5. Create commit-msg hook (optional)
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
// .commitlintrc.json
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"chore"
]
],
"subject-case": [2, "always", "sentence-case"]
}
}
// Example commit message:
// feat: add new button component styles
// fix: resolve Safari flexbox layout issue
// style: format SCSS files with Prettier
Example: Advanced pre-commit workflow
// .husky/pre-commit - Comprehensive checks
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🚀 Running pre-commit hooks..."
# 1. Run lint-staged for quick fixes
echo "📝 Linting and formatting staged files..."
npx lint-staged
# 2. Check for TODO/FIXME comments (optional warning)
echo "🔍 Checking for TODO/FIXME..."
git diff --cached --name-only --diff-filter=ACM | \
xargs grep -n -E "TODO|FIXME" && \
echo "⚠️ Warning: TODO/FIXME found in staged files" || true
# 3. Run type checking (if TypeScript)
if [ -f "tsconfig.json" ]; then
echo "🔷 Type checking..."
npm run type-check
fi
# 4. Check bundle size (optional)
# npm run size-check
echo "✅ Pre-commit checks passed!"
// package.json - Extended lint-staged config
{
"lint-staged": {
"*.scss": [
"stylelint --fix",
"prettier --write",
"git add"
],
"*.{css,scss}": [
// Custom script to check for vendor prefixes
"node scripts/check-prefixes.js"
],
"package.json": [
// Sort package.json
"sort-package-json",
"prettier --write"
]
}
}
// scripts/check-prefixes.js
const fs = require('fs');
const path = require('path');
// Check if files contain manual vendor prefixes
// (should use autoprefixer instead)
process.argv.slice(2).forEach(file => {
const content = fs.readFileSync(file, 'utf8');
const vendorPrefixes = /-webkit-|-moz-|-ms-|-o-/g;
if (vendorPrefixes.test(content)) {
console.error(`❌ Found vendor prefixes in ${file}`);
console.error(' Use autoprefixer instead of manual prefixes');
process.exit(1);
}
});
console.log('✅ No manual vendor prefixes found');
// .husky/pre-push - Run tests before push
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🧪 Running tests before push..."
# Run full test suite
npm run test
# Run full linting (not just staged)
npm run lint
# Check build
npm run build
echo "✅ All pre-push checks passed!"
Example: Custom validation scripts
// scripts/validate-scss.js - Custom SCSS validation
const fs = require('fs');
const path = require('path');
const glob = require('glob');
// Custom rules beyond stylelint
const rules = {
// Check for @import usage (should use @use)
noImport: {
pattern: /@import\s+['"][^'"]+['"]/g,
message: 'Use @use instead of @import'
},
// Check for hardcoded colors (should use variables)
noHardcodedColors: {
pattern: /(?<!\/\/.*)(#[0-9a-fA-F]{3,6}|rgba?\([^)]+\))/g,
exclude: /\$|@/,
message: 'Use color variables instead of hardcoded values'
},
// Check for z-index values (should use map)
zIndexMap: {
pattern: /z-index:\s*\d+/g,
message: 'Use z-index from $z-index map'
}
};
// Validate files
const files = glob.sync('src/**/*.scss');
let hasErrors = false;
files.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');
Object.entries(rules).forEach(([ruleName, rule]) => {
lines.forEach((line, index) => {
if (rule.pattern.test(line)) {
if (!rule.exclude || !rule.exclude.test(line)) {
console.error(
`❌ ${file}:${index + 1} - ${rule.message}`
);
console.error(` ${line.trim()}`);
hasErrors = true;
}
}
});
});
});
if (hasErrors) {
console.error('\n❌ SCSS validation failed!');
process.exit(1);
}
console.log('✅ SCSS validation passed!');
// Add to package.json
{
"scripts": {
"validate:scss": "node scripts/validate-scss.js",
"pre-commit": "npm run validate:scss && lint-staged"
}
}
5. Continuous Integration and Deployment
| Platform | Configuration | Steps | Artifacts |
|---|---|---|---|
| GitHub Actions | .github/workflows/*.yml | Lint, test, build | CSS files, reports |
| GitLab CI | .gitlab-ci.yml | Pipeline stages | Build artifacts |
| CircleCI | .circleci/config.yml | Workflow jobs | Compiled CSS |
| Jenkins | Jenkinsfile | Declarative pipeline | Distribution files |
| Vercel/Netlify | Build commands | Auto-deployment | Static assets |
Example: GitHub Actions workflow
// .github/workflows/scss-ci.yml
name: SCSS CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
lint-and-test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run stylelint
run: npm run lint:scss
- name: Check Prettier formatting
run: npm run format:check
- name: Run custom SCSS validation
run: npm run validate:scss
- name: Build SCSS
run: npm run build:scss
- name: Run tests
run: npm test
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: css-build-${{ matrix.node-version }}
path: dist/css/
- name: Generate stylelint report
if: always()
run: npm run lint:scss:report
- name: Upload lint report
if: always()
uses: actions/upload-artifact@v3
with:
name: stylelint-report
path: stylelint-report.json
size-check:
runs-on: ubuntu-latest
needs: lint-and-test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20.x'
- run: npm ci
- run: npm run build:scss
- name: Check bundle size
uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
build_script: build:scss
deploy:
runs-on: ubuntu-latest
needs: [lint-and-test, size-check]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20.x'
- run: npm ci
- run: npm run build:scss
- name: Deploy to production
run: npm run deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Example: GitLab CI pipeline
// .gitlab-ci.yml
image: node:20
cache:
paths:
- node_modules/
stages:
- install
- lint
- build
- test
- deploy
install:
stage: install
script:
- npm ci
artifacts:
paths:
- node_modules/
expire_in: 1 hour
lint:scss:
stage: lint
script:
- npm run lint:scss
- npm run format:check
artifacts:
reports:
codequality: stylelint-report.json
when: always
validate:scss:
stage: lint
script:
- npm run validate:scss
allow_failure: false
build:scss:
stage: build
script:
- npm run build:scss
artifacts:
paths:
- dist/css/
expire_in: 1 week
test:unit:
stage: test
script:
- npm run test
coverage: '/Coverage: \d+\.\d+%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
deploy:production:
stage: deploy
script:
- npm run deploy
only:
- main
environment:
name: production
url: https://example.com
when: manual
deploy:staging:
stage: deploy
script:
- npm run deploy:staging
only:
- develop
environment:
name: staging
url: https://staging.example.com
Example: Build optimization and caching
// package.json - Optimized build scripts
{
"scripts": {
"prebuild": "npm run clean",
"build": "npm run build:scss && npm run optimize:css",
"build:scss": "sass src/scss:dist/css --style=compressed --source-map",
"build:scss:dev": "sass src/scss:dist/css --style=expanded --source-map",
"watch:scss": "sass src/scss:dist/css --watch --style=expanded --source-map",
"optimize:css": "npm run autoprefixer && npm run purgecss",
"autoprefixer": "postcss dist/css/*.css --use autoprefixer -d dist/css",
"purgecss": "purgecss --css dist/css/*.css --content 'src/**/*.html' --output dist/css",
"clean": "rm -rf dist/css",
"deploy": "npm run build && npm run upload",
"upload": "aws s3 sync dist/css s3://my-bucket/css --cache-control max-age=31536000"
},
"devDependencies": {
"sass": "^1.70.0",
"postcss": "^8.4.0",
"postcss-cli": "^11.0.0",
"autoprefixer": "^10.4.0",
"purgecss": "^5.0.0"
}
}
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')({
overrideBrowserslist: [
'> 1%',
'last 2 versions',
'not dead'
]
}),
require('cssnano')({
preset: ['default', {
discardComments: {
removeAll: true
},
normalizeWhitespace: true
}]
})
]
};
// CI cache configuration
// GitHub Actions
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
// GitLab CI
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
6. Package Manager Integration (npm, yarn, pnpm)
| Manager | Installation | Lock File | Advantages |
|---|---|---|---|
| npm | npm install | package-lock.json | Built-in, universal |
| Yarn Classic | yarn install | yarn.lock | Faster, offline cache |
| Yarn Berry | yarn install | yarn.lock | PnP, zero-installs |
| pnpm | pnpm install | pnpm-lock.yaml | Disk efficient, strict |
| Bun | bun install | bun.lockb | Fastest, built-in |
Example: Complete package.json for SCSS project
{
"name": "my-scss-project",
"version": "1.0.0",
"description": "SCSS-powered web application",
"main": "index.js",
"scripts": {
"dev": "concurrently \"npm:watch:*\"",
"watch:scss": "sass src/scss:dist/css --watch --style=expanded --source-map",
"build": "npm run clean && npm run build:scss && npm run optimize",
"build:scss": "sass src/scss:dist/css --style=compressed --no-source-map",
"optimize": "postcss dist/css/*.css --use autoprefixer cssnano -d dist/css",
"lint": "npm run lint:scss && npm run format:check",
"lint:scss": "stylelint 'src/**/*.scss'",
"lint:scss:fix": "stylelint 'src/**/*.scss' --fix",
"format": "prettier --write 'src/**/*.{scss,js,json,md}'",
"format:check": "prettier --check 'src/**/*.{scss,js,json,md}'",
"test": "jest",
"clean": "rm -rf dist/css",
"prepare": "husky install",
"size": "size-limit",
"analyze": "sass src/scss:dist/css --style=expanded --embed-sources"
},
"dependencies": {},
"devDependencies": {
"sass": "^1.70.0",
"sass-loader": "^14.0.0",
"postcss": "^8.4.32",
"postcss-cli": "^11.0.0",
"autoprefixer": "^10.4.16",
"cssnano": "^6.0.2",
"stylelint": "^16.1.0",
"stylelint-config-standard-scss": "^12.0.0",
"stylelint-config-prettier-scss": "^1.0.0",
"stylelint-scss": "^6.0.0",
"stylelint-order": "^6.0.4",
"prettier": "^3.1.1",
"husky": "^8.0.3",
"lint-staged": "^15.2.0",
"concurrently": "^8.2.2",
"size-limit": "^11.0.1",
"@size-limit/file": "^11.0.1"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
],
"size-limit": [
{
"path": "dist/css/main.css",
"limit": "50 KB"
}
],
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
}
Example: pnpm workspace configuration
// pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
// package.json (root)
{
"name": "monorepo",
"private": true,
"scripts": {
"dev": "pnpm -r --parallel dev",
"build": "pnpm -r build",
"lint": "pnpm -r lint",
"test": "pnpm -r test"
},
"devDependencies": {
"sass": "^1.70.0",
"stylelint": "^16.0.0",
"prettier": "^3.0.0"
}
}
// packages/design-system/package.json
{
"name": "@company/design-system",
"version": "1.0.0",
"main": "dist/index.css",
"scripts": {
"dev": "sass src:dist --watch",
"build": "sass src:dist --style=compressed",
"lint": "stylelint 'src/**/*.scss'"
},
"devDependencies": {
"sass": "workspace:*",
"stylelint": "workspace:*"
}
}
// apps/website/package.json
{
"name": "@company/website",
"version": "1.0.0",
"dependencies": {
"@company/design-system": "workspace:*"
},
"devDependencies": {
"sass": "workspace:*"
}
}
// .npmrc (pnpm configuration)
shamefully-hoist=false
strict-peer-dependencies=true
auto-install-peers=true
resolution-mode=highest
// Commands
pnpm install # Install all dependencies
pnpm -r build # Build all packages
pnpm --filter @company/website dev # Run dev in specific package
pnpm -r --parallel dev # Run dev in all packages
Example: Monorepo with Yarn workspaces
// package.json (root)
{
"private": true,
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"dev": "yarn workspaces foreach -pi run dev",
"build": "yarn workspaces foreach -t run build",
"lint": "yarn workspaces foreach run lint",
"test": "yarn workspaces foreach run test"
},
"devDependencies": {
"sass": "^1.70.0",
"stylelint": "^16.0.0",
"prettier": "^3.0.0",
"lerna": "^8.0.0"
}
}
// .yarnrc.yml (Yarn Berry)
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.0.2.cjs
enableGlobalCache: true
// Commands
yarn install # Install all dependencies
yarn workspace @company/design-system build
yarn workspaces foreach build # Build all packages
yarn workspaces foreach -pi run dev # Parallel interactive dev
// Alternative: Using Lerna with Yarn
// lerna.json
{
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true,
"packages": [
"packages/*",
"apps/*"
],
"command": {
"version": {
"message": "chore(release): publish %s"
}
}
}
// Commands with Lerna
lerna bootstrap # Install dependencies
lerna run build # Run build in all packages
lerna run build --scope=@company/design-system
lerna publish # Publish packages
Development Workflow Best Practices
- Linting: Use stylelint with SCSS plugins, enforce consistent naming and structure
- Formatting: Prettier for automatic code formatting, integrate with editor and pre-commit
- VS Code: Install SCSS IntelliSense, stylelint, and Prettier extensions for best DX
- Git hooks: Use Husky + lint-staged to enforce quality before commits
- CI/CD: Automate linting, testing, and building in GitHub Actions or GitLab CI
- Package managers: Use pnpm for monorepos, enable caching and strict dependency resolution
- Build optimization: Compress with Sass, add autoprefixer, purge unused CSS
- Monorepo strategy: Share SCSS utilities across packages, use workspace dependencies
Note: A robust development workflow with automated linting, formatting,
and testing catches errors early and maintains code quality. Integrate these tools into your editor and
CI/CD pipeline for a seamless developer experience.