├── .editorconfig ├── .eslintrc ├── .gitignore ├── .node-version ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── config ├── .browserslistrc.sample ├── .eslintrc.json.sample ├── babel.config.json.sample ├── browser-sync.json.sample ├── sass-compiler.json.sample ├── sass-lint.yml.sample ├── stylelint.yml.sample ├── svg-sprite.yml.sample ├── themes.json.sample └── watcher.json.sample ├── gulpfile.mjs ├── helpers ├── babel.mjs ├── config-loader.mjs ├── config.mjs ├── css-lint.mjs ├── dependency-tree-builder.mjs ├── email-fix.mjs ├── error-message.mjs ├── eslint.mjs ├── get-themes.mjs ├── inheritance-resolver.mjs ├── pipeline.mjs ├── sass-error.mjs ├── sass-lint.mjs ├── scss.mjs └── svg.mjs ├── package-lock.json ├── package.json ├── tasks ├── babel.mjs ├── browser-sync.mjs ├── clean.mjs ├── css-lint.mjs ├── default.mjs ├── email-fix.mjs ├── eslint.mjs ├── inheritance.mjs ├── magepack-bundle.mjs ├── magepack-generate.mjs ├── sass-lint.mjs ├── setup.mjs ├── styles.mjs ├── svg.mjs └── watch.mjs └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | charset = utf-8 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | 7 | [*.{js, json}] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: 2 | - eslint:recommended 3 | - idiomatic 4 | parserOptions: 5 | ecmaVersion: 2017 6 | sourceType: module 7 | env: 8 | node: true 9 | es6: true 10 | rules: 11 | no-multi-spaces: 0 12 | space-in-parens: 0 13 | array-bracket-spacing: 0 14 | computed-property-spacing: 0 15 | key-spacing: 0 16 | no-var: 2 17 | indent: 18 | - 2 19 | - 2 20 | - 21 | SwitchCase: 1 22 | VariableDeclarator: 23 | var: 2 24 | let: 2 25 | const: 3 26 | brace-style: 27 | - 2 28 | - 'stroustrup' 29 | - 30 | allowSingleLine: false 31 | one-var: 32 | - 2 33 | - never 34 | semi: 35 | - 2 36 | - never 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /**/node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | >=14.0.0 <17.0.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 12 3 | cache: 4 | yarn: true 5 | directories: 6 | - node_modules 7 | script: 8 | - yarn test 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Snowdog Apps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **DEPRECATED** 2 | 3 | ## This is no longer supported, please consider using [Cream fork](https://github.com/creaminternet/magento2-frontools) instead. 4 | 5 | 6 | [![Packagist](https://img.shields.io/packagist/v/snowdog/frontools.svg)](https://packagist.org/packages/snowdog/frontools) [![Packagist](https://img.shields.io/packagist/dt/snowdog/frontools.svg)](https://packagist.org/packages/snowdog/frontools) 7 | 8 | ## Magento 2 Frontools 9 | Set of front-end tools for Magento 2 10 | 11 | ## Requirements 12 | * Unix-like OS (please, do not ask about Windows support) 13 | * Node.js [LTS version](https://nodejs.org/en/about/releases/). We recommend using [Volta](https://volta.sh/). 14 | * Magento 2 project with SASS based theme for example [SASS version of "Blank"](https://github.com/SnowdogApps/magento2-theme-blank-sass) or [Alpaca Theme](https://github.com/SnowdogApps/magento2-alpaca-theme) 15 | 16 | ## Installation 17 | 1. Run `composer require snowdog/frontools` 18 | 2. Go to package directory `cd vendor/snowdog/frontools` 19 | 3. Run `yarn` or `npm install` 20 | 4. Decide where you want to keep your config files. 21 | You can store them in Frontools `config` directory or in `dev/tools/frontools/config` (recommended). 22 | There is a `setup` task to copy all sample config files from the `config` to `dev/tools/frontools/config` and create a convenient symlink `tools` in the project root. 23 | If you want to keep config files inside frontools `config` dir, you have to handle this manually. 24 | 5. Define your themes in `themes.json`. 25 | 26 | ## `themes.json` structure 27 | Check `config/themes.json.sample` to get samples. 28 | - `src` - full path to theme 29 | - `dest` - full path to `pub/static/[theme_area]/[theme_vendor]/[theme_name]` 30 | - `locale` - array of available locales 31 | - `parent` - name of parent theme 32 | - `stylesDir` - (default `styles`) path to styles directory. For `theme-blank-sass` it's `styles`. By default Magento 2 use `web/css`. 33 | - `disableSuffix` - disable adding `.min` suffix using `--prod` flag. 34 | - `postcss` - (default `["autoprefixer({ overrideBrowserslist: browserslist })"]`) PostCSS plugins config. Have to be an array. 35 | - `modules` - list of modules witch you want to map inside your theme 36 | - `ignore` - array of ignore patterns 37 | 38 | ## `watcher.json` structure 39 | Check `config/watcher.json.sample` to get samples. 40 | - `usePolling` - set this to `true` to successfully watch files over a network (i.e. Docker or Vagrant) or when your watcher dosen't work well. Warning, enabling this option may lead to high CPU utilization! [chokidar docs](https://github.com/paulmillr/chokidar#performance) 41 | 42 | ## `sass-compiler.json` structure 43 | You can choose Sass compiler between the default, but [already deprecated](https://github.com/sass/node-sass/issues/2952), `node-sass` or a newer and faster `dart-sass`. 44 | 45 | Since the Dart Sass does not have the same set of features as Node Sass, for now we will keep the older version as default. 46 | 47 | ## Optional configurations for 3rd party plugins 48 | You will find sample config files for theses plugins in `vendor/snowdog/frontools/config` directory. 49 | * Create [browserSync](https://www.browsersync.io/) configuration 50 | * Create [eslint](https://eslint.org/) configuration 51 | * Create [sass-lint](https://github.com/sasstools/sass-lint) configuration 52 | * Create [stylelint](https://github.com/stylelint/stylelint) configuration 53 | * Create [svg-sprite](https://github.com/jkphl/gulp-svg-sprite) configuration 54 | 55 | ## Tasks list 56 | Use `yarn [taskName]` or `npm run [taskName]` to run the task. 57 | * `babel` - Run [Babel](https://babeljs.io/), a compiler for writing next generation JavaScript. 58 | * `--theme name` - Process single theme. 59 | * `--prod` - Production output - minifies and uglyfy code. 60 | * `clean` - Removes `/pub/static` directory content. 61 | * `csslint` - Run [stylelint](https://github.com/stylelint/stylelint) based tests. 62 | * `--theme name` - Process single theme. 63 | * `--ci` - Enable throwing errors. Useful in CI/CD pipelines. 64 | * `dev` - Runs [browserSync](https://www.browsersync.io/) and `inheritance`, `babel`, `styles`, `watch` tasks. 65 | * `--theme name` - Process single theme. 66 | * `--disableLinting` - Disable JS, SASS, CSS linting. 67 | * `--disableMaps` - Disable inline source maps generation. 68 | * `emailfix` - Fixes email stylesheet for Magento < 2.3.0. [Related issue](https://github.com/MyIntervals/emogrifier/issues/296) 69 | * `--prod` - Production output - minifies styles and add `.min` sufix. 70 | * `eslint` - Run [eslint](https://eslint.org/) against all JS files. 71 | * `--theme name` - Process single theme. 72 | * `--fix` - Autofix errors 73 | * `--ci` - Enable throwing errors. Useful in CI/CD pipelines. 74 | * `inheritance` - Create necessary symlinks to resolve theme styles inheritance and make the base for styles processing. You have to run in before styles compilation and after adding new files. 75 | * `magepackBundle` - Run [magepack](https://github.com/magesuite/magepack) `bundle` command. 76 | * `-c` or `--config` - (required) Path to previously generated Magepack config file. 77 | * `--theme name` - Process single theme. 78 | * `magepackGenerate` - Run [magepack](https://github.com/magesuite/magepack) `generate` command. 79 | * `--cms-url` - (required) URL to one of CMS pages (e.g. homepage). 80 | * `--category-url` - (required) URL to one of category pages. 81 | * `--product-url` - (required) URL to one of product pages. 82 | * `-u` or `--auth-username` - Username for Basic Auth. 83 | * `-p` or `--auth-password` - Passoword for Basic Auth. 84 | * `-d` or `--debug` - Turn on debugging mode. 85 | * `sasslint` - Run [sass-lint](https://github.com/sasstools/sass-lint) based tests. 86 | * `--theme name` - Process single theme. 87 | * `--ci` - Enable throwing errors. Useful in CI/CD pipelines. 88 | * `setup` - Creates a convenient symlink from `/tools` to `/vendor/snowdog/frontools` and copies all sample files if no configuration exists. 89 | * `--symlink name` - If you don't want to use `tools` as the symlink you can specify another name. 90 | * `styles` - Use this task to manually trigger styles processing pipeline. 91 | * `--theme name` - Process single theme. 92 | * `--disableMaps` - Disable inline source maps generation. 93 | * `--prod` - Production output - minifies styles and add `.min` suffix. 94 | * `--ci` - Enable throwing errors. Useful in CI/CD pipelines. 95 | * `svg` - Run [svg-sprite](https://github.com/jkphl/gulp-svg-sprite) to generate SVG sprite. 96 | * `--theme name` - Process single theme. 97 | * `watch` - Watch for style changes and run processing tasks. 98 | * `--theme name` - Process single theme. 99 | * `--disableLinting` - Disable JS, SASS, CSS linting. 100 | * `--disableMaps` - Disable inline source maps generation. 101 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snowdog/frontools", 3 | "description": "Set of front-end tools for Magento 2, based on Gulp.js", 4 | "license": "MIT", 5 | "type": "magento2-component" 6 | } 7 | -------------------------------------------------------------------------------- /config/.browserslistrc.sample: -------------------------------------------------------------------------------- 1 | > 1% in alt-eu 2 | > 1% in alt-na 3 | -------------------------------------------------------------------------------- /config/.eslintrc.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:compat/recommended" 6 | ], 7 | "parserOptions": { 8 | "ecmaVersion": "latest" 9 | }, 10 | "env": { 11 | "browser": true, 12 | "amd": true 13 | }, 14 | "rules": { 15 | "array-bracket-spacing": [ 16 | "error", 17 | "never" 18 | ], 19 | "arrow-spacing": [ 20 | "error", 21 | { 22 | "before": true, 23 | "after": true 24 | } 25 | ], 26 | "block-spacing": [ 27 | "error", 28 | "always" 29 | ], 30 | "brace-style": [ 31 | "error", 32 | "stroustrup", 33 | { 34 | "allowSingleLine": false 35 | } 36 | ], 37 | "comma-spacing": [ 38 | "error", 39 | { 40 | "before": false, 41 | "after": true 42 | } 43 | ], 44 | "comma-style": [ 45 | "error", 46 | "last" 47 | ], 48 | "computed-property-spacing": [ 49 | "error", 50 | "never" 51 | ], 52 | "curly": [ 53 | "error", 54 | "all" 55 | ], 56 | "dot-location": [ 57 | "error", 58 | "property" 59 | ], 60 | "eol-last": [ 61 | "error" 62 | ], 63 | "indent": [ 64 | "error", 65 | 2 66 | ], 67 | "key-spacing": [ 68 | "error", 69 | { 70 | "beforeColon": false, 71 | "afterColon": true 72 | } 73 | ], 74 | "keyword-spacing": [ 75 | "error" 76 | ], 77 | "linebreak-style": [ 78 | "error" 79 | ], 80 | "no-eq-null": [ 81 | "error" 82 | ], 83 | "no-func-assign": [ 84 | "error" 85 | ], 86 | "no-mixed-spaces-and-tabs": [ 87 | "error" 88 | ], 89 | "no-multi-spaces": [ 90 | "error" 91 | ], 92 | "no-spaced-func": [ 93 | "error" 94 | ], 95 | "no-trailing-spaces": [ 96 | "error" 97 | ], 98 | "object-curly-spacing": [ 99 | "error", 100 | "always" 101 | ], 102 | "one-var": [ 103 | "error", 104 | "never" 105 | ], 106 | "one-var-declaration-per-line": [ 107 | "error", 108 | "always" 109 | ], 110 | "quotes": [ 111 | "error", 112 | "single" 113 | ], 114 | "semi-spacing": [ 115 | "error", 116 | { 117 | "before": false, 118 | "after": true 119 | } 120 | ], 121 | "space-before-blocks": [ 122 | "error" 123 | ], 124 | "space-infix-ops": [ 125 | "error" 126 | ], 127 | "space-unary-ops": [ 128 | "error", 129 | { 130 | "words": true, 131 | "nonwords": false 132 | } 133 | ] 134 | }, 135 | "globals": { 136 | "module": "readonly" 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /config/babel.config.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /config/browser-sync.json.sample: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "proxy": "m2test.dev", 4 | "rewriteRules": [ 5 | { 6 | "match": ".m2test.dev", 7 | "replace": "" 8 | } 9 | ] 10 | }, 11 | { 12 | "proxy": "b2b.m2test.dev", 13 | "rewriteRules": [ 14 | { 15 | "match": ".b2b.m2test.dev", 16 | "replace": "" 17 | } 18 | ] 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /config/sass-compiler.json.sample: -------------------------------------------------------------------------------- 1 | "node-sass" 2 | -------------------------------------------------------------------------------- /config/sass-lint.yml.sample: -------------------------------------------------------------------------------- 1 | # Linter Options 2 | options: 3 | merge-default-rules: false 4 | 5 | # Rule Configuration 6 | rules: 7 | brace-style: 8 | - 2 9 | - 10 | style: 'stroustrup' 11 | clean-import-paths: 2 12 | extends-before-declarations: 2 13 | extends-before-mixins: 2 14 | final-newline: 2 15 | indentation: 16 | - 2 17 | - 18 | size: 4 19 | leading-zero: 20 | - 2 21 | - 22 | include: true 23 | no-css-comments: 2 24 | no-debug: 1 25 | no-empty-rulesets: 2 26 | no-ids: 1 27 | no-important: 1 28 | no-invalid-hex: 2 29 | no-misspelled-properties: 30 | - 2 31 | - 32 | extra-properties: 33 | - 'touch-callout' 34 | - 'overflow-scrolling' 35 | no-vendor-prefixes: 1 36 | no-warn: 1 37 | one-declaration-per-line: 2 38 | single-line-per-selector: 2 39 | space-after-colon: 2 40 | space-after-comma: 2 41 | space-around-operator: 2 42 | space-before-bang: 2 43 | space-before-brace: 2 44 | trailing-semicolon: 2 45 | zero-unit: 2 46 | -------------------------------------------------------------------------------- /config/stylelint.yml.sample: -------------------------------------------------------------------------------- 1 | rules: 2 | indentation: null 3 | -------------------------------------------------------------------------------- /config/svg-sprite.yml.sample: -------------------------------------------------------------------------------- 1 | symbol: 2 | dest: 'images' 3 | sprite: 'icons-sprite.svg' 4 | -------------------------------------------------------------------------------- /config/themes.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "blank": { 3 | "src": "vendor/snowdog/theme-blank-sass", 4 | "dest": "pub/static/frontend/Snowdog/blank", 5 | "locale": ["en_US"], 6 | "ignore": [".test"] 7 | }, 8 | "basic-child": { 9 | "src": "vendor/snowdog/theme-basic-child", 10 | "dest": "pub/static/frontend/Snowdog/basic-child", 11 | "locale": ["en_US"], 12 | "parent": "blank" 13 | }, 14 | "child-with-locale-overwrites": { 15 | "src": "vendor/snowdog/theme-custom", 16 | "dest": "pub/static/frontend/Snowdog/child-with-locale-overwrites", 17 | "locale": ["en_US", "pl_PL"], 18 | "parent": "blank" 19 | }, 20 | "all-possible-options": { 21 | "src": "vendor/snowdog/theme-all-possible-options", 22 | "dest": "pub/static/frontend/Snowdog/all-possible-options", 23 | "locale": ["en_US", "pl_PL"], 24 | "parent": "blank", 25 | "stylesDir": "web/css", 26 | "includePaths": ["../../../node_modules"], 27 | "postcss": ["autoprefixer()"], 28 | "disableSuffix": true, 29 | "modules": { 30 | "Snowdog_Sample": "vendor/snowdog/module-sample" 31 | }, 32 | "ignore": [ 33 | "vendor/snowdog/module-sample/{docs,dist}/**", 34 | "*.xml" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /config/watcher.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "usePolling": false 3 | } 4 | -------------------------------------------------------------------------------- /gulpfile.mjs: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp' 2 | 3 | import pipelineHelper from './helpers/pipeline.mjs' 4 | 5 | import { babel as babelTask } from './tasks/babel.mjs' 6 | import { browserSync as browserSyncTask } from './tasks/browser-sync.mjs' 7 | import { clean as cleanTask } from './tasks/clean.mjs' 8 | import { csslint as cssLintTask } from './tasks/css-lint.mjs' 9 | import { emailFix as emailFixTask } from './tasks/email-fix.mjs' 10 | import { eslint as eslintTask } from './tasks/eslint.mjs' 11 | import { inheritance as inheritanceTask } from './tasks/inheritance.mjs' 12 | import { sasslint as sassLintTask } from './tasks/sass-lint.mjs' 13 | import { setup as setupTask } from './tasks/setup.mjs' 14 | import { styles as stylesTask } from './tasks/styles.mjs' 15 | import { svg as svgTask } from './tasks/svg.mjs' 16 | import { watch as watchTask } from './tasks/watch.mjs' 17 | import magepackBundleTask from './tasks/magepack-bundle.mjs' 18 | import magepackGenerateTask from './tasks/magepack-generate.mjs' 19 | 20 | export const babel = gulp.series(inheritanceTask, babelTask) 21 | export const clean = cleanTask 22 | export const csslint = cssLintTask 23 | export const dev = gulp.series(browserSyncTask, watchTask) 24 | export const emailfix = emailFixTask 25 | export const eslint = eslintTask 26 | export const inheritance = inheritanceTask 27 | export const sasslint = sassLintTask 28 | export const setup = setupTask 29 | export const styles = gulp.series(inheritanceTask, stylesTask) 30 | export const svg = gulp.series(inheritanceTask, svgTask) 31 | export const watch = watchTask 32 | export const magepackBundle = magepackBundleTask 33 | export const magepackGenerate = magepackGenerateTask 34 | 35 | export { default as default } from './tasks/default.mjs' 36 | -------------------------------------------------------------------------------- /helpers/babel.mjs: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp' 2 | import path from 'path' 3 | import fs from 'fs-extra' 4 | import { globbySync } from 'globby' 5 | import gulpIf from 'gulp-if' 6 | import babel from 'gulp-babel' 7 | import rename from 'gulp-rename' 8 | import multiDest from 'gulp-multi-dest' 9 | import logger from 'gulp-logger' 10 | import plumber from 'gulp-plumber' 11 | import notify from 'gulp-notify' 12 | import sourcemaps from 'gulp-sourcemaps' 13 | import uglify from 'gulp-uglify' 14 | 15 | import configLoader from './config-loader.mjs' 16 | import { env, themes, tempPath, projectPath, browserSyncInstances } from './config.mjs' 17 | 18 | export default (name, file) => { 19 | const theme = themes[name] 20 | const srcBase = path.join(tempPath, theme.dest) 21 | const dest = [] 22 | const disableMaps = env.disableMaps || false 23 | const production = env.prod || false 24 | 25 | configLoader('.browserslistrc') 26 | 27 | function adjustDestinationDirectory(file) { 28 | file.dirname = file.dirname.replace('web/', '') 29 | return file 30 | } 31 | 32 | theme.locale.forEach(locale => { 33 | dest.push(path.join(projectPath, theme.dest, locale)) 34 | }) 35 | 36 | // Cleanup existing files from pub to remove symlinks 37 | globbySync(file || srcBase + '/**/*.babel.js') 38 | .forEach(file => { 39 | theme.locale.forEach(locale => { 40 | fs.removeSync( 41 | file 42 | .replace( 43 | srcBase, 44 | path.join(projectPath, theme.dest, locale) 45 | ) 46 | .replace( 47 | new RegExp('web/([^_]*)$'), 48 | '$1' 49 | ) 50 | ) 51 | }) 52 | }) 53 | 54 | const gulpTask = gulp.src( 55 | file || srcBase + '/**/*.babel.js', 56 | { base: srcBase } 57 | ) 58 | .pipe( 59 | gulpIf( 60 | !env.ci, 61 | plumber({ 62 | errorHandler: notify.onError('Error: <%= error.message %>') 63 | }) 64 | ) 65 | ) 66 | .pipe(gulpIf(!disableMaps, sourcemaps.init())) 67 | .pipe(babel(configLoader('babel.config.json'))) 68 | .pipe(gulpIf(production, uglify())) 69 | .pipe(gulpIf(production, rename({ suffix: '.min' }))) 70 | .pipe(gulpIf(!disableMaps, sourcemaps.write('.', { includeContent: true }))) 71 | .pipe(rename(adjustDestinationDirectory)) 72 | .pipe(multiDest(dest)) 73 | .pipe(logger({ 74 | display : 'name', 75 | beforeEach: 'Theme: ' + name + ' ', 76 | afterEach : ' Compiled!' 77 | })) 78 | 79 | if (browserSyncInstances) { 80 | Object.keys(browserSyncInstances).forEach(instanceKey => { 81 | const instance = browserSyncInstances[instanceKey] 82 | 83 | gulpTask.pipe(instance.stream()) 84 | }) 85 | } 86 | 87 | return gulpTask 88 | } 89 | -------------------------------------------------------------------------------- /helpers/config-loader.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | import { globbySync } from 'globby' 4 | import yaml from 'js-yaml' 5 | 6 | import errorMessage from './error-message.mjs' 7 | import { projectPath } from './config.mjs' 8 | 9 | function getContent(filePath) { 10 | if (filePath.endsWith('.browserslistrc')) { 11 | process.env.BROWSERSLIST_CONFIG = path.resolve(filePath) 12 | return 13 | } 14 | 15 | if (filePath.endsWith('.yml')) { 16 | return yaml.load(fs.readFileSync(filePath)) 17 | } 18 | 19 | if (filePath.endsWith('.json')) { 20 | return JSON.parse(fs.readFileSync(filePath)) 21 | } 22 | 23 | if (filePath.endsWith('.js')) { 24 | return require(filePath) 25 | } 26 | } 27 | 28 | export default (file, failOnError = true) => { 29 | const externalPath = path.join(projectPath, 'dev/tools/frontools/config/', file) 30 | 31 | // Check if file exists inside the config directory 32 | if (globbySync(externalPath).length) { 33 | return getContent(externalPath) 34 | } 35 | 36 | if (globbySync('config/' + file).length) { 37 | return getContent('config/' + file) 38 | } 39 | 40 | if (failOnError) { 41 | throw new Error(errorMessage('You have to create ' + file)) 42 | } 43 | 44 | return {} 45 | } 46 | -------------------------------------------------------------------------------- /helpers/config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | import parseArgs from 'minimist' 4 | 5 | import configLoader from './config-loader.mjs' 6 | 7 | export const env = parseArgs(process.argv.slice(2)) 8 | export const projectPath = path.join(fs.realpathSync('../../../'), '/') 9 | export const tempPath = path.join(projectPath, 'var/view_preprocessed/frontools/') 10 | export const themes = configLoader('themes.json', false) 11 | export const browserSyncInstances = {} 12 | -------------------------------------------------------------------------------- /helpers/css-lint.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import gulp from 'gulp' 3 | import { globbySync } from 'globby' 4 | import stylelint from 'stylelint' 5 | import postcss from 'gulp-postcss' 6 | import plumber from 'gulp-plumber' 7 | import gulpIf from 'gulp-if' 8 | import notify from 'gulp-notify' 9 | import logger from 'gulp-logger' 10 | import reporter from 'postcss-reporter' 11 | 12 | import { env, themes, projectPath } from './config.mjs' 13 | import configLoader from './config-loader.mjs' 14 | 15 | export default (name, file) => { 16 | const theme = themes[name] 17 | const srcBase = path.join(projectPath, theme.dest) 18 | const stylelintConfig = configLoader('stylelint.yml') 19 | const files = globbySync(srcBase + '/**/*.css') 20 | 21 | return gulp.src(file ? file : files.length ? files : '.') 22 | .pipe(gulpIf( 23 | !env.ci, 24 | plumber({ 25 | errorHandler: notify.onError('Error: <%= error.message %>') 26 | }) 27 | )) 28 | .pipe(postcss([ 29 | stylelint({ 30 | config: stylelintConfig 31 | }), 32 | reporter({ 33 | clearReportedMessages: true, 34 | throwError: env.ci || false 35 | }) 36 | ])) 37 | .pipe(logger({ 38 | display : 'name', 39 | beforeEach: 'Theme: ' + name + ' ' + 'File: ', 40 | afterEach : ' - CSS Lint finished.' 41 | })) 42 | } 43 | -------------------------------------------------------------------------------- /helpers/dependency-tree-builder.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | 4 | function findDependencies(file, dependencyTree) { 5 | if (!fs.existsSync(file)) { 6 | return dependencyTree 7 | } 8 | 9 | const content = fs.readFileSync(file, 'utf8') 10 | const srcPath = file.replace(/(.*)\/.*/g, '$1') 11 | const regex = /^(?:\s*@import )(?:'|")(.*)(?:'|")/gm 12 | 13 | let result = regex.exec(content) 14 | let imports = [] 15 | 16 | while (result) { 17 | let fullPath = '' 18 | if (result[1].includes('../')) { 19 | let parentPath = srcPath 20 | let filePath = result[1] 21 | 22 | while (filePath.includes('../')) { 23 | parentPath = parentPath.replace(/\/[^/]+$/g, '') 24 | filePath = filePath.replace(/\.\.\//, '') 25 | const filePathParts = /(.*)\/(.*)/g.exec(filePath) 26 | if (filePathParts) { 27 | fullPath = path.join(parentPath, filePathParts[1], `_${filePathParts[filePathParts.length - 1]}.scss`) 28 | } 29 | else { 30 | fullPath = path.join(parentPath, `_${filePath}.scss`) 31 | } 32 | } 33 | } 34 | else { 35 | if (result[1].includes('/')) { 36 | const filePath = /(.*)\/(.*)/g.exec(result[1]) 37 | fullPath = path.join(srcPath, filePath[1], `_${filePath[filePath.length - 1]}.scss`) 38 | } 39 | else { 40 | fullPath = path.join(srcPath, `_${result[1]}.scss`) 41 | } 42 | } 43 | imports.push(fullPath) 44 | result = regex.exec(content) 45 | } 46 | 47 | imports.forEach(el => { 48 | imports = imports.concat(findDependencies(el, dependencyTree)) 49 | }) 50 | 51 | dependencyTree = dependencyTree.concat(file) 52 | dependencyTree = dependencyTree.concat(imports) 53 | 54 | return dependencyTree 55 | } 56 | 57 | export default file => { 58 | return findDependencies(file, []) 59 | } 60 | -------------------------------------------------------------------------------- /helpers/email-fix.mjs: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp' 2 | import path from 'path' 3 | import gulpIf from 'gulp-if' 4 | import plumber from 'gulp-plumber' 5 | import notify from 'gulp-notify' 6 | import logger from 'gulp-logger' 7 | import replace from 'gulp-replace' 8 | 9 | import { env, projectPath, themes } from './config.mjs' 10 | 11 | export default name => { 12 | const production = env.prod || false 13 | const theme = themes[name] 14 | const srcBase = path.join(projectPath, theme.dest) 15 | const emailFilename = production ? 'email-inline.min.css' : 'email-inline.css' 16 | 17 | return gulp.src(srcBase + '/**/*/' + emailFilename, { base: './' }) 18 | .pipe(gulpIf( 19 | !env.ci, 20 | plumber({ 21 | errorHandler: notify.onError('Error: <%= error.message %>') 22 | }) 23 | )) 24 | .pipe(logger({ 25 | display : 'rel', 26 | beforeEach: 'Email styles from: ', 27 | afterEach: ' has been fixed!' 28 | })) 29 | .pipe(replace('@charset "UTF-8";', '')) 30 | .pipe(gulp.dest('./')) 31 | } 32 | -------------------------------------------------------------------------------- /helpers/error-message.mjs: -------------------------------------------------------------------------------- 1 | import colors from 'ansi-colors' 2 | 3 | export default message => { 4 | const lineLength = message.length > 50 ? 50 : message.length 5 | return ` 6 | ${colors.red('='.repeat(lineLength))} 7 | ${colors.yellow(message)} 8 | ${colors.red('='.repeat(lineLength))} 9 | ` 10 | } 11 | -------------------------------------------------------------------------------- /helpers/eslint.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | import { globby } from 'globby' 4 | import { ESLint } from 'eslint' 5 | import progress from 'progress' 6 | import log from 'fancy-log' 7 | import colors from 'ansi-colors' 8 | 9 | import configLoader from '../helpers/config-loader.mjs' 10 | import { env, themes, projectPath } from '../helpers/config.mjs' 11 | 12 | export default async (name, file) => { 13 | const theme = themes[name] 14 | const themePath = path.resolve(projectPath, theme.src) + '/' 15 | 16 | configLoader('.browserslistrc') 17 | 18 | const eslint = new ESLint({ 19 | cwd: themePath, 20 | baseConfig: configLoader('.eslintrc.json'), 21 | resolvePluginsRelativeTo: process.cwd(), 22 | useEslintrc: false, 23 | fix: env.fix 24 | }) 25 | 26 | const formatter = await eslint.loadFormatter('stylish') 27 | 28 | const paths = [] 29 | 30 | if (file) { 31 | paths.push(file) 32 | } 33 | else { 34 | paths.push('**/*.js', '!**/node_modules/**') 35 | 36 | if (await fs.pathExists(themePath + '.eslintignore')) { 37 | const ignore = await fs.readFile(themePath + '.eslintignore', 'utf8') 38 | ignore.split('\n').forEach(path => { 39 | if (path) { 40 | paths.push('!' + path) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | const files = await globby(paths, { absolute: true, cwd: themePath }) 47 | 48 | const progressBar = new progress(':percent | ETA: :etas | :current/:total | [:bar]', { 49 | total: files.length, 50 | clear: true 51 | }) 52 | 53 | const resultText = [] 54 | 55 | await Promise.all(files.map(async filePath => { 56 | const code = await fs.readFile(filePath, 'utf8') 57 | const results = await eslint.lintText(code, { filePath }) 58 | 59 | progressBar.tick() 60 | 61 | if (!results[0].messages.length) { 62 | return 63 | } 64 | 65 | if (env.fix) { 66 | await ESLint.outputFixes(results) 67 | } 68 | 69 | resultText.push(formatter.format(results)) 70 | })) 71 | 72 | if (resultText.length) { 73 | log(resultText.join('\n')) 74 | 75 | if (env.ci) { 76 | process.exit(1) 77 | } 78 | } 79 | else { 80 | log(colors.green('ESLint found no problems!')) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /helpers/get-themes.mjs: -------------------------------------------------------------------------------- 1 | import { env } from './config.mjs' 2 | import errorMessage from './error-message.mjs' 3 | import configLoader from './config-loader.mjs' 4 | 5 | export default () => { 6 | const themes = configLoader('themes.json') 7 | 8 | const themeName = env.theme || false 9 | const themesNames = Object.keys(themes) 10 | 11 | if (themeName && themesNames.indexOf(themeName) === -1) { 12 | throw new Error(errorMessage(themeName + ' theme is not defined in themes.json')) 13 | } 14 | 15 | return themeName ? [themeName] : themesNames 16 | } 17 | -------------------------------------------------------------------------------- /helpers/inheritance-resolver.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import { globbySync } from 'globby' 3 | import path from 'path' 4 | 5 | import { themes, projectPath, tempPath } from './config.mjs' 6 | 7 | function createSymlink(srcPath, destPath) { 8 | fs.removeSync(destPath) 9 | fs.ensureSymlinkSync(srcPath, destPath) 10 | } 11 | 12 | function generateSymlinks(src, dest, replacePattern, ignore = []) { 13 | globbySync( 14 | [src + '/**'].concat(ignore.map(pattern => '!**/' + pattern)), 15 | { nodir: true } 16 | ).forEach(srcPath => { 17 | createSymlink( 18 | srcPath, 19 | path.join(dest, srcPath).replace(src + '/', replacePattern + '/') 20 | ) 21 | }) 22 | } 23 | 24 | function themeDependencyTree(themeName, tree, dependencyTree) { 25 | dependencyTree = dependencyTree ? dependencyTree : [] 26 | dependencyTree.push(themeName) 27 | 28 | if (!tree) { 29 | return dependencyTree 30 | } 31 | 32 | if (themes[themeName].parent) { 33 | return themeDependencyTree( 34 | themes[themeName].parent, 35 | tree, 36 | dependencyTree 37 | ) 38 | } 39 | else { 40 | return dependencyTree.reverse() 41 | } 42 | } 43 | 44 | export default function(name, tree = true) { 45 | return new Promise(resolve => { 46 | themeDependencyTree(name, tree).forEach(themeName => { 47 | const theme = themes[themeName] 48 | const themeSrc = path.join(projectPath, theme.src) 49 | const themeDest = path.join(tempPath, theme.dest) 50 | 51 | // Clean destination dir before generating new symlinks 52 | fs.removeSync(themeDest) 53 | 54 | // Create symlinks for parent theme 55 | if (theme.parent) { 56 | const parentSrc = path.join(tempPath, themes[theme.parent].dest) 57 | generateSymlinks(parentSrc, themeDest, '', themes[theme.parent].ignore) 58 | } 59 | 60 | // Create symlinks for theme modules 61 | if (theme.modules) { 62 | Object.keys(theme.modules).forEach(name => { 63 | const moduleSrc = path.join(projectPath, theme.modules[name]) 64 | generateSymlinks( 65 | moduleSrc, 66 | themeDest, 67 | '/' + name, 68 | theme.ignore 69 | ) 70 | }) 71 | } 72 | 73 | // Create symlinks to all files in this theme. Will overwritte parent symlinks if exist. 74 | generateSymlinks(themeSrc, themeDest, '', theme.ignore) 75 | }) 76 | 77 | resolve() 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /helpers/pipeline.mjs: -------------------------------------------------------------------------------- 1 | import { env } from './config.mjs' 2 | 3 | export default callback => { 4 | env.pipeline = true 5 | callback() 6 | } 7 | -------------------------------------------------------------------------------- /helpers/sass-error.mjs: -------------------------------------------------------------------------------- 1 | import PluginError from 'plugin-error' 2 | 3 | export default fail => { 4 | return function(error) { // eslint-disable-line func-names 5 | const message = new PluginError('sass', error.messageFormatted).toString() 6 | 7 | // Throw error instead of logging it if module is set to fail on error 8 | if (fail) { 9 | throw message 10 | } 11 | 12 | process.stderr.write(message + '\n') 13 | this.emit('end') 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /helpers/sass-lint.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import gulp from 'gulp' 3 | import { globbySync } from 'globby' 4 | import plumber from 'gulp-plumber' 5 | import gulpIf from 'gulp-if' 6 | import notify from 'gulp-notify' 7 | import logger from 'gulp-logger' 8 | import sassLint from 'gulp-sass-lint' 9 | 10 | import { env, themes, tempPath } from './config.mjs' 11 | import configLoader from './config-loader.mjs' 12 | 13 | export default (name, file) => { 14 | const theme = themes[name] 15 | const srcBase = path.join(tempPath, theme.dest) 16 | const sassLintConfig = configLoader('sass-lint.yml') 17 | const files = globbySync(srcBase + '/**/*.scss') 18 | 19 | return gulp.src(file ? file : files.length ? files : '.') 20 | .pipe(gulpIf( 21 | !env.ci, 22 | plumber({ 23 | errorHandler: notify.onError('Error: <%= error.message %>') 24 | }) 25 | )) 26 | .pipe(sassLint(sassLintConfig)) 27 | .pipe(sassLint.format()) 28 | .pipe(gulpIf(env.ci, sassLint.failOnError())) 29 | .pipe(logger({ 30 | display : 'name', 31 | beforeEach: 'Theme: ' + name + ' ' + 'File: ', 32 | afterEach : ' - SASS Lint finished.' 33 | })) 34 | } 35 | -------------------------------------------------------------------------------- /helpers/scss.mjs: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp' 2 | import path from 'path' 3 | import gulpIf from 'gulp-if' 4 | import dartSass from 'sass' 5 | import nodeSass from 'node-sass' 6 | import gulpSass from 'gulp-sass' 7 | import rename from 'gulp-rename' 8 | import multiDest from 'gulp-multi-dest' 9 | import logger from 'gulp-logger' 10 | import plumber from 'gulp-plumber' 11 | import notify from 'gulp-notify' 12 | import sourcemaps from 'gulp-sourcemaps' 13 | import cssnano from 'cssnano' 14 | import autoprefixer from 'autoprefixer' 15 | import postcss from 'gulp-postcss' 16 | 17 | import configLoader from './config-loader.mjs' 18 | import sassError from './sass-error.mjs' 19 | import { env, themes, tempPath, projectPath, browserSyncInstances } from './config.mjs' 20 | 21 | export default function(name, file) { 22 | const theme = themes[name] 23 | const srcBase = path.join(tempPath, theme.dest) 24 | const stylesDir = theme.stylesDir ? theme.stylesDir : 'styles' 25 | const dest = [] 26 | const disableMaps = env.disableMaps || false 27 | const production = env.prod || false 28 | const includePaths = theme.includePaths ? theme.includePaths : [] 29 | const postcssConfig = [] 30 | const disableSuffix = theme.disableSuffix || false 31 | const sassCompiler = configLoader('sass-compiler.json', false) 32 | 33 | configLoader('.browserslistrc') 34 | 35 | if (theme.postcss) { 36 | theme.postcss.forEach(el => { 37 | postcssConfig.push(eval(el)) 38 | }) 39 | } 40 | else { 41 | postcssConfig.push(autoprefixer()) 42 | } 43 | 44 | function adjustDestinationDirectory(file) { 45 | if (file.dirname.startsWith(stylesDir)) { 46 | file.dirname = file.dirname.replace(stylesDir, 'css') 47 | } 48 | else { 49 | file.dirname = file.dirname.replace('/' + stylesDir, '') 50 | } 51 | return file 52 | } 53 | 54 | theme.locale.forEach(locale => { 55 | dest.push(path.join(projectPath, theme.dest, locale)) 56 | }) 57 | 58 | const gulpTask = gulp.src( // eslint-disable-line one-var 59 | file || srcBase + '/**/*.scss', 60 | { base: srcBase } 61 | ) 62 | .pipe( 63 | gulpIf( 64 | !env.ci, 65 | plumber({ 66 | errorHandler: notify.onError('Error: <%= error.message %>') 67 | }) 68 | ) 69 | ) 70 | .pipe(gulpIf(!disableMaps, sourcemaps.init())) 71 | .pipe(gulpSass(sassCompiler === 'dart-sass' ? dartSass : nodeSass)({ includePaths: includePaths }) 72 | .on('error', sassError(env.ci || false))) 73 | .pipe(gulpIf(production, postcss([cssnano()]))) 74 | .pipe(gulpIf(postcssConfig.length, postcss(postcssConfig || []))) 75 | .pipe(gulpIf(production && !disableSuffix, rename({ suffix: '.min' }))) 76 | .pipe(gulpIf(!disableMaps, sourcemaps.write('.', { includeContent: true }))) 77 | .pipe(rename(adjustDestinationDirectory)) 78 | .pipe(multiDest(dest)) 79 | .pipe(logger({ 80 | display : 'name', 81 | beforeEach: 'Theme: ' + name + ' ', 82 | afterEach : ' Compiled!' 83 | })) 84 | 85 | if (browserSyncInstances) { 86 | Object.keys(browserSyncInstances).forEach(instanceKey => { 87 | const instance = browserSyncInstances[instanceKey] 88 | 89 | gulpTask.pipe(instance.stream()) 90 | }) 91 | } 92 | 93 | return gulpTask 94 | } 95 | -------------------------------------------------------------------------------- /helpers/svg.mjs: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp' 2 | import path from 'path' 3 | import gulpIf from 'gulp-if' 4 | import multiDest from 'gulp-multi-dest' 5 | import logger from 'gulp-logger' 6 | import plumber from 'gulp-plumber' 7 | import notify from 'gulp-notify' 8 | import svgSprite from 'gulp-svg-sprite' 9 | 10 | import configLoader from './config-loader.mjs' 11 | import { env, tempPath, projectPath, themes, browserSyncInstances } from './config.mjs' 12 | 13 | export default name => { 14 | const theme = themes[name] 15 | const srcBase = path.join(tempPath, theme.dest) 16 | const dest = [] 17 | const svgConfig = configLoader('svg-sprite.yml') 18 | 19 | theme.locale.forEach(locale => { 20 | dest.push(path.join(projectPath, theme.dest, locale)) 21 | }) 22 | 23 | const gulpTask = gulp.src(srcBase + '/**/icons/**/*.svg') 24 | .pipe( 25 | gulpIf( 26 | !env.ci, 27 | plumber({ 28 | errorHandler: notify.onError('Error: <%= error.message %>') 29 | }) 30 | ) 31 | ) 32 | .pipe(svgSprite({ 33 | shape: { 34 | id: { 35 | generator: file => path.basename(file, '.svg') 36 | } 37 | }, 38 | mode: svgConfig 39 | })) 40 | .pipe(multiDest(dest)) 41 | .pipe(logger({ 42 | display : 'name', 43 | beforeEach: 'Theme: ' + name + ' ', 44 | afterEach : ' Compiled!' 45 | })) 46 | 47 | if (browserSyncInstances) { 48 | Object.keys(browserSyncInstances).forEach(instanceKey => { 49 | const instance = browserSyncInstances[instanceKey] 50 | 51 | gulpTask.pipe(instance.stream()) 52 | }) 53 | } 54 | 55 | return gulpTask 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magento2-frontools", 3 | "version": "1.12.2", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/SnowdogApps/magento2-frontools" 8 | }, 9 | "license": "MIT", 10 | "dependencies": { 11 | "@babel/core": "^7.15.8", 12 | "@babel/preset-env": "^7.15.8", 13 | "ansi-colors": "^4.1.1", 14 | "autoprefixer": "^10.3.7", 15 | "browser-sync": "^2.27.5", 16 | "chokidar": "^3.5.2", 17 | "cssnano": "^5.0.8", 18 | "eslint": "^8.1.0", 19 | "eslint-plugin-compat": "^3.13.0", 20 | "fancy-log": "^1.3.3", 21 | "fs-extra": "^10.0.0", 22 | "globby": "^12.0.2", 23 | "gulp": "^4.0.2", 24 | "gulp-babel": "^8.0.0", 25 | "gulp-concat": "^2.6.1", 26 | "gulp-if": "^3.0.0", 27 | "gulp-logger": "^0.0.2", 28 | "gulp-multi-dest": "^1.3.7", 29 | "gulp-notify": "^4.0.0", 30 | "gulp-plumber": "^1.2.1", 31 | "gulp-postcss": "^9.0.1", 32 | "gulp-rename": "^2.0.0", 33 | "gulp-replace": "^1.1.3", 34 | "gulp-rimraf": "^1.0.0", 35 | "gulp-sass": "^5.0.0", 36 | "gulp-sass-lint": "^1.4.0", 37 | "gulp-sourcemaps": "^3.0.0", 38 | "gulp-svg-sprite": "^1.5.0", 39 | "gulp-uglify": "^3.0.2", 40 | "js-yaml": "^4.1.0", 41 | "magepack": "^2.7.2", 42 | "marked": "^3.0.8", 43 | "marked-terminal": "^4.2.0", 44 | "merge-stream": "^2.0.0", 45 | "minimist": "^1.2.5", 46 | "node-sass": "^6.0.1", 47 | "postcss": "^8.4.5", 48 | "postcss-reporter": "^7.0.4", 49 | "progress": "^2.0.3", 50 | "run-sequence": "^2.2.1", 51 | "sass": "^1.43.3", 52 | "stylelint": "^14.0.0" 53 | }, 54 | "scripts": { 55 | "babel": "gulp babel", 56 | "clean": "gulp clean", 57 | "csslint": "gulp csslint", 58 | "default": "gulp", 59 | "dev": "gulp dev", 60 | "email-fix": "gulp emailfix", 61 | "emailfix": "gulp emailfix", 62 | "eslint": "gulp eslint", 63 | "inheritance": "gulp inheritance", 64 | "magepackBundle": "gulp magepackBundle", 65 | "magepackGenerate": "gulp magepackGenerate", 66 | "sasslint": "gulp sasslint", 67 | "setup": "gulp setup", 68 | "styles": "gulp styles", 69 | "svg": "gulp svg", 70 | "watch": "gulp watch", 71 | "test": "eslint *.js helpers/*.js tasks/*.js" 72 | }, 73 | "engines": { 74 | "node": ">=14.14.0 <15.0.0 || >=16.12.0 <17.0.0" 75 | }, 76 | "volta": { 77 | "node": "16.12.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tasks/babel.mjs: -------------------------------------------------------------------------------- 1 | import mergeStream from 'merge-stream' 2 | import helper from '../helpers/babel.mjs' 3 | import themes from '../helpers/get-themes.mjs' 4 | 5 | export const babel = () => { 6 | const streams = mergeStream() 7 | themes().forEach(name => { 8 | streams.add(helper(name)) 9 | }) 10 | return streams 11 | } 12 | -------------------------------------------------------------------------------- /tasks/browser-sync.mjs: -------------------------------------------------------------------------------- 1 | import bs from 'browser-sync' 2 | 3 | import configLoader from '../helpers/config-loader.mjs' 4 | import { browserSyncInstances } from '../helpers/config.mjs' 5 | 6 | export const browserSync = async() => { 7 | let browserSyncConfig = configLoader('browser-sync.json') 8 | 9 | if (!Array.isArray(browserSyncConfig)) { 10 | browserSyncConfig = [browserSyncConfig] 11 | } 12 | 13 | // Setup browser-sync 14 | await Promise.all(browserSyncConfig.map((bsConfig, index) => { 15 | const instance = `browserSyncInstance${index}` 16 | 17 | if (!bsConfig.port) { 18 | bsConfig.port = 3000 + (100 * index) 19 | } 20 | 21 | bsConfig.ui = bsConfig.ui ? bsConfig.ui : {} 22 | 23 | if (!bsConfig.ui.port) { 24 | bsConfig.ui.port = 3000 + (100 * index) + 1 25 | } 26 | 27 | return new Promise(resolve => { 28 | browserSyncInstances[instance] = bs.create() 29 | browserSyncInstances[instance].init(bsConfig, () => resolve()) 30 | }) 31 | })) 32 | } 33 | -------------------------------------------------------------------------------- /tasks/clean.mjs: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp' 2 | import rimraf from 'gulp-rimraf' 3 | 4 | import { projectPath } from '../helpers/config.mjs' 5 | 6 | export const clean = () => { 7 | // Remove all files under pub/static, except .htaccess 8 | return gulp.src([ 9 | projectPath + 'pub/static/*', 10 | '!' + projectPath + 'pub/static/.htaccess' 11 | ], { read: false }) 12 | .pipe(rimraf({ force: true })) 13 | } 14 | -------------------------------------------------------------------------------- /tasks/css-lint.mjs: -------------------------------------------------------------------------------- 1 | import mergeStream from 'merge-stream' 2 | import log from 'fancy-log' 3 | import colors from 'ansi-colors' 4 | 5 | import helper from '../helpers/css-lint.mjs' 6 | import themes from '../helpers/get-themes.mjs' 7 | 8 | export const csslint = () => { 9 | const streams = mergeStream() 10 | themes().forEach(name => { 11 | log(`${colors.green('Runing CSS Lint on')} ${colors.blue(name)} ${colors.green('theme...')}`) 12 | streams.add(helper(name)) 13 | }) 14 | 15 | return streams 16 | } 17 | -------------------------------------------------------------------------------- /tasks/default.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import marked from 'marked' 3 | import markedTerminal from 'marked-terminal' 4 | import log from 'fancy-log' 5 | 6 | export default function(callback) { 7 | // Display formatted readme.md 8 | marked.setOptions({ 9 | renderer: new markedTerminal() 10 | }) 11 | 12 | log.info(marked(fs.readFileSync('./README.md', 'UTF-8'))) 13 | 14 | callback() 15 | } 16 | -------------------------------------------------------------------------------- /tasks/email-fix.mjs: -------------------------------------------------------------------------------- 1 | 2 | import mergeStream from 'merge-stream' 3 | import colors from 'ansi-colors' 4 | import log from 'fancy-log' 5 | 6 | import helper from '../helpers/email-fix.mjs' 7 | import themes from '../helpers/get-themes.mjs' 8 | 9 | export const emailFix = () => { 10 | const streams = mergeStream() 11 | themes().forEach(name => { 12 | log( 13 | `${colors.green('Runing email-fix on')} ${colors.blue(name)} ${colors.green('theme...')}` 14 | ) 15 | streams.add(helper(name)) 16 | }) 17 | 18 | return streams 19 | } 20 | -------------------------------------------------------------------------------- /tasks/eslint.mjs: -------------------------------------------------------------------------------- 1 | import log from 'fancy-log' 2 | import colors from 'ansi-colors' 3 | 4 | import helper from '../helpers/eslint.mjs' 5 | import themes from '../helpers/get-themes.mjs' 6 | 7 | export const eslint = async () => { 8 | await Promise.all( 9 | themes().map(async name => { 10 | log(`${colors.green('Runing ESLint on')} ${colors.blue(name)} ${colors.green('theme...')}`) 11 | await helper(name) 12 | }) 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /tasks/inheritance.mjs: -------------------------------------------------------------------------------- 1 | import themes from '../helpers/get-themes.mjs' 2 | import inheritanceResolver from '../helpers/inheritance-resolver.mjs' 3 | 4 | import { env } from '../helpers/config.mjs' 5 | 6 | export const inheritance = async() => { 7 | if (!env.pipeline) { 8 | await Promise.all( 9 | themes().map(name => inheritanceResolver(name)) 10 | ) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tasks/magepack-bundle.mjs: -------------------------------------------------------------------------------- 1 | import bundle from 'magepack/lib/bundle.js' 2 | 3 | import getThemes from '../helpers/get-themes.mjs' 4 | import { env, projectPath, themes } from '../helpers/config.mjs' 5 | 6 | function getThemesGlobPattern() { 7 | const themesNames = getThemes() 8 | 9 | if (themesNames.length === 1) { 10 | return `${projectPath}${themes[themesNames[0]].dest}/*` 11 | } 12 | 13 | return `${projectPath}{${themesNames.map(name => themes[name].dest).join(',')}}/*` 14 | } 15 | 16 | export default async function() { 17 | if (!env['c'] && !env['config']) { 18 | throw 'Please set the config path!' 19 | } 20 | 21 | const configPath = env.c || env.config 22 | const themesGlobPattern = getThemesGlobPattern() 23 | await bundle(configPath, themesGlobPattern) 24 | } 25 | -------------------------------------------------------------------------------- /tasks/magepack-generate.mjs: -------------------------------------------------------------------------------- 1 | import generate from 'magepack/lib/generate.js' 2 | 3 | import { env } from '../helpers/config.mjs' 4 | 5 | export default async function() { 6 | if (!env['cms-url'] || !env['category-url'] || !env['product-url']) { 7 | throw 'Please set the cms-url, category-url and product-url params!' 8 | } 9 | 10 | const config = { 11 | cmsUrl: env['cms-url'], 12 | categoryUrl: env['category-url'], 13 | productUrl: env['product-url'], 14 | authUsername: env.u || env['auth-username'], 15 | authPassword: env.p || env['auth-password'], 16 | debug: env.d || env.debug || false 17 | } 18 | 19 | await generate(config) 20 | } 21 | -------------------------------------------------------------------------------- /tasks/sass-lint.mjs: -------------------------------------------------------------------------------- 1 | import mergeStream from 'merge-stream' 2 | import log from 'fancy-log' 3 | import colors from 'ansi-colors' 4 | 5 | import helper from '../helpers/sass-lint.mjs' 6 | import themes from '../helpers/get-themes.mjs' 7 | 8 | export const sasslint = () => { 9 | const streams = mergeStream() 10 | themes().forEach(name => { 11 | log(`${colors.green('Runing SASS Lint on')} ${colors.blue(name)} ${colors.green('theme...')}`) 12 | streams.add(helper(name)) 13 | }) 14 | 15 | return streams 16 | } 17 | -------------------------------------------------------------------------------- /tasks/setup.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | import log from 'fancy-log' 4 | import colors from 'ansi-colors' 5 | 6 | import { env, projectPath } from '../helpers/config.mjs' 7 | 8 | export const setup = callback => { 9 | // Create a relative symlink in project root to /vendor/snowdog/frontools 10 | const relativeDirectory = path.relative( 11 | projectPath, 12 | fs.realpathSync('./') 13 | ) 14 | const symlinkDirectoryName = env.symlink || 'tools' 15 | 16 | // Set config files paths 17 | const configSamplesPath = './config/' 18 | const configPath = path.join(projectPath, 'dev/tools/frontools/config/') 19 | 20 | try { 21 | fs.symlinkSync( 22 | relativeDirectory, 23 | path.join(projectPath, symlinkDirectoryName), 24 | 'dir' 25 | ) 26 | 27 | log( 28 | colors.green('Symlink created. You can now use Frontools from the "' + symlinkDirectoryName + '" directory.') 29 | ) 30 | } 31 | catch (error) { 32 | log( 33 | colors.yellow(symlinkDirectoryName + ' already exists. Skipped it.') 34 | ) 35 | } 36 | 37 | // Copy all all non existent config files to /dev/tools/frontools/config/ 38 | fs.readdirSync(configSamplesPath).forEach((fileName) => { 39 | const newFileName = fileName.replace('.sample', '') 40 | 41 | try { 42 | fs.copySync( 43 | path.join(configSamplesPath, fileName), 44 | path.join(configPath, newFileName), { 45 | overwrite: false, 46 | errorOnExist: true 47 | } 48 | ) 49 | 50 | log('File ' + fileName + ' copied to /dev/tools/frontools/config/' + newFileName) 51 | } 52 | catch (error) { 53 | log( 54 | colors.yellow('File ' + newFileName + ' already exists. Skipped it.') 55 | ) 56 | } 57 | }) 58 | 59 | log( 60 | colors.green('Setup complete! Go to "/dev/tools/frontools/config/" directory and edit the configuration there.') 61 | ) 62 | 63 | callback() 64 | } 65 | -------------------------------------------------------------------------------- /tasks/styles.mjs: -------------------------------------------------------------------------------- 1 | import mergeStream from 'merge-stream' 2 | import helper from '../helpers/scss.mjs' 3 | import themes from '../helpers/get-themes.mjs' 4 | 5 | export const styles = () => { 6 | const streams = mergeStream() 7 | themes().forEach(name => { 8 | streams.add(helper(name)) 9 | }) 10 | return streams 11 | } 12 | -------------------------------------------------------------------------------- /tasks/svg.mjs: -------------------------------------------------------------------------------- 1 | import mergeStream from 'merge-stream' 2 | import helper from '../helpers/svg.mjs' 3 | import themes from '../helpers/get-themes.mjs' 4 | 5 | export const svg = () => { 6 | const streams = mergeStream() 7 | themes().forEach(name => { 8 | streams.add(helper(name)) 9 | }) 10 | return streams 11 | } 12 | -------------------------------------------------------------------------------- /tasks/watch.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | import chokidar from 'chokidar' 4 | import { globbySync } from 'globby' 5 | import colors from 'ansi-colors' 6 | import log from 'fancy-log' 7 | 8 | import babel from '../helpers/babel.mjs' 9 | import configLoader from '../helpers/config-loader.mjs' 10 | import cssLint from '../helpers/css-lint.mjs' 11 | import dependecyTree from '../helpers/dependency-tree-builder.mjs' 12 | import eslint from '../helpers/eslint.mjs' 13 | import inheritance from '../helpers/inheritance-resolver.mjs' 14 | import sass from '../helpers/scss.mjs' 15 | import sassLint from '../helpers/sass-lint.mjs' 16 | import svg from '../helpers/svg.mjs' 17 | 18 | import { env, themes, tempPath, projectPath, browserSyncInstances } from '../helpers/config.mjs' 19 | import getThemes from '../helpers/get-themes.mjs' 20 | 21 | export const watch = () => { 22 | log(colors.yellow('Initializing watcher...')) 23 | 24 | // Chokidar watcher config 25 | const watcherConfig = configLoader('watcher.json') 26 | watcherConfig.ignoreInitial = true 27 | 28 | getThemes().forEach(name => { 29 | const theme = themes[name] 30 | const themeTempSrc = path.join(tempPath, theme.dest) 31 | const themeDest = path.join(projectPath, theme.dest) 32 | const themeSrc = [path.join(projectPath, theme.src)] 33 | 34 | // Add modules source directeoried to theme source paths array 35 | if (theme.modules) { 36 | Object.keys(theme.modules).forEach(module => { 37 | themeSrc.push(path.join(projectPath, theme.modules[module])) 38 | }) 39 | } 40 | 41 | // Initialize watchers 42 | const tempWatcher = chokidar.watch(themeTempSrc, watcherConfig) 43 | const srcWatcher = chokidar.watch(themeSrc, watcherConfig) 44 | const destWatcher = chokidar.watch(themeDest, watcherConfig) 45 | 46 | let reinitTimeout = false 47 | let reinitPaths = [] 48 | let sassDependecyTree = {} 49 | 50 | function generateSassDependencyTree() { 51 | // Cleanup tree 52 | sassDependecyTree = {} 53 | 54 | // Find all main SASS files 55 | globbySync([ 56 | themeTempSrc + '/**/*.scss', 57 | '!/**/_*.scss' 58 | ]).forEach(file => { 59 | // Generate array of main file dependecies 60 | sassDependecyTree[file] = dependecyTree(file) 61 | }) 62 | } 63 | 64 | generateSassDependencyTree() 65 | 66 | function reinitialize(filePath) { 67 | // Reset previously set timeout 68 | clearTimeout(reinitTimeout) 69 | reinitPaths.push(filePath) 70 | 71 | // Timeout to run only once while moving or renaming files 72 | reinitTimeout = setTimeout(() => { 73 | const paths = reinitPaths 74 | reinitPaths = [] 75 | 76 | log( 77 | colors.yellow('Change detected.') + ' ' + 78 | colors.green('Theme:') + ' ' + 79 | colors.blue(name) + ' ' + 80 | colors.green(`${paths.length} file(s) changed`) 81 | ) 82 | 83 | log( 84 | colors.yellow('Resolving inheritance.') + ' ' + 85 | colors.green('Theme:') + ' ' + 86 | colors.blue(name) 87 | ) 88 | 89 | // Disable watcher to not fire tons of events while solving inheritance 90 | tempWatcher.unwatch(themeTempSrc) 91 | 92 | // Run inheritance resolver just for one theme without parent(s) 93 | inheritance(name, false).then(() => { 94 | // Regenerate SASS Dependency Tree 95 | generateSassDependencyTree() 96 | 97 | // Add all files to watch again after solving inheritance 98 | tempWatcher.add(themeTempSrc) 99 | 100 | // Emit event on added / moved / renamed / deleted file to trigger regualr pipeline 101 | paths.forEach(filePath => { 102 | if (fs.existsSync(filePath)) { 103 | globbySync(themeTempSrc + '/**/' + path.basename(filePath)) 104 | .forEach(file => { 105 | tempWatcher.emit('change', file) 106 | }) 107 | } 108 | }) 109 | }) 110 | }, 100) 111 | } 112 | 113 | // Watch add / move / rename / delete events on source files 114 | srcWatcher 115 | .on('add', reinitialize) 116 | .on('addDir', reinitialize) 117 | .on('unlink', reinitialize) 118 | .on('unlinkDir', reinitialize) 119 | .on('change', filePath => { 120 | // Linters 121 | if (env.disableLinting) { 122 | return 123 | } 124 | 125 | if (path.extname(filePath) === '.scss') { 126 | sassLint(name, filePath) 127 | } 128 | 129 | if (path.extname(filePath) === '.js') { 130 | eslint(name, filePath) 131 | } 132 | }) 133 | 134 | // print msg when temp dir watcher is initialized 135 | tempWatcher.on('ready', () => { 136 | log( 137 | colors.yellow('Watcher initialized!') + ' ' + 138 | colors.green('Theme:') + ' ' + 139 | colors.blue(name) + ' ' + 140 | colors.green('and dependencies...') 141 | ) 142 | }) 143 | 144 | // Events handling 145 | tempWatcher.on('change', filePath => { 146 | // Print message to know what's going on 147 | log( 148 | colors.yellow('Change detected.') + ' ' + 149 | colors.green('Theme:') + ' ' + 150 | colors.blue(name) + ' ' + 151 | colors.green('File:') + ' ' + 152 | colors.blue(path.relative(themeTempSrc, filePath)) 153 | ) 154 | 155 | // SASS Compilation 156 | if (path.extname(filePath) === '.scss') { 157 | Object.keys(sassDependecyTree).forEach(file => { 158 | if (sassDependecyTree[file].includes(filePath)) { 159 | sass(name, file) 160 | } 161 | }) 162 | } 163 | 164 | // Babel 165 | if (path.basename(filePath).endsWith('.babel.js')) { 166 | babel(name, filePath) 167 | } 168 | 169 | // SVG Sprite 170 | if (path.extname(filePath) === '.svg') { 171 | svg(name) 172 | } 173 | 174 | // Files that require reload after save 175 | if (['.html', '.phtml', '.xml', '.csv', '.js', '.vue'].some( 176 | ext => path.extname(filePath) === ext 177 | )) { 178 | if (browserSyncInstances) { 179 | Object.keys(browserSyncInstances).forEach((instanceKey) => { 180 | const instance = browserSyncInstances[instanceKey] 181 | instance.reload() 182 | }) 183 | } 184 | } 185 | }) 186 | 187 | destWatcher.on('change', filePath => { 188 | // CSS Lint 189 | if (!env.disableLinting) { 190 | if (path.extname(filePath) === '.css') { 191 | cssLint(name, filePath) 192 | } 193 | } 194 | }) 195 | }) 196 | } 197 | --------------------------------------------------------------------------------