├── .gitignore ├── src ├── template │ ├── .npmignore-t │ ├── src │ │ ├── locale │ │ │ └── en.js │ │ ├── blocks.js │ │ ├── components.js │ │ └── index.js │ ├── .gitignore-t │ ├── package.json │ ├── tsconfig.json │ ├── _index.html │ └── README.md ├── main.ts ├── banner.txt ├── serve.ts ├── utils.ts ├── webpack.config.ts ├── build.ts ├── cli.ts └── init.ts ├── babel.config.js ├── jest.config.ts ├── tsconfig.json ├── LICENSE ├── index.html ├── .github └── dependabot.yml ├── webpack.cli.ts ├── package.json ├── README.md └── test └── utils.spec.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | private/ 3 | node_modules/ 4 | .eslintrc 5 | *.log 6 | ./_index.html 7 | dist/ 8 | .npmrc -------------------------------------------------------------------------------- /src/template/.npmignore-t: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | *.html 4 | **/tsconfig.json 5 | **/webpack.config.js 6 | node_modules 7 | src -------------------------------------------------------------------------------- /src/template/src/locale/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '<%= rName %>': { 3 | // 'key': 'value', 4 | }, 5 | }; -------------------------------------------------------------------------------- /src/template/.gitignore-t: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | private/ 3 | /locale 4 | node_modules/ 5 | *.log 6 | _index.html 7 | dist/ 8 | stats.json -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | export { default as init } from './init'; 2 | export { default as build } from './build'; 3 | export { default as serve } from './serve'; -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | }; -------------------------------------------------------------------------------- /src/template/src/blocks.js: -------------------------------------------------------------------------------- 1 | export default (editor, opts = {}) => { 2 | const bm = editor.BlockManager; 3 | 4 | bm.add('MY-BLOCK', { 5 | label: 'My block', 6 | content: { type: 'MY-COMPONENT' }, 7 | // media: '...', 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | testEnvironment: 'node', 5 | verbose: true, 6 | modulePaths: ['/src'], 7 | testMatch: ['/test/**/*.(t|j)s'], 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /src/template/src/components.js: -------------------------------------------------------------------------------- 1 | export default (editor, opts = {}) => { 2 | const domc = editor.DomComponents; 3 | 4 | domc.addType('MY-COMPONENT', { 5 | model: { 6 | defaults: { 7 | // Default props 8 | }, 9 | }, 10 | view: { 11 | 12 | }, 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/banner.txt: -------------------------------------------------------------------------------- 1 |   ______ _______ ________ ____ 2 |   / ____/________ _____ ___ _____ / / ___/ / ____/ / / _/ 3 |  / / __/ ___/ __ `/ __ \/ _ \/ ___/_ / /\__ \______/ / / / / / 4 | / /_/ / / / /_/ / /_/ / __(__ ) /_/ /___/ /_____/ /___/ /____/ / 5 | \____/_/ \__,_/ .___/\___/____/\____//____/ \____/_____/___/ 6 |   /_/ -------------------------------------------------------------------------------- /src/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= rName %>", 3 | "version": "1.0.0", 4 | "description": "<%= name %>", 5 | "main": "dist/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/<%= user %>/<%= rName %>.git" 9 | }, 10 | "scripts": { 11 | "start": "grapesjs-cli serve", 12 | "build": "grapesjs-cli build", 13 | "bump": "npm version patch -m 'Bump v%s'" 14 | }, 15 | "keywords": [ 16 | "grapesjs", 17 | "plugin" 18 | ], 19 | "devDependencies": { 20 | "grapesjs-cli": "^<%= version %>" 21 | }, 22 | "license": "<%= license %>" 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "noImplicitThis": true, 5 | "moduleResolution": "node", 6 | "noUnusedLocals": true, 7 | "allowUnreachableCode": false, 8 | "module": "commonjs", 9 | "target": "es2016", 10 | "outDir": "dist", 11 | "esModuleInterop": true, 12 | "declaration": true, 13 | "noImplicitReturns": false, 14 | "noImplicitAny": false, 15 | "strictNullChecks": false, 16 | "resolveJsonModule": true, 17 | "emitDecoratorMetadata": true, 18 | "experimentalDecorators": true 19 | }, 20 | "include": ["src/cli.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /src/template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "sourceMap": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": false 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/template/src/index.js: -------------------------------------------------------------------------------- 1 | <% if(components){ %>import loadComponents from './components';<% } %> 2 | <% if(blocks){ %>import loadBlocks from './blocks';<% } %> 3 | <% if(i18n){ %>import en from './locale/en';<% } %> 4 | 5 | export default (editor, opts = {}) => { 6 | const options = { ...{ 7 | <% if(i18n){ %>i18n: {},<% } %> 8 | // default options 9 | }, ...opts }; 10 | 11 | <% if(components){ %>// Add components 12 | loadComponents(editor, options);<% } %> 13 | <% if(blocks){ %>// Add blocks 14 | loadBlocks(editor, options);<% } %> 15 | <% if(i18n){ %>// Load i18n files 16 | editor.I18n && editor.I18n.addMessages({ 17 | en, 18 | ...options.i18n, 19 | });<% } %> 20 | 21 | // TODO Remove 22 | editor.on('load', () => 23 | editor.addComponents( 24 | `
25 | Content loaded from the plugin 26 |
`, 27 | { at: 0 } 28 | )) 29 | }; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-current Artur Arseniev 2 | 3 | All rights reserved. 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/template/_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= name %> 6 | 7 | 8 | 15 | 16 | 17 | 18 |
19 |
20 | This is a demo content from _index.html. You can use this template file for 21 | development purpose. It won't be stored in your git repository 22 |
23 |
24 | 25 | 26 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title %> 6 | 7 | 8 | 15 | 16 | 17 |
18 |
19 | This is a demo content generated from GrapesJS CLI. 20 | For the development, you should create a _index.html template file (might be a copy of this one) and on the next server start 21 | the new file will be served, and it will be ignored by git. 22 |
23 |
24 | 25 | 26 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/serve.ts: -------------------------------------------------------------------------------- 1 | import { printRow, buildWebpackArgs, log, normalizeJsonOpt } from './utils'; 2 | import webpack from 'webpack'; 3 | import webpackDevServer from 'webpack-dev-server'; 4 | import webpackConfig from './webpack.config'; 5 | import chalk from 'chalk'; 6 | 7 | interface ServeOptions { 8 | host?: string; 9 | port?: number; 10 | verbose?: boolean; 11 | } 12 | 13 | /** 14 | * Start up the development server 15 | * @param {Object} opts 16 | */ 17 | export default (opts: ServeOptions = {}) => { 18 | printRow('Start the development server...'); 19 | const { host, port } = opts; 20 | const isVerb = opts.verbose; 21 | const resultWebpackConf = { 22 | ...webpackConfig({ args: buildWebpackArgs(opts), cmdOpts: opts }), 23 | ...normalizeJsonOpt(opts, 'webpack'), 24 | }; 25 | const devServerConf = { 26 | ...resultWebpackConf.devServer, 27 | open: true, 28 | ...normalizeJsonOpt(opts, 'devServer'), 29 | }; 30 | 31 | if (host !== 'localhost') { 32 | devServerConf.host = host; 33 | } 34 | 35 | if (port !== 8080) { 36 | devServerConf.port = port; 37 | } 38 | 39 | if (isVerb) { 40 | log(chalk.yellow('Server config:\n'), opts, '\n'); 41 | log(chalk.yellow('DevServer config:\n'), devServerConf, '\n'); 42 | } 43 | 44 | const compiler = webpack(resultWebpackConf); 45 | const server = new webpackDevServer(devServerConf, compiler); 46 | 47 | server.start(); 48 | }; -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: "@babel/core" 11 | versions: 12 | - 7.12.13 13 | - 7.12.16 14 | - 7.12.17 15 | - 7.13.10 16 | - 7.13.13 17 | - 7.13.14 18 | - 7.13.15 19 | - 7.13.8 20 | - dependency-name: core-js 21 | versions: 22 | - 3.10.0 23 | - 3.10.1 24 | - 3.8.3 25 | - 3.9.0 26 | - 3.9.1 27 | - dependency-name: y18n 28 | versions: 29 | - 4.0.1 30 | - dependency-name: eslint 31 | versions: 32 | - 7.18.0 33 | - 7.19.0 34 | - 7.20.0 35 | - 7.21.0 36 | - 7.22.0 37 | - 7.23.0 38 | - 7.24.0 39 | - dependency-name: webpack-cli 40 | versions: 41 | - 4.4.0 42 | - 4.5.0 43 | - 4.5.0 44 | - dependency-name: webpack 45 | versions: 46 | - 4.46.0 47 | - 5.28.0 48 | - dependency-name: spdx-license-list 49 | versions: 50 | - 6.4.0 51 | - dependency-name: "@babel/plugin-transform-runtime" 52 | versions: 53 | - 7.12.15 54 | - 7.12.17 55 | - 7.13.10 56 | - 7.13.8 57 | - 7.13.9 58 | - dependency-name: "@babel/runtime" 59 | versions: 60 | - 7.12.13 61 | - 7.12.18 62 | - 7.13.10 63 | - 7.13.8 64 | - 7.13.9 65 | - dependency-name: "@babel/preset-env" 66 | versions: 67 | - 7.12.13 68 | - 7.12.16 69 | - 7.12.17 70 | - 7.13.10 71 | - 7.13.8 72 | - 7.13.9 73 | - dependency-name: webpack-dev-server 74 | versions: 75 | - 3.11.2 76 | -------------------------------------------------------------------------------- /webpack.cli.ts: -------------------------------------------------------------------------------- 1 | import webpack, { type Configuration } from 'webpack'; 2 | import NodeExternals from 'webpack-node-externals'; 3 | import CopyPlugin from 'copy-webpack-plugin'; 4 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; 5 | import { resolve } from 'path'; 6 | 7 | const MODE = process.env.BUILD_MODE === 'production' ? 'production' : 'development'; 8 | 9 | const config: Configuration = { 10 | context: process.cwd(), 11 | mode: MODE, 12 | entry: './src/cli.ts', 13 | output: { 14 | filename: 'cli.js', 15 | path: resolve(__dirname, 'dist'), 16 | }, 17 | target: 'node', 18 | stats: { 19 | preset: 'minimal', 20 | warnings: false, 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.(jsx?|tsx?)$/, 26 | use: { 27 | loader: 'babel-loader', 28 | options: { 29 | cacheDirectory: true, 30 | presets: ['@babel/preset-typescript'], 31 | assumptions: { 32 | setPublicClassFields: false, 33 | }, 34 | }, 35 | }, 36 | exclude: [/node_modules/], 37 | }, 38 | ], 39 | }, 40 | resolve: { 41 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.d.ts'], 42 | }, 43 | plugins: [ 44 | new ForkTsCheckerWebpackPlugin(), 45 | new webpack.BannerPlugin({ banner: '#!/usr/bin/env node', raw: true }), 46 | new CopyPlugin({ 47 | patterns: [ 48 | { from: 'src/banner.txt', to: 'banner.txt' }, 49 | { 50 | from: 'src/template', 51 | to: 'template', 52 | // Terser skip this file for minimization 53 | info: { minimized: true }, 54 | }, 55 | ], 56 | }), 57 | ], 58 | externalsPresets: { node: true }, 59 | externals: [ 60 | NodeExternals(), 61 | ], 62 | }; 63 | 64 | export default config; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grapesjs-cli", 3 | "version": "4.1.3", 4 | "description": "GrapesJS CLI tool for the plugin development", 5 | "bin": { 6 | "grapesjs-cli": "dist/cli.js" 7 | }, 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "build": "BUILD_MODE=production webpack --config ./webpack.cli.ts", 13 | "build:watch": "webpack --config ./webpack.cli.ts --watch", 14 | "lint": "eslint src", 15 | "patch": "npm version patch -m 'Bump v%s'", 16 | "test": "jest" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/GrapesJS/cli.git" 21 | }, 22 | "keywords": [ 23 | "grapesjs", 24 | "plugin", 25 | "dev", 26 | "cli" 27 | ], 28 | "author": "Artur Arseniev", 29 | "license": "BSD-3-Clause", 30 | "engines": { 31 | "node": ">=14.15.0" 32 | }, 33 | "dependencies": { 34 | "@babel/core": "^7.20.12", 35 | "@babel/plugin-transform-runtime": "^7.19.6", 36 | "@babel/preset-env": "^7.20.2", 37 | "@babel/runtime": "^7.20.13", 38 | "babel-loader": "^9.1.2", 39 | "chalk": "^4.1.2", 40 | "core-js": "^3.27.2", 41 | "dts-bundle-generator": "^8.0.1", 42 | "html-webpack-plugin": "^5.5.0", 43 | "inquirer": "^8.2.5", 44 | "listr": "^0.14.3", 45 | "lodash.template": "^4.5.0", 46 | "rimraf": "^4.1.2", 47 | "spdx-license-list": "^6.6.0", 48 | "ts-loader": "^9.4.2", 49 | "ts-node": "^10.9.1", 50 | "webpack": "^5.94.0", 51 | "webpack-cli": "^5.0.1", 52 | "webpack-dev-server": "^4.11.1", 53 | "yargs": "^17.6.2" 54 | }, 55 | "devDependencies": { 56 | "@babel/preset-typescript": "^7.21.5", 57 | "@types/jest": "^29.5.12", 58 | "@types/webpack-node-externals": "^3.0.0", 59 | "babel-jest": "^29.7.0", 60 | "copy-webpack-plugin": "^11.0.0", 61 | "eslint": "^7.32.0", 62 | "fork-ts-checker-webpack-plugin": "^8.0.0", 63 | "jest": "^29.7.0", 64 | "webpack-node-externals": "^3.0.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/template/README.md: -------------------------------------------------------------------------------- 1 | # <%= name %> 2 | 3 | ## Live Demo 4 | 5 | > **Show a live example of your plugin** 6 | 7 | To make your plugin more engaging, create a simple live demo using online tools like [JSFiddle](https://jsfiddle.net), [CodeSandbox](https://codesandbox.io), or [CodePen](https://codepen.io). Include the demo link in your README. Adding a screenshot or GIF of the demo is a bonus. 8 | 9 | Below, you'll find the necessary HTML, CSS, and JavaScript. Copy and paste this code into one of the tools mentioned. Once you're done, delete this section and update the link at the top with your demo. 10 | 11 | ### HTML 12 | 13 | ```html 14 | 15 | 16 | 17 | 18 |
19 | ``` 20 | 21 | ### JS 22 | 23 | ```js 24 | const editor = grapesjs.init({ 25 | container: '#gjs', 26 | height: '100%', 27 | fromElement: true, 28 | storageManager: false, 29 | plugins: ['<%= rName %>'], 30 | }); 31 | ``` 32 | 33 | ### CSS 34 | 35 | ```css 36 | body, html { 37 | margin: 0; 38 | height: 100%; 39 | } 40 | ``` 41 | 42 | ## Summary 43 | 44 | * Plugin name: `<%= rName %>` 45 | * Components 46 | * `component-id-1` 47 | * `component-id-2` 48 | * ... 49 | * Blocks 50 | * `block-id-1` 51 | * `block-id-2` 52 | * ... 53 | 54 | ## Options 55 | 56 | | Option | Description | Default | 57 | |-|-|-| 58 | | `option1` | Description option | `default value` | 59 | 60 | ## Download 61 | 62 | * CDN 63 | * `https://unpkg.com/<%= rName %>` 64 | * NPM 65 | * `npm i <%= rName %>` 66 | * GIT 67 | * `git clone https://github.com/<%= user %>/<%= rName %>.git` 68 | 69 | 70 | 71 | ## Usage 72 | 73 | Directly in the browser 74 | 75 | ```html 76 | 77 | 78 | 79 | 80 |
81 | 82 | 92 | ``` 93 | 94 | Modern javascript 95 | 96 | ```js 97 | import grapesjs from 'grapesjs'; 98 | import plugin from '<%= rName %>'; 99 | import 'grapesjs/dist/css/grapes.min.css'; 100 | 101 | const editor = grapesjs.init({ 102 | container : '#gjs', 103 | // ... 104 | plugins: [plugin], 105 | pluginsOpts: { 106 | [plugin]: { /* options */ } 107 | } 108 | // or 109 | plugins: [ 110 | editor => plugin(editor, { /* options */ }), 111 | ], 112 | }); 113 | ``` 114 | 115 | ## Development 116 | 117 | Clone the repository 118 | 119 | ```sh 120 | $ git clone https://github.com/<%= user %>/<%= rName %>.git 121 | $ cd <%= rName %> 122 | ``` 123 | 124 | Install dependencies 125 | 126 | ```sh 127 | npm i 128 | ``` 129 | 130 | Start the dev server 131 | 132 | ```sh 133 | npm start 134 | ``` 135 | 136 | Build the source 137 | 138 | ```sh 139 | npm run build 140 | ``` 141 | 142 | ## License 143 | 144 | MIT 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GrapesJS CLI 2 | 3 | [![npm](https://img.shields.io/npm/v/grapesjs-cli.svg)](https://www.npmjs.com/package/grapesjs-cli) 4 | 5 | ![grapesjs-cli](https://user-images.githubusercontent.com/11614725/67523496-0ed41300-f6af-11e9-9850-7175355f2946.jpg) 6 | 7 | A simple CLI library for helping in GrapesJS plugin development. 8 | 9 | The goal of this package is to avoid the hassle of setting up all the dependencies and configurations for the plugin development by centralizing and speeding up the necessary steps during the process. 10 | 11 | * Fast project scaffolding 12 | * No need to touch Babel and Webpack configurations 13 | 14 | ## Plugin from 0 to 100 15 | 16 | Create a production-ready plugin in a few simple steps. 17 | 18 | * Create a folder for your plugin and init some preliminary steps 19 | 20 | ```sh 21 | mkdir grapesjs-my-plugin 22 | cd grapesjs-my-plugin 23 | npm init -y 24 | git init 25 | ``` 26 | 27 | * Install the package 28 | 29 | ```sh 30 | npm i -D grapesjs-cli 31 | ``` 32 | 33 | * Init your plugin project by following few steps 34 | 35 | ```sh 36 | npx grapesjs-cli init 37 | ``` 38 | 39 | You can also skip all the questions with `-y` option or pass all the answers via options (to see all available options run `npx grapesjs-cli init --help`) 40 | 41 | ```sh 42 | npx grapesjs-cli init -y --user=YOUR-GITHUB-USERNAME 43 | ``` 44 | 45 | * The command will scaffold the `src` directory and a bunch of other files inside your project. The `src/index.js` will be the entry point of your plugin. Before starting developing your plugin run the development server and open the printed URL (eg. the default is http://localhost:8080) 46 | 47 | ```sh 48 | npx grapesjs-cli serve 49 | ``` 50 | 51 | If you need a custom port use the `-p` option 52 | 53 | ```sh 54 | npx grapesjs-cli serve -p 8081 55 | ``` 56 | 57 | Under the hood we use `webpack-dev-server` and you can pass its option via CLI in this way 58 | 59 | ```sh 60 | npx grapesjs-cli serve --devServer='{"https": true}' 61 | ``` 62 | 63 | * Once the development is finished you can build your plugin and generate the minified file ready for production 64 | 65 | ```sh 66 | npx grapesjs-cli build 67 | ``` 68 | 69 | * Before publishing your package remember to complete your README.md file with all the available options, components, blocks and so on. 70 | For a better user engagement create a simple live demo by using services like [JSFiddle](https://jsfiddle.net) [CodeSandbox](https://codesandbox.io) [CodePen](https://codepen.io) and link it in your README. To help you in this process we'll print all the necessary HTML/CSS/JS in your README, so it will be just a matter of copy-pasting on some of those services. 71 | 72 | ## Customization 73 | 74 | ### Customize webpack config 75 | 76 | If you need to customize the webpack configuration, you can create `webpack.config.js` file in the root dir of your project and export a function, which should return the new configuration object. Check the example below. 77 | 78 | ```js 79 | // YOUR-PROJECT-DIR/webpack.config.js 80 | 81 | // config is the default configuration 82 | export default ({ config }) => { 83 | // This is how you can distinguish the `build` command from the `serve` 84 | const isBuild = config.mode === 'production'; 85 | 86 | return { 87 | ...config, 88 | module: { 89 | rules: [ { /* extra rule */ }, ...config.module.rules ], 90 | }, 91 | }; 92 | } 93 | ``` 94 | 95 | ## Generic CLI usage 96 | 97 | Show all available commands 98 | 99 | ```sh 100 | grapesjs-cli 101 | ``` 102 | 103 | Show available options for a command 104 | 105 | ```sh 106 | grapesjs-cli COMMAND --help 107 | ``` 108 | 109 | Run the command 110 | 111 | ```sh 112 | grapesjs-cli COMMAND --OPT1 --OPT2=VALUE 113 | ``` 114 | 115 | 116 | ## License 117 | 118 | MIT 119 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import fsp from 'fs/promises'; 5 | 6 | export const isString = (val: any): val is string => typeof val === 'string'; 7 | 8 | export const isUndefined = (value: any) => typeof value === 'undefined'; 9 | 10 | export const isFunction = (value: any): value is Function => typeof value === 'function'; 11 | 12 | export const isObject = (val: any) => val !== null && !Array.isArray(val) && typeof val === 'object'; 13 | 14 | export const printRow = (str: string, { 15 | color = 'green', 16 | lineDown = 1, 17 | } = {}) => { 18 | console.log(''); 19 | console.log(chalk[color].bold(str)); 20 | lineDown && console.log(''); 21 | } 22 | 23 | export const printError = (str: string) => { 24 | printRow(str, { color: 'red' }); 25 | } 26 | 27 | export const log = (...args: any[]) => console.log.apply(this, args); 28 | 29 | export const ensureDir = (filePath: string) => { 30 | const dirname = path.dirname(filePath); 31 | if (fs.existsSync(dirname)) return true; 32 | fs.mkdirSync(dirname); 33 | return ensureDir(dirname); 34 | } 35 | 36 | /** 37 | * Normalize JSON options 38 | * @param opts Options 39 | * @param key Options name to normalize 40 | * @returns {Object} 41 | */ 42 | export const normalizeJsonOpt = (opts: Record, key: string) => { 43 | let devServerOpt = opts[key] || {}; 44 | 45 | if (isString(devServerOpt)) { 46 | try { 47 | devServerOpt = JSON.parse(devServerOpt); 48 | } catch (e) { 49 | printError(`Error while parsing "${key}" option`); 50 | printError(e); 51 | devServerOpt = {} 52 | } 53 | } 54 | 55 | return devServerOpt; 56 | } 57 | 58 | export const buildWebpackArgs = (opts: Record) => { 59 | return { 60 | ...opts, 61 | babel: normalizeJsonOpt(opts, 'babel'), 62 | htmlWebpack: normalizeJsonOpt(opts, 'htmlWebpack'), 63 | } 64 | } 65 | 66 | export const copyRecursiveSync = (src: string, dest: string) => { 67 | const exists = fs.existsSync(src); 68 | const isDir = exists && fs.statSync(src).isDirectory(); 69 | 70 | if (isDir) { 71 | fs.mkdirSync(dest); 72 | fs.readdirSync(src).forEach((file) => { 73 | copyRecursiveSync(path.join(src, file), path.join(dest, file)); 74 | }); 75 | } else if (exists) { 76 | fs.copyFileSync(src, dest); 77 | } 78 | }; 79 | 80 | export const isPathExists = async (path: string) => { 81 | try { 82 | await fsp.access(path); 83 | return true; 84 | } catch { 85 | return false; 86 | } 87 | }; 88 | 89 | 90 | export const writeFile = async (filePath: string, data: string) => { 91 | try { 92 | const dirname = path.dirname(filePath); 93 | const exist = await isPathExists(dirname); 94 | if (!exist) { 95 | await fsp.mkdir(dirname, { recursive: true }); 96 | } 97 | 98 | await fsp.writeFile(filePath, data, 'utf8'); 99 | } catch (err) { 100 | throw new Error(err); 101 | } 102 | } 103 | 104 | export const rootResolve = (val: string) => path.resolve(process.cwd(), val); 105 | 106 | export const originalRequire = () => { 107 | // @ts-ignore need this to use the original 'require.resolve' as it's replaced by webpack 108 | return __non_webpack_require__; 109 | }; 110 | 111 | export const resolve = (value: string) => { 112 | return originalRequire().resolve(value); 113 | }; 114 | 115 | export const babelConfig = (opts: { targets?: string } = {}) => ({ 116 | presets: [ 117 | [ resolve('@babel/preset-env'), { 118 | targets: opts.targets, 119 | // useBuiltIns: 'usage', // this makes the build much bigger 120 | // corejs: 3, 121 | } ] 122 | ], 123 | plugins: [ resolve('@babel/plugin-transform-runtime') ], 124 | }) -------------------------------------------------------------------------------- /src/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import { babelConfig, rootResolve, isFunction, isObject, log, resolve, originalRequire } from './utils'; 2 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 3 | import TerserPlugin from 'terser-webpack-plugin'; 4 | import chalk from 'chalk'; 5 | import path from 'path'; 6 | import fs from 'fs'; 7 | import webpack from 'webpack'; 8 | 9 | const dirCwd = process.cwd(); 10 | let plugins = []; 11 | 12 | export default (opts: Record = {}) => { 13 | const pkgPath = path.join(dirCwd, 'package.json'); 14 | const rawPackageJson = fs.readFileSync(pkgPath) as unknown as string; 15 | const pkg = JSON.parse(rawPackageJson); 16 | const { args, cmdOpts = {} } = opts; 17 | const { htmlWebpack = {} } = args; 18 | const name = pkg.name; 19 | const isProd = opts.production; 20 | const banner = `/*! ${name} - ${pkg.version} */`; 21 | 22 | if (!isProd) { 23 | const fname = 'index.html'; 24 | const index = `${dirCwd}/${fname}`; 25 | const indexDev = `${dirCwd}/_${fname}`; 26 | let template = path.resolve(__dirname, `./../${fname}`); 27 | 28 | if (fs.existsSync(indexDev)) { 29 | template = indexDev; 30 | } else if (fs.existsSync(index)) { 31 | template = index; 32 | } 33 | 34 | plugins.push(new HtmlWebpackPlugin({ 35 | inject: 'head', 36 | template, 37 | ...htmlWebpack, 38 | templateParameters: { 39 | name, 40 | title: name, 41 | gjsVersion: 'latest', 42 | pathGjs: '', 43 | pathGjsCss: '', 44 | ...htmlWebpack.templateParameters || {}, 45 | }, 46 | })); 47 | } 48 | 49 | const outPath = path.resolve(dirCwd, args.output); 50 | const modulesPaths = [ 'node_modules', path.join(__dirname, '../node_modules')]; 51 | 52 | let config = { 53 | entry: path.resolve(dirCwd, args.entry), 54 | mode: isProd ? 'production' : 'development', 55 | devtool: isProd ? 'source-map' : 'eval', 56 | optimization: { 57 | minimizer: [new TerserPlugin({ 58 | extractComments: false, 59 | terserOptions: { 60 | compress: { 61 | evaluate: false, // Avoid breaking gjs scripts 62 | }, 63 | output: { 64 | comments: false, 65 | quote_style: 3, // Preserve original quotes 66 | preamble: banner, // banner here instead of BannerPlugin 67 | } 68 | } 69 | })], 70 | }, 71 | output: { 72 | path: outPath, 73 | filename: 'index.js', 74 | library: name, 75 | libraryTarget: 'umd', 76 | globalObject: `typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this)`, 77 | }, 78 | module: { 79 | rules: [{ 80 | test: /\.tsx?$/, 81 | loader: resolve('ts-loader'), 82 | exclude: /node_modules/, 83 | options: { 84 | context: rootResolve(''), 85 | configFile: rootResolve('tsconfig.json'), 86 | } 87 | }, { 88 | test: /\.js$/, 89 | loader: resolve('babel-loader'), 90 | include: /src/, 91 | options: { 92 | ...babelConfig(args), 93 | cacheDirectory: true, 94 | ...args.babel, 95 | }, 96 | }], 97 | }, 98 | resolve: { 99 | extensions: ['.tsx', '.ts', '.js'], 100 | modules: modulesPaths, 101 | }, 102 | plugins, 103 | }; 104 | 105 | // Try to load local webpack config 106 | const localWebpackPath = rootResolve('webpack.config.js'); 107 | let localWebpackConf: any; 108 | 109 | if (fs.existsSync(localWebpackPath)) { 110 | const customWebpack = originalRequire()(localWebpackPath); 111 | localWebpackConf = customWebpack.default || customWebpack; 112 | } 113 | 114 | if (isFunction(localWebpackConf)) { 115 | const fnRes = localWebpackConf({ config, webpack, pkg }); 116 | config = isObject(fnRes) ? fnRes : config; 117 | } 118 | 119 | cmdOpts.verbose && log(chalk.yellow('Webpack config:\n'), config, '\n'); 120 | 121 | return config; 122 | } 123 | -------------------------------------------------------------------------------- /src/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | printRow, 3 | printError, 4 | buildWebpackArgs, 5 | normalizeJsonOpt, 6 | copyRecursiveSync, 7 | rootResolve, 8 | babelConfig, 9 | log, 10 | writeFile, 11 | } from './utils'; 12 | import { generateDtsBundle } from "dts-bundle-generator"; 13 | import webpack from 'webpack'; 14 | import fs from 'fs'; 15 | import webpackConfig from './webpack.config'; 16 | import { exec } from 'child_process'; 17 | import chalk from 'chalk'; 18 | import rimraf from 'rimraf' 19 | import { transformFileSync } from '@babel/core'; 20 | 21 | interface BuildOptions { 22 | verbose?: boolean; 23 | patch?: boolean; 24 | statsOutput?: string; 25 | localePath?: string; 26 | dts?: 'include' | 'skip' | 'only'; 27 | } 28 | 29 | /** 30 | * Build locale files 31 | * @param {Object} opts 32 | */ 33 | export const buildLocale = async (opts: BuildOptions = {}) => { 34 | const { localePath } = opts; 35 | if (!fs.existsSync(rootResolve(localePath))) return; 36 | printRow('Start building locale files...', { lineDown: 0 }); 37 | 38 | await rimraf('locale'); 39 | 40 | const localDst = rootResolve('locale'); 41 | copyRecursiveSync(rootResolve(localePath), localDst); 42 | 43 | // Create locale/index.js file 44 | let result = ''; 45 | fs.readdirSync(localDst).forEach(file => { 46 | const name = file.split('.')[0]; 47 | result += `export { default as ${name} } from './${name}'\n`; 48 | }); 49 | fs.writeFileSync(`${localDst}/index.js`, result); 50 | 51 | // Compile files 52 | const babelOpts = { ...babelConfig(buildWebpackArgs(opts) as any) }; 53 | fs.readdirSync(localDst).forEach(file => { 54 | const filePath = `${localDst}/${file}`; 55 | const compiled = transformFileSync(filePath, babelOpts).code; 56 | fs.writeFileSync(filePath, compiled); 57 | }); 58 | 59 | printRow('Locale files building completed successfully!'); 60 | } 61 | 62 | /** 63 | * Build TS declaration file 64 | * @param {Object} opts 65 | */ 66 | export const buildDeclaration = async (opts: BuildOptions = {}) => { 67 | const filePath = rootResolve('src/index.ts'); 68 | if (!fs.existsSync(filePath)) return; 69 | 70 | printRow('Start building TS declaration file...', { lineDown: 0 }); 71 | 72 | const entry = { filePath, output: { noBanner: true }}; 73 | const bundleOptions = { preferredConfigPath: rootResolve('tsconfig.json') }; 74 | const result = generateDtsBundle([entry], bundleOptions)[0]; 75 | await writeFile(rootResolve('dist/index.d.ts'), result); 76 | 77 | printRow('TS declaration file building completed successfully!'); 78 | } 79 | 80 | /** 81 | * Build the library files 82 | * @param {Object} opts 83 | */ 84 | export default (opts: BuildOptions = {}) => { 85 | printRow('Start building the library...'); 86 | const isVerb = opts.verbose; 87 | const { dts } = opts; 88 | isVerb && log(chalk.yellow('Build config:\n'), opts, '\n'); 89 | 90 | const buildWebpack = () => { 91 | const buildConf = { 92 | ...webpackConfig({ 93 | production: 1, 94 | args: buildWebpackArgs(opts), 95 | cmdOpts: opts, 96 | }), 97 | ...normalizeJsonOpt(opts, 'config'), 98 | }; 99 | 100 | if (dts === 'only') { 101 | return buildDeclaration(opts); 102 | } 103 | 104 | webpack(buildConf, async (err, stats) => { 105 | const errors = err || (stats ? stats.hasErrors() : false); 106 | const statConf = { 107 | hash: false, 108 | colors: true, 109 | builtAt: false, 110 | entrypoints: false, 111 | modules: false, 112 | ...normalizeJsonOpt(opts, 'stats'), 113 | }; 114 | 115 | if (stats) { 116 | opts.statsOutput && 117 | fs.writeFileSync(rootResolve(opts.statsOutput), JSON.stringify(stats.toJson())); 118 | isVerb && log(chalk.yellow('Stats config:\n'), statConf, '\n'); 119 | const result = stats.toString(statConf); 120 | log(result, '\n'); 121 | } 122 | 123 | await buildLocale(opts); 124 | 125 | if (dts !== 'skip') { 126 | await buildDeclaration(opts); 127 | } 128 | 129 | if (errors) { 130 | printError(`Error during building`); 131 | console.error(err); 132 | } else { 133 | printRow('Building completed successfully!'); 134 | } 135 | }); 136 | }; 137 | 138 | if (opts.patch) { 139 | isVerb && log(chalk.yellow('Patch the version'), '\n'); 140 | exec('npm version --no-git-tag-version patch', buildWebpack); 141 | } else { 142 | buildWebpack(); 143 | } 144 | }; 145 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { serve, build, init } from './main'; 5 | import chalk from 'chalk'; 6 | import { printError } from './utils'; 7 | import { version } from '../package.json'; 8 | 9 | yargs.usage( 10 | chalk.green.bold( 11 | fs.readFileSync(path.resolve(__dirname, './banner.txt'), 'utf8') + 12 | `\nv${version}` 13 | ) 14 | ); 15 | 16 | const webpackOptions = yargs => { 17 | yargs.positional('config', { 18 | describe: 'webpack configuration options', 19 | type: 'string', 20 | default: '{}', 21 | }) 22 | .positional('babel', { 23 | describe: 'Babel configuration object', 24 | type: 'string', 25 | default: '{}', 26 | }) 27 | .positional('targets', { 28 | describe: 'Browser targets in browserslist query', 29 | type: 'string', 30 | default: '> 0.25%, not dead', 31 | }) 32 | .positional('entry', { 33 | describe: 'Library entry point', 34 | type: 'string', 35 | default: 'src/index', 36 | }) 37 | .positional('output', { 38 | describe: 'Build destination directory', 39 | type: 'string', 40 | default: 'dist', 41 | }) 42 | } 43 | 44 | export const createCommands = (yargs) => { 45 | return yargs 46 | .command(['serve [port]', 'server'], 'Start the server', (yargs) => { 47 | yargs 48 | .positional('devServer', { 49 | describe: 'webpack-dev-server options', 50 | type: 'string', 51 | default: '{}', 52 | }) 53 | .positional('host', { 54 | alias: 'h', 55 | describe: 'Host to bind on', 56 | type: 'string', 57 | default: 'localhost', 58 | }) 59 | .positional('port', { 60 | alias: 'p', 61 | describe: 'Port to bind on', 62 | type: 'number', 63 | default: 8080, 64 | }) 65 | .positional('htmlWebpack', { 66 | describe: 'html-webpack-plugin options', 67 | type: 'string', 68 | default: '{}', 69 | }) 70 | webpackOptions(yargs); 71 | }, (argv) => serve(argv)) 72 | .command('build', 'Build the source', (yargs) => { 73 | yargs 74 | .positional('stats', { 75 | describe: 'Options for webpack Stats instance', 76 | type: 'string', 77 | default: '{}', 78 | }) 79 | .positional('statsOutput', { 80 | describe: 'Specify the path where to output webpack stats file (eg. "stats.json")', 81 | type: 'string', 82 | default: '', 83 | }) 84 | .positional('patch', { 85 | describe: 'Increase automatically the patch version', 86 | type: 'boolean', 87 | default: true, 88 | }) 89 | .positional('localePath', { 90 | describe: 'Path to the directory containing locale files', 91 | type: 'string', 92 | default: 'src/locale', 93 | }) 94 | .positional('dts', { 95 | describe: 'Generate typescript dts file ("include", "skip", "only")', 96 | type: 'string', 97 | default: 'include', 98 | }); 99 | webpackOptions(yargs); 100 | }, (argv) => build(argv)) 101 | .command('init', 'Init GrapesJS plugin project', (yargs) => { 102 | yargs 103 | .positional('yes', { 104 | alias: 'y', 105 | describe: 'All default answers', 106 | type: 'boolean', 107 | default: false, 108 | }) 109 | .positional('name', { 110 | describe: 'Name of the project', 111 | type: 'string', 112 | }) 113 | .positional('rName', { 114 | describe: 'Repository name', 115 | type: 'string', 116 | }) 117 | .positional('user', { 118 | describe: 'Repository username', 119 | type: 'string', 120 | }) 121 | .positional('components', { 122 | describe: 'Indicate to include custom component types API', 123 | type: 'boolean', 124 | }) 125 | .positional('blocks', { 126 | describe: 'Indicate to include blocks API', 127 | type: 'boolean', 128 | }) 129 | .positional('i18n', { 130 | describe: 'Indicate to include the support for i18n', 131 | type: 'boolean', 132 | }) 133 | .positional('license', { 134 | describe: 'License of the project', 135 | type: 'string', 136 | }) 137 | }, (argv) => init(argv)) 138 | .options({ 139 | verbose: { 140 | alias: 'v', 141 | description: 'Run with verbose logging', 142 | type: 'boolean', // boolean | number | string 143 | default: false, 144 | }, 145 | }) 146 | .recommendCommands() 147 | .strict() 148 | } 149 | 150 | export const argsToOpts = async () => { 151 | return await createCommands(yargs).parse(); 152 | }; 153 | 154 | export const run = async (opts = {}) => { 155 | try { 156 | let options = await argsToOpts(); 157 | if (!options._.length) yargs.showHelp(); 158 | } catch (error) { 159 | printError((error.stack || error).toString()) 160 | } 161 | } 162 | 163 | run(); -------------------------------------------------------------------------------- /src/init.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import { printRow, isUndefined, log, ensureDir } from './utils'; 3 | import Listr from 'listr'; 4 | import path from 'path'; 5 | import fs from 'fs'; 6 | import spdxLicenseList from 'spdx-license-list/full'; 7 | import template from 'lodash.template'; 8 | import { version } from '../package.json'; 9 | 10 | interface InitOptions { 11 | license?: string; 12 | name?: string; 13 | components?: boolean; 14 | blocks?: boolean; 15 | i18n?: boolean; 16 | verbose?: boolean, 17 | rName?: string, 18 | user?: string, 19 | yes?: boolean, 20 | }; 21 | 22 | const tmpPath = './template'; 23 | const rootPath = process.cwd(); 24 | 25 | const getName = (str: string) => str 26 | .replace(/\_/g, '-') 27 | .split('-') 28 | .filter(i => i) 29 | .map(i => i[0].toUpperCase() + i.slice(1)) 30 | .join(' '); 31 | 32 | const getTemplateFileContent = (pth: string) => { 33 | const pt = path.resolve(__dirname, `${tmpPath}/${pth}`); 34 | return fs.readFileSync(pt, 'utf8'); 35 | }; 36 | 37 | const resolveRoot = (pth: string) => { 38 | return path.resolve(rootPath, pth); 39 | }; 40 | 41 | const resolveLocal = (pth: string) => { 42 | return path.resolve(__dirname, `${tmpPath}/${pth}`); 43 | }; 44 | 45 | const createSourceFiles = async (opts: InitOptions = {}) => { 46 | const rdmSrc = getTemplateFileContent('README.md') 47 | const rdmDst = resolveRoot('README.md'); 48 | const indxSrc = getTemplateFileContent('src/index.js'); 49 | const indxDst = resolveRoot('src/index.js'); 50 | const indexCnt = getTemplateFileContent('_index.html'); 51 | const indexDst = resolveRoot('_index.html'); 52 | const license = spdxLicenseList[opts.license]; 53 | const licenseTxt = license && (license.licenseText || '') 54 | .replace('', `${new Date().getFullYear()}-current`) 55 | .replace('', opts.name); 56 | ensureDir(indxDst); 57 | // write src/_index.html 58 | fs.writeFileSync(indxDst, template(indxSrc)(opts).trim()); 59 | // write _index.html 60 | fs.writeFileSync(indexDst, template(indexCnt)(opts)); 61 | // Write README.md 62 | fs.writeFileSync(rdmDst, template(rdmSrc)(opts)); 63 | // write LICENSE 64 | licenseTxt && fs.writeFileSync(resolveRoot('LICENSE'), licenseTxt); 65 | // Copy files 66 | fs.copyFileSync(resolveLocal('.gitignore-t'), resolveRoot('.gitignore')); 67 | fs.copyFileSync(resolveLocal('.npmignore-t'), resolveRoot('.npmignore')); 68 | fs.copyFileSync(resolveLocal('tsconfig.json'), resolveRoot('tsconfig.json')); 69 | }; 70 | 71 | const createFileComponents = (opts: InitOptions = {}) => { 72 | const filepath = 'src/components.js'; 73 | const cmpSrc = resolveLocal(filepath); 74 | const cmpDst = resolveRoot(filepath); 75 | opts.components && fs.copyFileSync(cmpSrc, cmpDst); 76 | }; 77 | 78 | const createFileBlocks = (opts: InitOptions = {}) => { 79 | const filepath = 'src/blocks.js'; 80 | const blkSrc = resolveLocal(filepath); 81 | const blkDst = resolveRoot(filepath); 82 | opts.blocks && fs.copyFileSync(blkSrc, blkDst); 83 | }; 84 | 85 | const createI18n = (opts = {}) => { 86 | const enPath = 'src/locale/en.js'; 87 | const tmpEn = getTemplateFileContent(enPath); 88 | const dstEn = resolveRoot(enPath); 89 | ensureDir(dstEn); 90 | fs.writeFileSync(dstEn, template(tmpEn)(opts)); 91 | }; 92 | 93 | const createPackage = (opts = {}) => { 94 | const filepath = 'package.json'; 95 | const cnt = getTemplateFileContent(filepath); 96 | const dst = resolveRoot(filepath); 97 | fs.writeFileSync(dst, template(cnt)({ 98 | ...opts, 99 | version, 100 | })); 101 | }; 102 | 103 | const checkBoolean = value => value && value !== 'false' ? true : false; 104 | 105 | export const initPlugin = async(opts: InitOptions = {}) => { 106 | printRow('Start project creation...'); 107 | opts.components = checkBoolean(opts.components); 108 | opts.blocks = checkBoolean(opts.blocks); 109 | opts.i18n = checkBoolean(opts.i18n); 110 | 111 | const tasks = new Listr([ 112 | { 113 | title: 'Creating initial source files', 114 | task: () => createSourceFiles(opts), 115 | }, { 116 | title: 'Creating custom Component Type file', 117 | task: () => createFileComponents(opts), 118 | enabled: () => opts.components, 119 | }, { 120 | title: 'Creating Blocks file', 121 | task: () => createFileBlocks(opts), 122 | enabled: () => opts.blocks, 123 | }, { 124 | title: 'Creating i18n structure', 125 | task: () => createI18n(opts), 126 | enabled: () => opts.i18n, 127 | }, { 128 | title: 'Update package.json', 129 | task: () => createPackage(opts), 130 | }, 131 | ]); 132 | await tasks.run(); 133 | } 134 | 135 | export default async (opts: InitOptions = {}) => { 136 | const rootDir = path.basename(process.cwd()); 137 | const questions = []; 138 | const { 139 | verbose, 140 | name, 141 | rName, 142 | user, 143 | yes, 144 | components, 145 | blocks, 146 | i18n, 147 | license, 148 | } = opts; 149 | let results = { 150 | name: name || getName(rootDir), 151 | rName: rName || rootDir, 152 | user: user || 'YOUR-USERNAME', 153 | components: isUndefined(components) ? true : components, 154 | blocks: isUndefined(blocks) ? true : blocks, 155 | i18n: isUndefined(i18n) ? true : i18n, 156 | license: license || 'MIT', 157 | }; 158 | printRow(`Init the project${verbose ? ' (verbose)' : ''}...`); 159 | 160 | if (!yes) { 161 | !name && questions.push({ 162 | name: 'name', 163 | message: 'Name of the project', 164 | default: results.name, 165 | }); 166 | !rName && questions.push({ 167 | name: 'rName', 168 | message: 'Repository name (used also as the plugin name)', 169 | default: results.rName, 170 | }); 171 | !user && questions.push({ 172 | name: 'user', 173 | message: 'Repository username (eg. on GitHub/Bitbucket)', 174 | default: results.user, 175 | }); 176 | isUndefined(components) && questions.push({ 177 | type: 'boolean', 178 | name: 'components', 179 | message: 'Will you need to add custom Component Types?', 180 | default: results.components, 181 | }); 182 | isUndefined(blocks) && questions.push({ 183 | type: 'boolean', 184 | name: 'blocks', 185 | message: 'Will you need to add Blocks?', 186 | default: results.blocks, 187 | }); 188 | isUndefined(i18n) && questions.push({ 189 | type: 'boolean', 190 | name: 'i18n', 191 | message: 'Do you want to setup i18n structure in this plugin?', 192 | default: results.i18n, 193 | }); 194 | !license && questions.push({ 195 | name: 'license', 196 | message: 'License of the project', 197 | default: results.license, 198 | }); 199 | } 200 | 201 | const answers = await inquirer.prompt(questions); 202 | results = { 203 | ...results, 204 | ...answers, 205 | } 206 | 207 | verbose && log({ results, opts }); 208 | await initPlugin(results); 209 | printRow('Project created! Happy coding'); 210 | } -------------------------------------------------------------------------------- /test/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isObject, isString, isUndefined, printRow, printError, log, ensureDir, normalizeJsonOpt, buildWebpackArgs, copyRecursiveSync, babelConfig, originalRequire, resolve, rootResolve } from "../src/utils"; 2 | import chalk from 'chalk'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import * as process from 'process'; 6 | 7 | const typeTestValues = { 8 | undefinedValue: undefined, 9 | nullValue: null, 10 | stringValue: 'hello', 11 | emptyObject: {}, 12 | nonEmptyObject: { key: 'value' }, 13 | emptyArray: [], 14 | functionValue: () => { }, 15 | numberValue: 42, 16 | booleanValue: true, 17 | dateValue: new Date(), 18 | }; 19 | 20 | function runTypeCheck(typeCheckFunction: (value: any) => boolean) { 21 | const keysWithPassingTypeChecks = Object.keys(typeTestValues).filter(key => { 22 | const value = typeTestValues[key]; 23 | return typeCheckFunction(value); 24 | }); 25 | 26 | return keysWithPassingTypeChecks; 27 | } 28 | 29 | jest.mock('fs'); 30 | jest.mock('fs/promises'); 31 | 32 | describe('utils', () => { 33 | afterEach(() => { 34 | jest.clearAllMocks(); 35 | }); 36 | 37 | describe('isString', () => { 38 | it('should correctly identify strings', () => { 39 | const result = runTypeCheck(isString); 40 | expect(result).toEqual(['stringValue']); 41 | }); 42 | }); 43 | 44 | describe('isUndefined', () => { 45 | it('should correctly identify undefined values', () => { 46 | const result = runTypeCheck(isUndefined); 47 | expect(result).toEqual(['undefinedValue']); 48 | }); 49 | }); 50 | 51 | describe('isFunction', () => { 52 | it('should correctly identify functions', () => { 53 | const result = runTypeCheck(isFunction); 54 | expect(result).toEqual(['functionValue']); 55 | }); 56 | }); 57 | 58 | describe('isObject', () => { 59 | it('should correctly identify objects', () => { 60 | const result = runTypeCheck(isObject); 61 | expect(result).toEqual(['emptyObject', 'nonEmptyObject', 'dateValue']); 62 | }); 63 | }); 64 | 65 | 66 | describe('printRow', () => { 67 | // TODO: We should refactor the function to make lineDown a boolean not a number 68 | it('should console.log the given string with the specified color and line breaks', () => { 69 | const str = 'Test string'; 70 | const color = 'blue'; 71 | const lineDown = 1; 72 | 73 | console.log = jest.fn(); 74 | 75 | printRow(str, { color, lineDown }); 76 | 77 | expect(console.log).toHaveBeenCalledTimes(3); // 1 for empty line, 1 for colored string, 1 for line break 78 | expect(console.log.mock.calls[1][0]).toEqual(chalk[color].bold(str)); 79 | }); 80 | 81 | it('should not add a line break if lineDown is false', () => { 82 | const str = 'Test string'; 83 | const color = 'green'; 84 | const lineDown = 0; 85 | 86 | console.log = jest.fn(); 87 | 88 | printRow(str, { color, lineDown }); 89 | 90 | expect(console.log).toHaveBeenCalledTimes(2); // 1 for empty line, 1 for colored string 91 | }); 92 | }); 93 | 94 | describe('printError', () => { 95 | it('should print the given string in red', () => { 96 | const str = 'Error message'; 97 | 98 | console.log = jest.fn(); 99 | 100 | printError(str); 101 | 102 | expect(console.log).toHaveBeenCalledTimes(3); // 1 for empty line, 1 for red string, 1 for line break 103 | expect(console.log.mock.calls[1][0]).toEqual(chalk.red.bold(str)); 104 | }); 105 | }); 106 | 107 | describe('log', () => { 108 | it('should call console.log with the given arguments', () => { 109 | const arg1 = 'Argument 1'; 110 | const arg2 = 'Argument 2'; 111 | 112 | console.log = jest.fn(); 113 | 114 | log(arg1, arg2); 115 | 116 | expect(console.log).toHaveBeenCalledWith(arg1, arg2); 117 | }); 118 | }); 119 | 120 | describe('ensureDir', () => { 121 | it('should return true when the directory already exists', () => { 122 | (fs.existsSync as jest.Mock).mockReturnValue(true); 123 | 124 | const result = ensureDir('/path/to/file.txt'); 125 | expect(result).toBe(true); 126 | expect(fs.existsSync).toHaveBeenCalledWith('/path/to'); 127 | expect(fs.mkdirSync).not.toHaveBeenCalled(); 128 | }); 129 | 130 | it('should create the directory when it does not exist', () => { 131 | (fs.existsSync as jest.Mock).mockReturnValueOnce(false).mockReturnValueOnce(true); 132 | 133 | const result = ensureDir('/path/to/file.txt'); 134 | expect(result).toBe(true); 135 | expect(fs.existsSync).toHaveBeenCalledWith('/path/to'); 136 | expect(fs.mkdirSync).toHaveBeenCalledWith('/path/to'); 137 | }); 138 | 139 | it('should create parent directories recursively when they do not exist', () => { 140 | (fs.existsSync as jest.Mock) 141 | .mockReturnValueOnce(false) // Check /path/to (does not exist) 142 | .mockReturnValueOnce(false) // Check /path (does not exist) 143 | .mockReturnValueOnce(true); // Check / (root, exists) 144 | 145 | const result = ensureDir('/path/to/file.txt'); 146 | expect(result).toBe(true); 147 | expect(fs.existsSync).toHaveBeenCalledTimes(3); // /path/to, /path, / 148 | expect(fs.mkdirSync).toHaveBeenCalledTimes(2); // /path, /path/to 149 | }); 150 | }); 151 | 152 | describe('normalizeJsonOpt', () => { 153 | it('should return the object if the option is already an object', () => { 154 | const opts = { babel: { presets: ['@babel/preset-env'] } }; 155 | const result = normalizeJsonOpt(opts, 'babel'); 156 | expect(result).toEqual(opts.babel); 157 | }); 158 | 159 | it('should parse and return the object if the option is a valid JSON string', () => { 160 | const opts = { babel: '{"presets":["@babel/preset-env"]}' }; 161 | const result = normalizeJsonOpt(opts, 'babel'); 162 | expect(result).toEqual({ presets: ['@babel/preset-env'] }); 163 | }); 164 | 165 | it('should return an empty object if the option is an invalid JSON string', () => { 166 | const opts = { babel: '{"presets":["@babel/preset-env"]' }; // Invalid JSON 167 | const result = normalizeJsonOpt(opts, 'babel'); 168 | expect(result).toEqual({}); 169 | }); 170 | 171 | it('should return an empty object if the option is not provided', () => { 172 | const opts = {}; 173 | const result = normalizeJsonOpt(opts, 'babel'); 174 | expect(result).toEqual({}); 175 | }); 176 | }); 177 | 178 | describe('buildWebpackArgs', () => { 179 | it('should return the options with normalized JSON options for babel and htmlWebpack', () => { 180 | const opts = { 181 | babel: '{"presets":["@babel/preset-env"]}', 182 | htmlWebpack: '{"template":"./src/index.html"}', 183 | otherOption: 'someValue' 184 | }; 185 | 186 | const result = buildWebpackArgs(opts); 187 | expect(result).toEqual({ 188 | babel: { presets: ['@babel/preset-env'] }, 189 | htmlWebpack: { template: './src/index.html' }, 190 | otherOption: 'someValue', 191 | }); 192 | }); 193 | 194 | it('should return empty objects for babel and htmlWebpack if they are invalid JSON strings', () => { 195 | const opts = { 196 | babel: '{"presets":["@babel/preset-env"]', // Invalid JSON 197 | htmlWebpack: '{"template":"./src/index.html', // Invalid JSON 198 | }; 199 | 200 | const result = buildWebpackArgs(opts); 201 | expect(result).toEqual({ 202 | babel: {}, 203 | htmlWebpack: {}, 204 | }); 205 | }); 206 | 207 | it('should return the original objects if babel and htmlWebpack are already objects', () => { 208 | const opts = { 209 | babel: { presets: ['@babel/preset-env'] }, 210 | htmlWebpack: { template: './src/index.html' }, 211 | }; 212 | 213 | const result = buildWebpackArgs(opts); 214 | expect(result).toEqual({ 215 | babel: opts.babel, 216 | htmlWebpack: opts.htmlWebpack, 217 | }); 218 | }); 219 | 220 | it('should handle missing babel and htmlWebpack keys gracefully', () => { 221 | const opts = { otherOption: 'someValue' }; 222 | 223 | const result = buildWebpackArgs(opts); 224 | expect(result).toEqual({ 225 | babel: {}, 226 | htmlWebpack: {}, 227 | otherOption: 'someValue', 228 | }); 229 | }); 230 | }); 231 | 232 | describe('copyRecursiveSync', () => { 233 | // TODO: Maybe this test case is a bit complex and we should think of an easier solution 234 | it('should copy a directory and its contents recursively', () => { 235 | /** 236 | * First call: Mock as a directory with two files 237 | * Subsequent calls: Mock as a file 238 | */ 239 | const existsSyncMock = (fs.existsSync as jest.Mock).mockReturnValue(true); 240 | const statSyncMock = (fs.statSync as jest.Mock).mockReturnValueOnce({ isDirectory: () => true }).mockReturnValue({ isDirectory: () => false }); 241 | const readdirSyncMock = (fs.readdirSync as jest.Mock).mockReturnValueOnce(['file1.txt', 'file2.txt']).mockReturnValue([]); 242 | const copyFileSyncMock = (fs.copyFileSync as jest.Mock).mockImplementation(() => { }); 243 | 244 | copyRecursiveSync('/src', '/dest'); 245 | 246 | expect(existsSyncMock).toHaveBeenCalledWith('/src'); 247 | expect(statSyncMock).toHaveBeenCalledWith('/src'); 248 | expect(fs.mkdirSync).toHaveBeenCalledWith('/dest'); 249 | expect(readdirSyncMock).toHaveBeenCalledWith('/src'); 250 | expect(copyFileSyncMock).toHaveBeenCalledWith(path.normalize('/src/file1.txt'), path.normalize('/dest/file1.txt')); 251 | expect(copyFileSyncMock).toHaveBeenCalledWith(path.normalize('/src/file2.txt'), path.normalize('/dest/file2.txt')); 252 | }); 253 | 254 | it('should copy a file when source is a file', () => { 255 | (fs.existsSync as jest.Mock).mockReturnValue(true); 256 | (fs.statSync as jest.Mock).mockReturnValue({ isDirectory: () => false }); 257 | 258 | copyRecursiveSync('/src/file.txt', '/dest/file.txt'); 259 | 260 | expect(fs.existsSync).toHaveBeenCalledWith('/src/file.txt'); 261 | expect(fs.statSync).toHaveBeenCalledWith('/src/file.txt'); 262 | expect(fs.copyFileSync).toHaveBeenCalledWith('/src/file.txt', '/dest/file.txt'); 263 | }); 264 | 265 | // Maybe we can change the behavior to throw an error if the `src` doesn't exist 266 | it('should do nothing when source does not exist', () => { 267 | (fs.existsSync as jest.Mock).mockReturnValue(false); 268 | 269 | copyRecursiveSync('/src/file.txt', '/dest/file.txt'); 270 | 271 | expect(fs.existsSync).toHaveBeenCalledWith('/src/file.txt'); 272 | expect(fs.statSync).not.toHaveBeenCalled(); 273 | expect(fs.mkdirSync).not.toHaveBeenCalled(); 274 | expect(fs.copyFileSync).not.toHaveBeenCalled(); 275 | }); 276 | }); 277 | 278 | describe('rootResolve', () => { 279 | it('should resolve a relative path to an absolute path', () => { 280 | const result = rootResolve('src/index.js'); 281 | 282 | expect(result).toBe(path.join(process.cwd(), 'src/index.js')); 283 | }); 284 | }); 285 | 286 | describe('originalRequire', () => { 287 | it('should return the original require.resolve function', () => { 288 | const originalRequireMock = jest.fn(); 289 | global.__non_webpack_require__ = originalRequireMock; 290 | 291 | const result = originalRequire(); 292 | 293 | expect(result).toBe(originalRequireMock); 294 | }); 295 | }); 296 | 297 | describe('resolve', () => { 298 | it('should resolve a module path using the original require.resolve', () => { 299 | const originalRequireMock = { 300 | resolve: jest.fn().mockReturnValue('resolved/path'), 301 | }; 302 | global.__non_webpack_require__ = originalRequireMock; 303 | 304 | const result = resolve('my-module'); 305 | 306 | expect(result).toBe('resolved/path'); 307 | expect(originalRequireMock.resolve).toHaveBeenCalledWith('my-module'); 308 | }); 309 | }); 310 | 311 | describe('babelConfig', () => { 312 | afterEach(() => { 313 | jest.restoreAllMocks(); 314 | }); 315 | 316 | it('should return a Babel configuration object with specified presets and plugins', () => { 317 | const result = babelConfig(); 318 | 319 | expect(result).toEqual({ 320 | presets: [ 321 | [resolve('@babel/preset-env'), { targets: undefined }], 322 | ], 323 | plugins: [resolve('@babel/plugin-transform-runtime')], 324 | }); 325 | }); 326 | 327 | it('should include the specified targets in the Babel configuration', () => { 328 | const result = babelConfig({ targets: 'node 14' }); 329 | 330 | expect(result).toEqual({ 331 | presets: [ 332 | [resolve('@babel/preset-env'), { targets: 'node 14' }], 333 | ], 334 | plugins: [resolve('@babel/plugin-transform-runtime')], 335 | }); 336 | }); 337 | }); 338 | }); --------------------------------------------------------------------------------