Browse Source

feat: linting and formatting on cli package

feat/cli-mono-repo
danstarns 2 years ago
parent
commit
201a1625e1
  1. 2
      .eslintrc.js
  2. 3
      .prettierignore
  3. 4
      package.json
  4. 42
      packages/cli/README.md
  5. 7
      packages/cli/babel.config.js
  6. 20
      packages/cli/index.html
  7. 217
      packages/cli/src/build.ts
  8. 177
      packages/cli/src/cli.ts
  9. 339
      packages/cli/src/init.ts
  10. 2
      packages/cli/src/main.ts
  11. 58
      packages/cli/src/serve.ts
  12. 59
      packages/cli/src/template/README.md
  13. 2
      packages/cli/src/template/src/blocks.js
  14. 4
      packages/cli/src/template/src/components.js
  15. 10
      packages/cli/src/template/tsconfig.json
  16. 147
      packages/cli/src/utils.ts
  17. 95
      packages/cli/src/webpack.config.ts
  18. 565
      packages/cli/test/utils.spec.ts
  19. 4
      packages/cli/webpack.cli.ts

2
.eslintrc.js

@ -52,5 +52,5 @@ module.exports = {
'max-len': ['error', { code: 300 }], 'max-len': ['error', { code: 300 }],
'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1 }], 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1 }],
}, },
ignorePatterns: ['*/docs/api/*', 'dist'], ignorePatterns: ['*/docs/api/*', 'dist', 'packages/cli/src/template/**/*.*'],
}; };

3
.prettierignore

@ -1,3 +1,4 @@
docs/**/*.md docs/**/*.md
dist/ dist/
pnpm-lock.yaml pnpm-lock.yaml
packages/cli/src/template/**/*.*

4
package.json

@ -10,8 +10,8 @@
"lint": "eslint .", "lint": "eslint .",
"build": "pnpm --filter \"!@grapesjs/docs\" build", "build": "pnpm --filter \"!@grapesjs/docs\" build",
"clean": "find . -type d \\( -name \"node_modules\" -o -name \"build\" -o -name \"dist\" \\) -exec rm -rf {} + && rm ./pnpm-lock.yaml", "clean": "find . -type d \\( -name \"node_modules\" -o -name \"build\" -o -name \"dist\" \\) -exec rm -rf {} + && rm ./pnpm-lock.yaml",
"format": "prettier . --write", "format": "prettier . --write --ignore-path .prettierignore",
"format:check": "prettier . --check" "format:check": "prettier . --check --ignore-path .prettierignore"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "7.24.8", "@babel/cli": "7.24.8",

42
packages/cli/README.md

@ -8,14 +8,14 @@ A simple CLI library for helping in GrapesJS plugin development.
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. 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.
* Fast project scaffolding - Fast project scaffolding
* No need to touch Babel and Webpack configurations - No need to touch Babel and Webpack configurations
## Plugin from 0 to 100 ## Plugin from 0 to 100
Create a production-ready plugin in a few simple steps. Create a production-ready plugin in a few simple steps.
* Create a folder for your plugin and init some preliminary steps - Create a folder for your plugin and init some preliminary steps
```sh ```sh
mkdir grapesjs-my-plugin mkdir grapesjs-my-plugin
@ -24,13 +24,13 @@ npm init -y
git init git init
``` ```
* Install the package - Install the package
```sh ```sh
npm i -D grapesjs-cli npm i -D grapesjs-cli
``` ```
* Init your plugin project by following few steps - Init your plugin project by following few steps
```sh ```sh
npx grapesjs-cli init npx grapesjs-cli init
@ -42,7 +42,7 @@ You can also skip all the questions with `-y` option or pass all the answers via
npx grapesjs-cli init -y --user=YOUR-GITHUB-USERNAME npx grapesjs-cli init -y --user=YOUR-GITHUB-USERNAME
``` ```
* 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) - 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)
```sh ```sh
npx grapesjs-cli serve npx grapesjs-cli serve
@ -60,14 +60,14 @@ Under the hood we use `webpack-dev-server` and you can pass its option via CLI i
npx grapesjs-cli serve --devServer='{"https": true}' npx grapesjs-cli serve --devServer='{"https": true}'
``` ```
* Once the development is finished you can build your plugin and generate the minified file ready for production - Once the development is finished you can build your plugin and generate the minified file ready for production
```sh ```sh
npx grapesjs-cli build npx grapesjs-cli build
``` ```
* Before publishing your package remember to complete your README.md file with all the available options, components, blocks and so on. - Before publishing your package remember to complete your README.md file with all the available options, components, blocks and so on.
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. 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.
## Customization ## Customization
@ -80,16 +80,21 @@ If you need to customize the webpack configuration, you can create `webpack.conf
// config is the default configuration // config is the default configuration
export default ({ config }) => { export default ({ config }) => {
// This is how you can distinguish the `build` command from the `serve` // This is how you can distinguish the `build` command from the `serve`
const isBuild = config.mode === 'production'; const isBuild = config.mode === 'production';
return { return {
...config, ...config,
module: { module: {
rules: [ { /* extra rule */ }, ...config.module.rules ], rules: [
{
/* extra rule */
}, },
}; ...config.module.rules,
} ],
},
};
};
``` ```
## Generic CLI usage ## Generic CLI usage
@ -112,7 +117,6 @@ Run the command
grapesjs-cli COMMAND --OPT1 --OPT2=VALUE grapesjs-cli COMMAND --OPT1 --OPT2=VALUE
``` ```
## License ## License
MIT MIT

7
packages/cli/babel.config.js

@ -1,6 +1,3 @@
module.exports = { module.exports = {
presets: [ presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
['@babel/preset-env', { targets: { node: 'current' } }], };
'@babel/preset-typescript',
],
};

20
packages/cli/index.html

@ -1,9 +1,12 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<title><%= title %></title> <title><%= title %></title>
<link href="<%= pathGjsCss ? pathGjsCss : 'https://unpkg.com/grapesjs@' + gjsVersion + '/dist/css/grapes.min.css' %>" rel="stylesheet"> <link
href="<%= pathGjsCss ? pathGjsCss : 'https://unpkg.com/grapesjs@' + gjsVersion + '/dist/css/grapes.min.css' %>"
rel="stylesheet"
/>
<script src="<%= pathGjs ? pathGjs : 'https://unpkg.com/grapesjs@' + gjsVersion %>"></script> <script src="<%= pathGjs ? pathGjs : 'https://unpkg.com/grapesjs@' + gjsVersion %>"></script>
<style> <style>
body, body,
@ -14,15 +17,14 @@
</style> </style>
</head> </head>
<body> <body>
<div id="gjs" style="height:0px; overflow:hidden"> <div id="gjs" style="height: 0px; overflow: hidden">
<div style="margin:100px 100px 25px; padding:25px; font:caption"> <div style="margin: 100px 100px 25px; padding: 25px; font: caption">
This is a demo content generated from <b>GrapesJS CLI</b>. This is a demo content generated from <b>GrapesJS CLI</b>. For the development, you should create a _index.html
For the development, you should create a _index.html template file (might be a copy of this one) and on the next server start template file (might be a copy of this one) and on the next server start the new file will be served, and it
the new file will be served, and it will be ignored by git. will be ignored by git.
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
window.onload = () => { window.onload = () => {
window.editor = grapesjs.init({ window.editor = grapesjs.init({
@ -34,7 +36,7 @@
fromElement: true, fromElement: true,
plugins: ['<%= name %>'], plugins: ['<%= name %>'],
}); });
} };
</script> </script>
</body> </body>
</html> </html>

217
packages/cli/src/build.ts

@ -1,29 +1,29 @@
import { import {
printRow, printRow,
printError, printError,
buildWebpackArgs, buildWebpackArgs,
normalizeJsonOpt, normalizeJsonOpt,
copyRecursiveSync, copyRecursiveSync,
rootResolve, rootResolve,
babelConfig, babelConfig,
log, log,
writeFile, writeFile,
} from './utils'; } from './utils';
import { generateDtsBundle } from "dts-bundle-generator"; import { generateDtsBundle } from 'dts-bundle-generator';
import webpack from 'webpack'; import webpack from 'webpack';
import fs from 'fs'; import fs from 'fs';
import webpackConfig from './webpack.config'; import webpackConfig from './webpack.config';
import { exec } from 'child_process'; import { exec } from 'child_process';
import chalk from 'chalk'; import chalk from 'chalk';
import rimraf from 'rimraf' import rimraf from 'rimraf';
import { transformFileSync } from '@babel/core'; import { transformFileSync } from '@babel/core';
interface BuildOptions { interface BuildOptions {
verbose?: boolean; verbose?: boolean;
patch?: boolean; patch?: boolean;
statsOutput?: string; statsOutput?: string;
localePath?: string; localePath?: string;
dts?: 'include' | 'skip' | 'only'; dts?: 'include' | 'skip' | 'only';
} }
/** /**
@ -31,114 +31,113 @@ interface BuildOptions {
* @param {Object} opts * @param {Object} opts
*/ */
export const buildLocale = async (opts: BuildOptions = {}) => { export const buildLocale = async (opts: BuildOptions = {}) => {
const { localePath } = opts; const { localePath } = opts;
if (!fs.existsSync(rootResolve(localePath))) return; if (!fs.existsSync(rootResolve(localePath))) return;
printRow('Start building locale files...', { lineDown: 0 }); printRow('Start building locale files...', { lineDown: 0 });
await rimraf('locale'); await rimraf('locale');
const localDst = rootResolve('locale'); const localDst = rootResolve('locale');
copyRecursiveSync(rootResolve(localePath), localDst); copyRecursiveSync(rootResolve(localePath), localDst);
// Create locale/index.js file // Create locale/index.js file
let result = ''; let result = '';
fs.readdirSync(localDst).forEach(file => { fs.readdirSync(localDst).forEach((file) => {
const name = file.split('.')[0]; const name = file.split('.')[0];
result += `export { default as ${name} } from './${name}'\n`; result += `export { default as ${name} } from './${name}'\n`;
}); });
fs.writeFileSync(`${localDst}/index.js`, result); fs.writeFileSync(`${localDst}/index.js`, result);
// Compile files // Compile files
const babelOpts = { ...babelConfig(buildWebpackArgs(opts) as any) }; const babelOpts = { ...babelConfig(buildWebpackArgs(opts) as any) };
fs.readdirSync(localDst).forEach(file => { fs.readdirSync(localDst).forEach((file) => {
const filePath = `${localDst}/${file}`; const filePath = `${localDst}/${file}`;
const compiled = transformFileSync(filePath, babelOpts).code; const compiled = transformFileSync(filePath, babelOpts).code;
fs.writeFileSync(filePath, compiled); fs.writeFileSync(filePath, compiled);
}); });
printRow('Locale files building completed successfully!'); printRow('Locale files building completed successfully!');
} };
/** /**
* Build TS declaration file * Build TS declaration file
* @param {Object} opts * @param {Object} opts
*/ */
export const buildDeclaration = async (opts: BuildOptions = {}) => { export const buildDeclaration = async (opts: BuildOptions = {}) => {
const filePath = rootResolve('src/index.ts'); const filePath = rootResolve('src/index.ts');
if (!fs.existsSync(filePath)) return; if (!fs.existsSync(filePath)) return;
printRow('Start building TS declaration file...', { lineDown: 0 }); printRow('Start building TS declaration file...', { lineDown: 0 });
const entry = { filePath, output: { noBanner: true }}; const entry = { filePath, output: { noBanner: true } };
const bundleOptions = { preferredConfigPath: rootResolve('tsconfig.json') }; const bundleOptions = { preferredConfigPath: rootResolve('tsconfig.json') };
const result = generateDtsBundle([entry], bundleOptions)[0]; const result = generateDtsBundle([entry], bundleOptions)[0];
await writeFile(rootResolve('dist/index.d.ts'), result); await writeFile(rootResolve('dist/index.d.ts'), result);
printRow('TS declaration file building completed successfully!'); printRow('TS declaration file building completed successfully!');
} };
/** /**
* Build the library files * Build the library files
* @param {Object} opts * @param {Object} opts
*/ */
export default (opts: BuildOptions = {}) => { export default (opts: BuildOptions = {}) => {
printRow('Start building the library...'); printRow('Start building the library...');
const isVerb = opts.verbose; const isVerb = opts.verbose;
const { dts } = opts; const { dts } = opts;
isVerb && log(chalk.yellow('Build config:\n'), opts, '\n'); isVerb && log(chalk.yellow('Build config:\n'), opts, '\n');
const buildWebpack = () => { const buildWebpack = () => {
const buildConf = { const buildConf = {
...webpackConfig({ ...webpackConfig({
production: 1, production: 1,
args: buildWebpackArgs(opts), args: buildWebpackArgs(opts),
cmdOpts: opts, cmdOpts: opts,
}), }),
...normalizeJsonOpt(opts, 'config'), ...normalizeJsonOpt(opts, 'config'),
};
if (dts === 'only') {
return buildDeclaration(opts);
}
webpack(buildConf, async (err, stats) => {
const errors = err || (stats ? stats.hasErrors() : false);
const statConf = {
hash: false,
colors: true,
builtAt: false,
entrypoints: false,
modules: false,
...normalizeJsonOpt(opts, 'stats'),
};
if (stats) {
opts.statsOutput &&
fs.writeFileSync(rootResolve(opts.statsOutput), JSON.stringify(stats.toJson()));
isVerb && log(chalk.yellow('Stats config:\n'), statConf, '\n');
const result = stats.toString(statConf);
log(result, '\n');
}
await buildLocale(opts);
if (dts !== 'skip') {
await buildDeclaration(opts);
}
if (errors) {
printError(`Error during building`);
console.error(err);
} else {
printRow('Building completed successfully!');
}
});
}; };
if (opts.patch) { if (dts === 'only') {
isVerb && log(chalk.yellow('Patch the version'), '\n'); return buildDeclaration(opts);
exec('npm version --no-git-tag-version patch', buildWebpack);
} else {
buildWebpack();
} }
webpack(buildConf, async (err, stats) => {
const errors = err || (stats ? stats.hasErrors() : false);
const statConf = {
hash: false,
colors: true,
builtAt: false,
entrypoints: false,
modules: false,
...normalizeJsonOpt(opts, 'stats'),
};
if (stats) {
opts.statsOutput && fs.writeFileSync(rootResolve(opts.statsOutput), JSON.stringify(stats.toJson()));
isVerb && log(chalk.yellow('Stats config:\n'), statConf, '\n');
const result = stats.toString(statConf);
log(result, '\n');
}
await buildLocale(opts);
if (dts !== 'skip') {
await buildDeclaration(opts);
}
if (errors) {
printError(`Error during building`);
console.error(err);
} else {
printRow('Building completed successfully!');
}
});
};
if (opts.patch) {
isVerb && log(chalk.yellow('Patch the version'), '\n');
exec('npm version --no-git-tag-version patch', buildWebpack);
} else {
buildWebpack();
}
}; };

177
packages/cli/src/cli.ts

@ -6,158 +6,169 @@ import chalk from 'chalk';
import { printError } from './utils'; import { printError } from './utils';
import { version } from '../package.json'; import { version } from '../package.json';
yargs.usage( yargs.usage(chalk.green.bold(fs.readFileSync(path.resolve(__dirname, './banner.txt'), 'utf8') + `\nv${version}`));
chalk.green.bold(
fs.readFileSync(path.resolve(__dirname, './banner.txt'), 'utf8') +
`\nv${version}`
)
);
const webpackOptions = yargs => { const webpackOptions = (yargs) => {
yargs.positional('config', { yargs
describe: 'webpack configuration options', .positional('config', {
type: 'string', describe: 'webpack configuration options',
default: '{}', type: 'string',
default: '{}',
}) })
.positional('babel', { .positional('babel', {
describe: 'Babel configuration object', describe: 'Babel configuration object',
type: 'string', type: 'string',
default: '{}', default: '{}',
}) })
.positional('targets', { .positional('targets', {
describe: 'Browser targets in browserslist query', describe: 'Browser targets in browserslist query',
type: 'string', type: 'string',
default: '> 0.25%, not dead', default: '> 0.25%, not dead',
}) })
.positional('entry', { .positional('entry', {
describe: 'Library entry point', describe: 'Library entry point',
type: 'string', type: 'string',
default: 'src/index', default: 'src/index',
}) })
.positional('output', { .positional('output', {
describe: 'Build destination directory', describe: 'Build destination directory',
type: 'string', type: 'string',
default: 'dist', default: 'dist',
}) });
} };
export const createCommands = (yargs) => { export const createCommands = (yargs) => {
return yargs return yargs
.command(['serve [port]', 'server'], 'Start the server', (yargs) => { .command(
['serve [port]', 'server'],
'Start the server',
(yargs) => {
yargs yargs
.positional('devServer', { .positional('devServer', {
describe: 'webpack-dev-server options', describe: 'webpack-dev-server options',
type: 'string', type: 'string',
default: '{}', default: '{}',
}) })
.positional('host', { .positional('host', {
alias: 'h', alias: 'h',
describe: 'Host to bind on', describe: 'Host to bind on',
type: 'string', type: 'string',
default: 'localhost', default: 'localhost',
}) })
.positional('port', { .positional('port', {
alias: 'p', alias: 'p',
describe: 'Port to bind on', describe: 'Port to bind on',
type: 'number', type: 'number',
default: 8080, default: 8080,
}) })
.positional('htmlWebpack', { .positional('htmlWebpack', {
describe: 'html-webpack-plugin options', describe: 'html-webpack-plugin options',
type: 'string', type: 'string',
default: '{}', default: '{}',
}) });
webpackOptions(yargs); webpackOptions(yargs);
}, (argv) => serve(argv)) },
.command('build', 'Build the source', (yargs) => { (argv) => serve(argv),
)
.command(
'build',
'Build the source',
(yargs) => {
yargs yargs
.positional('stats', { .positional('stats', {
describe: 'Options for webpack Stats instance', describe: 'Options for webpack Stats instance',
type: 'string', type: 'string',
default: '{}', default: '{}',
}) })
.positional('statsOutput', { .positional('statsOutput', {
describe: 'Specify the path where to output webpack stats file (eg. "stats.json")', describe: 'Specify the path where to output webpack stats file (eg. "stats.json")',
type: 'string', type: 'string',
default: '', default: '',
}) })
.positional('patch', { .positional('patch', {
describe: 'Increase automatically the patch version', describe: 'Increase automatically the patch version',
type: 'boolean', type: 'boolean',
default: true, default: true,
}) })
.positional('localePath', { .positional('localePath', {
describe: 'Path to the directory containing locale files', describe: 'Path to the directory containing locale files',
type: 'string', type: 'string',
default: 'src/locale', default: 'src/locale',
}) })
.positional('dts', { .positional('dts', {
describe: 'Generate typescript dts file ("include", "skip", "only")', describe: 'Generate typescript dts file ("include", "skip", "only")',
type: 'string', type: 'string',
default: 'include', default: 'include',
}); });
webpackOptions(yargs); webpackOptions(yargs);
}, (argv) => build(argv)) },
.command('init', 'Init GrapesJS plugin project', (yargs) => { (argv) => build(argv),
)
.command(
'init',
'Init GrapesJS plugin project',
(yargs) => {
yargs yargs
.positional('yes', { .positional('yes', {
alias: 'y', alias: 'y',
describe: 'All default answers', describe: 'All default answers',
type: 'boolean', type: 'boolean',
default: false, default: false,
}) })
.positional('name', { .positional('name', {
describe: 'Name of the project', describe: 'Name of the project',
type: 'string', type: 'string',
}) })
.positional('rName', { .positional('rName', {
describe: 'Repository name', describe: 'Repository name',
type: 'string', type: 'string',
}) })
.positional('user', { .positional('user', {
describe: 'Repository username', describe: 'Repository username',
type: 'string', type: 'string',
}) })
.positional('components', { .positional('components', {
describe: 'Indicate to include custom component types API', describe: 'Indicate to include custom component types API',
type: 'boolean', type: 'boolean',
}) })
.positional('blocks', { .positional('blocks', {
describe: 'Indicate to include blocks API', describe: 'Indicate to include blocks API',
type: 'boolean', type: 'boolean',
}) })
.positional('i18n', { .positional('i18n', {
describe: 'Indicate to include the support for i18n', describe: 'Indicate to include the support for i18n',
type: 'boolean', type: 'boolean',
}) })
.positional('license', { .positional('license', {
describe: 'License of the project', describe: 'License of the project',
type: 'string', type: 'string',
}) });
}, (argv) => init(argv)) },
(argv) => init(argv),
)
.options({ .options({
verbose: { verbose: {
alias: 'v', alias: 'v',
description: 'Run with verbose logging', description: 'Run with verbose logging',
type: 'boolean', // boolean | number | string type: 'boolean', // boolean | number | string
default: false, default: false,
}, },
}) })
.recommendCommands() .recommendCommands()
.strict() .strict();
} };
export const argsToOpts = async () => { export const argsToOpts = async () => {
return await createCommands(yargs).parse(); return await createCommands(yargs).parse();
}; };
export const run = async (opts = {}) => { export const run = async (opts = {}) => {
try { try {
let options = await argsToOpts(); let options = await argsToOpts();
if (!options._.length) yargs.showHelp(); if (!options._.length) yargs.showHelp();
} catch (error) { } catch (error) {
printError((error.stack || error).toString()) printError((error.stack || error).toString());
} }
} };
run(); run();

339
packages/cli/src/init.ts

@ -8,203 +8,210 @@ import template from 'lodash.template';
import { version } from '../package.json'; import { version } from '../package.json';
interface InitOptions { interface InitOptions {
license?: string; license?: string;
name?: string; name?: string;
components?: boolean; components?: boolean;
blocks?: boolean; blocks?: boolean;
i18n?: boolean; i18n?: boolean;
verbose?: boolean, verbose?: boolean;
rName?: string, rName?: string;
user?: string, user?: string;
yes?: boolean, yes?: boolean;
}; }
const tmpPath = './template'; const tmpPath = './template';
const rootPath = process.cwd(); const rootPath = process.cwd();
const getName = (str: string) => str const getName = (str: string) =>
str
.replace(/\_/g, '-') .replace(/\_/g, '-')
.split('-') .split('-')
.filter(i => i) .filter((i) => i)
.map(i => i[0].toUpperCase() + i.slice(1)) .map((i) => i[0].toUpperCase() + i.slice(1))
.join(' '); .join(' ');
const getTemplateFileContent = (pth: string) => { const getTemplateFileContent = (pth: string) => {
const pt = path.resolve(__dirname, `${tmpPath}/${pth}`); const pt = path.resolve(__dirname, `${tmpPath}/${pth}`);
return fs.readFileSync(pt, 'utf8'); return fs.readFileSync(pt, 'utf8');
}; };
const resolveRoot = (pth: string) => { const resolveRoot = (pth: string) => {
return path.resolve(rootPath, pth); return path.resolve(rootPath, pth);
}; };
const resolveLocal = (pth: string) => { const resolveLocal = (pth: string) => {
return path.resolve(__dirname, `${tmpPath}/${pth}`); return path.resolve(__dirname, `${tmpPath}/${pth}`);
}; };
const createSourceFiles = async (opts: InitOptions = {}) => { const createSourceFiles = async (opts: InitOptions = {}) => {
const rdmSrc = getTemplateFileContent('README.md') const rdmSrc = getTemplateFileContent('README.md');
const rdmDst = resolveRoot('README.md'); const rdmDst = resolveRoot('README.md');
const indxSrc = getTemplateFileContent('src/index.js'); const indxSrc = getTemplateFileContent('src/index.js');
const indxDst = resolveRoot('src/index.js'); const indxDst = resolveRoot('src/index.js');
const indexCnt = getTemplateFileContent('_index.html'); const indexCnt = getTemplateFileContent('_index.html');
const indexDst = resolveRoot('_index.html'); const indexDst = resolveRoot('_index.html');
const license = spdxLicenseList[opts.license]; const license = spdxLicenseList[opts.license];
const licenseTxt = license && (license.licenseText || '') const licenseTxt =
.replace('<year>', `${new Date().getFullYear()}-current`) license &&
.replace('<copyright holders>', opts.name); (license.licenseText || '')
ensureDir(indxDst); .replace('<year>', `${new Date().getFullYear()}-current`)
// write src/_index.html .replace('<copyright holders>', opts.name);
fs.writeFileSync(indxDst, template(indxSrc)(opts).trim()); ensureDir(indxDst);
// write _index.html // write src/_index.html
fs.writeFileSync(indexDst, template(indexCnt)(opts)); fs.writeFileSync(indxDst, template(indxSrc)(opts).trim());
// Write README.md // write _index.html
fs.writeFileSync(rdmDst, template(rdmSrc)(opts)); fs.writeFileSync(indexDst, template(indexCnt)(opts));
// write LICENSE // Write README.md
licenseTxt && fs.writeFileSync(resolveRoot('LICENSE'), licenseTxt); fs.writeFileSync(rdmDst, template(rdmSrc)(opts));
// Copy files // write LICENSE
fs.copyFileSync(resolveLocal('.gitignore-t'), resolveRoot('.gitignore')); licenseTxt && fs.writeFileSync(resolveRoot('LICENSE'), licenseTxt);
fs.copyFileSync(resolveLocal('.npmignore-t'), resolveRoot('.npmignore')); // Copy files
fs.copyFileSync(resolveLocal('tsconfig.json'), resolveRoot('tsconfig.json')); fs.copyFileSync(resolveLocal('.gitignore-t'), resolveRoot('.gitignore'));
fs.copyFileSync(resolveLocal('.npmignore-t'), resolveRoot('.npmignore'));
fs.copyFileSync(resolveLocal('tsconfig.json'), resolveRoot('tsconfig.json'));
}; };
const createFileComponents = (opts: InitOptions = {}) => { const createFileComponents = (opts: InitOptions = {}) => {
const filepath = 'src/components.js'; const filepath = 'src/components.js';
const cmpSrc = resolveLocal(filepath); const cmpSrc = resolveLocal(filepath);
const cmpDst = resolveRoot(filepath); const cmpDst = resolveRoot(filepath);
opts.components && fs.copyFileSync(cmpSrc, cmpDst); opts.components && fs.copyFileSync(cmpSrc, cmpDst);
}; };
const createFileBlocks = (opts: InitOptions = {}) => { const createFileBlocks = (opts: InitOptions = {}) => {
const filepath = 'src/blocks.js'; const filepath = 'src/blocks.js';
const blkSrc = resolveLocal(filepath); const blkSrc = resolveLocal(filepath);
const blkDst = resolveRoot(filepath); const blkDst = resolveRoot(filepath);
opts.blocks && fs.copyFileSync(blkSrc, blkDst); opts.blocks && fs.copyFileSync(blkSrc, blkDst);
}; };
const createI18n = (opts = {}) => { const createI18n = (opts = {}) => {
const enPath = 'src/locale/en.js'; const enPath = 'src/locale/en.js';
const tmpEn = getTemplateFileContent(enPath); const tmpEn = getTemplateFileContent(enPath);
const dstEn = resolveRoot(enPath); const dstEn = resolveRoot(enPath);
ensureDir(dstEn); ensureDir(dstEn);
fs.writeFileSync(dstEn, template(tmpEn)(opts)); fs.writeFileSync(dstEn, template(tmpEn)(opts));
}; };
const createPackage = (opts = {}) => { const createPackage = (opts = {}) => {
const filepath = 'package.json'; const filepath = 'package.json';
const cnt = getTemplateFileContent(filepath); const cnt = getTemplateFileContent(filepath);
const dst = resolveRoot(filepath); const dst = resolveRoot(filepath);
fs.writeFileSync(dst, template(cnt)({ fs.writeFileSync(
...opts, dst,
version, template(cnt)({
})); ...opts,
version,
}),
);
}; };
const checkBoolean = value => value && value !== 'false' ? true : false; const checkBoolean = (value) => (value && value !== 'false' ? true : false);
export const initPlugin = async(opts: InitOptions = {}) => { export const initPlugin = async (opts: InitOptions = {}) => {
printRow('Start project creation...'); printRow('Start project creation...');
opts.components = checkBoolean(opts.components); opts.components = checkBoolean(opts.components);
opts.blocks = checkBoolean(opts.blocks); opts.blocks = checkBoolean(opts.blocks);
opts.i18n = checkBoolean(opts.i18n); opts.i18n = checkBoolean(opts.i18n);
const tasks = new Listr([ const tasks = new Listr([
{ {
title: 'Creating initial source files', title: 'Creating initial source files',
task: () => createSourceFiles(opts), task: () => createSourceFiles(opts),
}, { },
title: 'Creating custom Component Type file', {
task: () => createFileComponents(opts), title: 'Creating custom Component Type file',
enabled: () => opts.components, task: () => createFileComponents(opts),
}, { enabled: () => opts.components,
title: 'Creating Blocks file', },
task: () => createFileBlocks(opts), {
enabled: () => opts.blocks, title: 'Creating Blocks file',
}, { task: () => createFileBlocks(opts),
title: 'Creating i18n structure', enabled: () => opts.blocks,
task: () => createI18n(opts), },
enabled: () => opts.i18n, {
}, { title: 'Creating i18n structure',
title: 'Update package.json', task: () => createI18n(opts),
task: () => createPackage(opts), enabled: () => opts.i18n,
}, },
]); {
await tasks.run(); title: 'Update package.json',
} task: () => createPackage(opts),
},
]);
await tasks.run();
};
export default async (opts: InitOptions = {}) => { export default async (opts: InitOptions = {}) => {
const rootDir = path.basename(process.cwd()); const rootDir = path.basename(process.cwd());
const questions = []; const questions = [];
const { const { verbose, name, rName, user, yes, components, blocks, i18n, license } = opts;
verbose, let results = {
name, name: name || getName(rootDir),
rName, rName: rName || rootDir,
user, user: user || 'YOUR-USERNAME',
yes, components: isUndefined(components) ? true : components,
components, blocks: isUndefined(blocks) ? true : blocks,
blocks, i18n: isUndefined(i18n) ? true : i18n,
i18n, license: license || 'MIT',
license, };
} = opts; printRow(`Init the project${verbose ? ' (verbose)' : ''}...`);
let results = {
name: name || getName(rootDir), if (!yes) {
rName: rName || rootDir, !name &&
user: user || 'YOUR-USERNAME', questions.push({
components: isUndefined(components) ? true : components, name: 'name',
blocks: isUndefined(blocks) ? true : blocks, message: 'Name of the project',
i18n: isUndefined(i18n) ? true : i18n, default: results.name,
license: license || 'MIT', });
}; !rName &&
printRow(`Init the project${verbose ? ' (verbose)' : ''}...`); questions.push({
name: 'rName',
if (!yes) { message: 'Repository name (used also as the plugin name)',
!name && questions.push({ default: results.rName,
name: 'name', });
message: 'Name of the project', !user &&
default: results.name, questions.push({
}); name: 'user',
!rName && questions.push({ message: 'Repository username (eg. on GitHub/Bitbucket)',
name: 'rName', default: results.user,
message: 'Repository name (used also as the plugin name)', });
default: results.rName, isUndefined(components) &&
}); questions.push({
!user && questions.push({ type: 'boolean',
name: 'user', name: 'components',
message: 'Repository username (eg. on GitHub/Bitbucket)', message: 'Will you need to add custom Component Types?',
default: results.user, default: results.components,
}); });
isUndefined(components) && questions.push({ isUndefined(blocks) &&
type: 'boolean', questions.push({
name: 'components', type: 'boolean',
message: 'Will you need to add custom Component Types?', name: 'blocks',
default: results.components, message: 'Will you need to add Blocks?',
}); default: results.blocks,
isUndefined(blocks) && questions.push({ });
type: 'boolean', isUndefined(i18n) &&
name: 'blocks', questions.push({
message: 'Will you need to add Blocks?', type: 'boolean',
default: results.blocks, name: 'i18n',
}); message: 'Do you want to setup i18n structure in this plugin?',
isUndefined(i18n) && questions.push({ default: results.i18n,
type: 'boolean', });
name: 'i18n', !license &&
message: 'Do you want to setup i18n structure in this plugin?', questions.push({
default: results.i18n, name: 'license',
}); message: 'License of the project',
!license && questions.push({ default: results.license,
name: 'license', });
message: 'License of the project', }
default: results.license,
}); const answers = await inquirer.prompt(questions);
} results = {
...results,
const answers = await inquirer.prompt(questions); ...answers,
results = { };
...results,
...answers, verbose && log({ results, opts });
} await initPlugin(results);
printRow('Project created! Happy coding');
verbose && log({ results, opts }); };
await initPlugin(results);
printRow('Project created! Happy coding');
}

2
packages/cli/src/main.ts

@ -1,3 +1,3 @@
export { default as init } from './init'; export { default as init } from './init';
export { default as build } from './build'; export { default as build } from './build';
export { default as serve } from './serve'; export { default as serve } from './serve';

58
packages/cli/src/serve.ts

@ -5,9 +5,9 @@ import webpackConfig from './webpack.config';
import chalk from 'chalk'; import chalk from 'chalk';
interface ServeOptions { interface ServeOptions {
host?: string; host?: string;
port?: number; port?: number;
verbose?: boolean; verbose?: boolean;
} }
/** /**
@ -15,34 +15,34 @@ interface ServeOptions {
* @param {Object} opts * @param {Object} opts
*/ */
export default (opts: ServeOptions = {}) => { export default (opts: ServeOptions = {}) => {
printRow('Start the development server...'); printRow('Start the development server...');
const { host, port } = opts; const { host, port } = opts;
const isVerb = opts.verbose; const isVerb = opts.verbose;
const resultWebpackConf = { const resultWebpackConf = {
...webpackConfig({ args: buildWebpackArgs(opts), cmdOpts: opts }), ...webpackConfig({ args: buildWebpackArgs(opts), cmdOpts: opts }),
...normalizeJsonOpt(opts, 'webpack'), ...normalizeJsonOpt(opts, 'webpack'),
}; };
const devServerConf = { const devServerConf = {
...resultWebpackConf.devServer, ...resultWebpackConf.devServer,
open: true, open: true,
...normalizeJsonOpt(opts, 'devServer'), ...normalizeJsonOpt(opts, 'devServer'),
}; };
if (host !== 'localhost') { if (host !== 'localhost') {
devServerConf.host = host; devServerConf.host = host;
} }
if (port !== 8080) { if (port !== 8080) {
devServerConf.port = port; devServerConf.port = port;
} }
if (isVerb) { if (isVerb) {
log(chalk.yellow('Server config:\n'), opts, '\n'); log(chalk.yellow('Server config:\n'), opts, '\n');
log(chalk.yellow('DevServer config:\n'), devServerConf, '\n'); log(chalk.yellow('DevServer config:\n'), devServerConf, '\n');
} }
const compiler = webpack(resultWebpackConf); const compiler = webpack(resultWebpackConf);
const server = new webpackDevServer(devServerConf, compiler); const server = new webpackDevServer(devServerConf, compiler);
server.start(); server.start();
}; };

59
packages/cli/src/template/README.md

@ -11,7 +11,7 @@ Below, you'll find the necessary HTML, CSS, and JavaScript. Copy and paste this
### HTML ### HTML
```html ```html
<link href="https://unpkg.com/grapesjs/dist/css/grapes.min.css" rel="stylesheet"> <link href="https://unpkg.com/grapesjs/dist/css/grapes.min.css" rel="stylesheet" />
<script src="https://unpkg.com/grapesjs"></script> <script src="https://unpkg.com/grapesjs"></script>
<script src="https://unpkg.com/<%= rName %>"></script> <script src="https://unpkg.com/<%= rName %>"></script>
@ -22,7 +22,7 @@ Below, you'll find the necessary HTML, CSS, and JavaScript. Copy and paste this
```js ```js
const editor = grapesjs.init({ const editor = grapesjs.init({
container: '#gjs', container: '#gjs',
height: '100%', height: '100%',
fromElement: true, fromElement: true,
storageManager: false, storageManager: false,
@ -33,7 +33,8 @@ const editor = grapesjs.init({
### CSS ### CSS
```css ```css
body, html { body,
html {
margin: 0; margin: 0;
height: 100%; height: 100%;
} }
@ -41,39 +42,37 @@ body, html {
## Summary ## Summary
* Plugin name: `<%= rName %>` - Plugin name: `<%= rName %>`
* Components - Components
* `component-id-1` - `component-id-1`
* `component-id-2` - `component-id-2`
* ... - ...
* Blocks - Blocks
* `block-id-1` - `block-id-1`
* `block-id-2` - `block-id-2`
* ... - ...
## Options ## Options
| Option | Description | Default | | Option | Description | Default |
|-|-|-| | --------- | ------------------ | --------------- |
| `option1` | Description option | `default value` | | `option1` | Description option | `default value` |
## Download ## Download
* CDN - CDN
* `https://unpkg.com/<%= rName %>` - `https://unpkg.com/<%= rName %>`
* NPM - NPM
* `npm i <%= rName %>` - `npm i <%= rName %>`
* GIT - GIT
* `git clone https://github.com/<%= user %>/<%= rName %>.git` - `git clone https://github.com/<%= user %>/<%= rName %>.git`
## Usage ## Usage
Directly in the browser Directly in the browser
```html ```html
<link href="https://unpkg.com/grapesjs/dist/css/grapes.min.css" rel="stylesheet"/> <link href="https://unpkg.com/grapesjs/dist/css/grapes.min.css" rel="stylesheet" />
<script src="https://unpkg.com/grapesjs"></script> <script src="https://unpkg.com/grapesjs"></script>
<script src="path/to/<%= rName %>.min.js"></script> <script src="path/to/<%= rName %>.min.js"></script>
@ -81,12 +80,14 @@ Directly in the browser
<script type="text/javascript"> <script type="text/javascript">
var editor = grapesjs.init({ var editor = grapesjs.init({
container: '#gjs', container: '#gjs',
// ... // ...
plugins: ['<%= rName %>'], plugins: ['<%= rName %>'],
pluginsOpts: { pluginsOpts: {
'<%= rName %>': { /* options */ } '<%= rName %>': {
} /* options */
},
},
}); });
</script> </script>
``` ```

2
packages/cli/src/template/src/blocks.js

@ -6,4 +6,4 @@ export default (editor, opts = {}) => {
content: { type: 'MY-COMPONENT' }, content: { type: 'MY-COMPONENT' },
// media: '<svg>...</svg>', // media: '<svg>...</svg>',
}); });
} };

4
packages/cli/src/template/src/components.js

@ -7,8 +7,6 @@ export default (editor, opts = {}) => {
// Default props // Default props
}, },
}, },
view: { view: {},
},
}); });
}; };

10
packages/cli/src/template/tsconfig.json

@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"sourceMap": true, "sourceMap": true,
"skipLibCheck": true, "skipLibCheck": true,
@ -20,7 +16,5 @@
"isolatedModules": true, "isolatedModules": true,
"noEmit": false "noEmit": false
}, },
"include": [ "include": ["src"]
"src"
]
} }

147
packages/cli/src/utils.ts

@ -11,27 +11,24 @@ export const isFunction = (value: any): value is Function => typeof value === 'f
export const isObject = (val: any) => val !== null && !Array.isArray(val) && typeof val === 'object'; export const isObject = (val: any) => val !== null && !Array.isArray(val) && typeof val === 'object';
export const printRow = (str: string, { export const printRow = (str: string, { color = 'green', lineDown = 1 } = {}) => {
color = 'green', console.log('');
lineDown = 1, console.log(chalk[color].bold(str));
} = {}) => { lineDown && console.log('');
console.log(''); };
console.log(chalk[color].bold(str));
lineDown && console.log('');
}
export const printError = (str: string) => { export const printError = (str: string) => {
printRow(str, { color: 'red' }); printRow(str, { color: 'red' });
} };
export const log = (...args: any[]) => console.log.apply(this, args); export const log = (...args: any[]) => console.log.apply(this, args);
export const ensureDir = (filePath: string) => { export const ensureDir = (filePath: string) => {
const dirname = path.dirname(filePath); const dirname = path.dirname(filePath);
if (fs.existsSync(dirname)) return true; if (fs.existsSync(dirname)) return true;
fs.mkdirSync(dirname); fs.mkdirSync(dirname);
return ensureDir(dirname); return ensureDir(dirname);
} };
/** /**
* Normalize JSON options * Normalize JSON options
@ -40,85 +37,87 @@ export const ensureDir = (filePath: string) => {
* @returns {Object} * @returns {Object}
*/ */
export const normalizeJsonOpt = (opts: Record<string, any>, key: string) => { export const normalizeJsonOpt = (opts: Record<string, any>, key: string) => {
let devServerOpt = opts[key] || {}; let devServerOpt = opts[key] || {};
if (isString(devServerOpt)) { if (isString(devServerOpt)) {
try { try {
devServerOpt = JSON.parse(devServerOpt); devServerOpt = JSON.parse(devServerOpt);
} catch (e) { } catch (e) {
printError(`Error while parsing "${key}" option`); printError(`Error while parsing "${key}" option`);
printError(e); printError(e);
devServerOpt = {} devServerOpt = {};
}
} }
}
return devServerOpt; return devServerOpt;
} };
export const buildWebpackArgs = (opts: Record<string, any>) => { export const buildWebpackArgs = (opts: Record<string, any>) => {
return { return {
...opts, ...opts,
babel: normalizeJsonOpt(opts, 'babel'), babel: normalizeJsonOpt(opts, 'babel'),
htmlWebpack: normalizeJsonOpt(opts, 'htmlWebpack'), htmlWebpack: normalizeJsonOpt(opts, 'htmlWebpack'),
} };
} };
export const copyRecursiveSync = (src: string, dest: string) => { export const copyRecursiveSync = (src: string, dest: string) => {
const exists = fs.existsSync(src); const exists = fs.existsSync(src);
const isDir = exists && fs.statSync(src).isDirectory(); const isDir = exists && fs.statSync(src).isDirectory();
if (isDir) { if (isDir) {
fs.mkdirSync(dest); fs.mkdirSync(dest);
fs.readdirSync(src).forEach((file) => { fs.readdirSync(src).forEach((file) => {
copyRecursiveSync(path.join(src, file), path.join(dest, file)); copyRecursiveSync(path.join(src, file), path.join(dest, file));
}); });
} else if (exists) { } else if (exists) {
fs.copyFileSync(src, dest); fs.copyFileSync(src, dest);
} }
}; };
export const isPathExists = async (path: string) => { export const isPathExists = async (path: string) => {
try { try {
await fsp.access(path); await fsp.access(path);
return true; return true;
} catch { } catch {
return false; return false;
} }
}; };
export const writeFile = async (filePath: string, data: string) => { export const writeFile = async (filePath: string, data: string) => {
try { try {
const dirname = path.dirname(filePath); const dirname = path.dirname(filePath);
const exist = await isPathExists(dirname); const exist = await isPathExists(dirname);
if (!exist) { if (!exist) {
await fsp.mkdir(dirname, { recursive: true }); await fsp.mkdir(dirname, { recursive: true });
}
await fsp.writeFile(filePath, data, 'utf8');
} catch (err) {
throw new Error(err);
} }
}
await fsp.writeFile(filePath, data, 'utf8');
} catch (err) {
throw new Error(err);
}
};
export const rootResolve = (val: string) => path.resolve(process.cwd(), val); export const rootResolve = (val: string) => path.resolve(process.cwd(), val);
export const originalRequire = () => { export const originalRequire = () => {
// @ts-ignore need this to use the original 'require.resolve' as it's replaced by webpack // @ts-ignore need this to use the original 'require.resolve' as it's replaced by webpack
return __non_webpack_require__; return __non_webpack_require__;
}; };
export const resolve = (value: string) => { export const resolve = (value: string) => {
return originalRequire().resolve(value); return originalRequire().resolve(value);
}; };
export const babelConfig = (opts: { targets?: string } = {}) => ({ export const babelConfig = (opts: { targets?: string } = {}) => ({
presets: [ presets: [
[ resolve('@babel/preset-env'), { [
targets: opts.targets, resolve('@babel/preset-env'),
// useBuiltIns: 'usage', // this makes the build much bigger {
// corejs: 3, targets: opts.targets,
} ] // useBuiltIns: 'usage', // this makes the build much bigger
// corejs: 3,
},
], ],
plugins: [ resolve('@babel/plugin-transform-runtime') ], ],
}) plugins: [resolve('@babel/plugin-transform-runtime')],
});

95
packages/cli/src/webpack.config.ts

@ -31,60 +31,66 @@ export default (opts: Record<string, any> = {}) => {
template = index; template = index;
} }
plugins.push(new HtmlWebpackPlugin({ plugins.push(
inject: 'head', new HtmlWebpackPlugin({
template, inject: 'head',
...htmlWebpack, template,
templateParameters: { ...htmlWebpack,
name, templateParameters: {
title: name, name,
gjsVersion: 'latest', title: name,
pathGjs: '', gjsVersion: 'latest',
pathGjsCss: '', pathGjs: '',
...htmlWebpack.templateParameters || {}, pathGjsCss: '',
}, ...(htmlWebpack.templateParameters || {}),
})); },
}),
);
} }
const outPath = path.resolve(dirCwd, args.output); const outPath = path.resolve(dirCwd, args.output);
const modulesPaths = [ 'node_modules', path.join(__dirname, '../node_modules')]; const modulesPaths = ['node_modules', path.join(__dirname, '../node_modules')];
let config = { let config = {
entry: path.resolve(dirCwd, args.entry), entry: path.resolve(dirCwd, args.entry),
mode: isProd ? 'production' : 'development', mode: isProd ? 'production' : 'development',
devtool: isProd ? 'source-map' : 'eval', devtool: isProd ? 'source-map' : 'eval',
optimization: { optimization: {
minimizer: [new TerserPlugin({ minimizer: [
extractComments: false, new TerserPlugin({
terserOptions: { extractComments: false,
compress: { terserOptions: {
evaluate: false, // Avoid breaking gjs scripts compress: {
evaluate: false, // Avoid breaking gjs scripts
},
output: {
comments: false,
quote_style: 3, // Preserve original quotes
preamble: banner, // banner here instead of BannerPlugin
},
}, },
output: { }),
comments: false, ],
quote_style: 3, // Preserve original quotes
preamble: banner, // banner here instead of BannerPlugin
}
}
})],
}, },
output: { output: {
path: outPath, path: outPath,
filename: 'index.js', filename: 'index.js',
library: name, library: name,
libraryTarget: 'umd', libraryTarget: 'umd',
globalObject: `typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this)`, globalObject: `typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this)`,
}, },
module: { module: {
rules: [{ rules: [
test: /\.tsx?$/, {
loader: resolve('ts-loader'), test: /\.tsx?$/,
exclude: /node_modules/, loader: resolve('ts-loader'),
options: { exclude: /node_modules/,
context: rootResolve(''), options: {
configFile: rootResolve('tsconfig.json'), context: rootResolve(''),
} configFile: rootResolve('tsconfig.json'),
}, { },
},
{
test: /\.js$/, test: /\.js$/,
loader: resolve('babel-loader'), loader: resolve('babel-loader'),
include: /src/, include: /src/,
@ -93,7 +99,8 @@ export default (opts: Record<string, any> = {}) => {
cacheDirectory: true, cacheDirectory: true,
...args.babel, ...args.babel,
}, },
}], },
],
}, },
resolve: { resolve: {
extensions: ['.tsx', '.ts', '.js'], extensions: ['.tsx', '.ts', '.js'],
@ -112,11 +119,11 @@ export default (opts: Record<string, any> = {}) => {
} }
if (isFunction(localWebpackConf)) { if (isFunction(localWebpackConf)) {
const fnRes = localWebpackConf({ config, webpack, pkg }); const fnRes = localWebpackConf({ config, webpack, pkg });
config = isObject(fnRes) ? fnRes : config; config = isObject(fnRes) ? fnRes : config;
} }
cmdOpts.verbose && log(chalk.yellow('Webpack config:\n'), config, '\n'); cmdOpts.verbose && log(chalk.yellow('Webpack config:\n'), config, '\n');
return config; return config;
} };

565
packages/cli/test/utils.spec.ts

@ -1,338 +1,359 @@
import { isFunction, isObject, isString, isUndefined, printRow, printError, log, ensureDir, normalizeJsonOpt, buildWebpackArgs, copyRecursiveSync, babelConfig, originalRequire, resolve, rootResolve } from "../src/utils"; import {
isFunction,
isObject,
isString,
isUndefined,
printRow,
printError,
log,
ensureDir,
normalizeJsonOpt,
buildWebpackArgs,
copyRecursiveSync,
babelConfig,
originalRequire,
resolve,
rootResolve,
} from '../src/utils';
import chalk from 'chalk'; import chalk from 'chalk';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import * as process from 'process'; import * as process from 'process';
const typeTestValues = { const typeTestValues = {
undefinedValue: undefined, undefinedValue: undefined,
nullValue: null, nullValue: null,
stringValue: 'hello', stringValue: 'hello',
emptyObject: {}, emptyObject: {},
nonEmptyObject: { key: 'value' }, nonEmptyObject: { key: 'value' },
emptyArray: [], emptyArray: [],
functionValue: () => { }, functionValue: () => {},
numberValue: 42, numberValue: 42,
booleanValue: true, booleanValue: true,
dateValue: new Date(), dateValue: new Date(),
}; };
function runTypeCheck(typeCheckFunction: (value: any) => boolean) { function runTypeCheck(typeCheckFunction: (value: any) => boolean) {
const keysWithPassingTypeChecks = Object.keys(typeTestValues).filter(key => { const keysWithPassingTypeChecks = Object.keys(typeTestValues).filter((key) => {
const value = typeTestValues[key]; const value = typeTestValues[key];
return typeCheckFunction(value); return typeCheckFunction(value);
}); });
return keysWithPassingTypeChecks; return keysWithPassingTypeChecks;
} }
jest.mock('fs'); jest.mock('fs');
jest.mock('fs/promises'); jest.mock('fs/promises');
describe('utils', () => { describe('utils', () => {
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
});
describe('isString', () => {
it('should correctly identify strings', () => {
const result = runTypeCheck(isString);
expect(result).toEqual(['stringValue']);
}); });
});
describe('isString', () => { describe('isUndefined', () => {
it('should correctly identify strings', () => { it('should correctly identify undefined values', () => {
const result = runTypeCheck(isString); const result = runTypeCheck(isUndefined);
expect(result).toEqual(['stringValue']); expect(result).toEqual(['undefinedValue']);
});
}); });
});
describe('isUndefined', () => { describe('isFunction', () => {
it('should correctly identify undefined values', () => { it('should correctly identify functions', () => {
const result = runTypeCheck(isUndefined); const result = runTypeCheck(isFunction);
expect(result).toEqual(['undefinedValue']); expect(result).toEqual(['functionValue']);
});
}); });
});
describe('isFunction', () => { describe('isObject', () => {
it('should correctly identify functions', () => { it('should correctly identify objects', () => {
const result = runTypeCheck(isFunction); const result = runTypeCheck(isObject);
expect(result).toEqual(['functionValue']); expect(result).toEqual(['emptyObject', 'nonEmptyObject', 'dateValue']);
});
}); });
});
describe('printRow', () => {
// TODO: We should refactor the function to make lineDown a boolean not a number
it('should console.log the given string with the specified color and line breaks', () => {
const str = 'Test string';
const color = 'blue';
const lineDown = 1;
console.log = jest.fn();
printRow(str, { color, lineDown });
describe('isObject', () => { expect(console.log).toHaveBeenCalledTimes(3); // 1 for empty line, 1 for colored string, 1 for line break
it('should correctly identify objects', () => { expect(console.log.mock.calls[1][0]).toEqual(chalk[color].bold(str));
const result = runTypeCheck(isObject);
expect(result).toEqual(['emptyObject', 'nonEmptyObject', 'dateValue']);
});
}); });
it('should not add a line break if lineDown is false', () => {
const str = 'Test string';
const color = 'green';
const lineDown = 0;
describe('printRow', () => { console.log = jest.fn();
// TODO: We should refactor the function to make lineDown a boolean not a number
it('should console.log the given string with the specified color and line breaks', () => {
const str = 'Test string';
const color = 'blue';
const lineDown = 1;
console.log = jest.fn(); printRow(str, { color, lineDown });
printRow(str, { color, lineDown }); expect(console.log).toHaveBeenCalledTimes(2); // 1 for empty line, 1 for colored string
});
});
describe('printError', () => {
it('should print the given string in red', () => {
const str = 'Error message';
expect(console.log).toHaveBeenCalledTimes(3); // 1 for empty line, 1 for colored string, 1 for line break console.log = jest.fn();
expect(console.log.mock.calls[1][0]).toEqual(chalk[color].bold(str));
});
it('should not add a line break if lineDown is false', () => { printError(str);
const str = 'Test string';
const color = 'green'; expect(console.log).toHaveBeenCalledTimes(3); // 1 for empty line, 1 for red string, 1 for line break
const lineDown = 0; expect(console.log.mock.calls[1][0]).toEqual(chalk.red.bold(str));
});
});
console.log = jest.fn(); describe('log', () => {
it('should call console.log with the given arguments', () => {
const arg1 = 'Argument 1';
const arg2 = 'Argument 2';
printRow(str, { color, lineDown }); console.log = jest.fn();
expect(console.log).toHaveBeenCalledTimes(2); // 1 for empty line, 1 for colored string log(arg1, arg2);
});
expect(console.log).toHaveBeenCalledWith(arg1, arg2);
}); });
});
describe('printError', () => { describe('ensureDir', () => {
it('should print the given string in red', () => { it('should return true when the directory already exists', () => {
const str = 'Error message'; (fs.existsSync as jest.Mock).mockReturnValue(true);
console.log = jest.fn(); const result = ensureDir('/path/to/file.txt');
expect(result).toBe(true);
expect(fs.existsSync).toHaveBeenCalledWith('/path/to');
expect(fs.mkdirSync).not.toHaveBeenCalled();
});
printError(str); it('should create the directory when it does not exist', () => {
(fs.existsSync as jest.Mock).mockReturnValueOnce(false).mockReturnValueOnce(true);
expect(console.log).toHaveBeenCalledTimes(3); // 1 for empty line, 1 for red string, 1 for line break const result = ensureDir('/path/to/file.txt');
expect(console.log.mock.calls[1][0]).toEqual(chalk.red.bold(str)); expect(result).toBe(true);
}); expect(fs.existsSync).toHaveBeenCalledWith('/path/to');
expect(fs.mkdirSync).toHaveBeenCalledWith('/path/to');
}); });
describe('log', () => { it('should create parent directories recursively when they do not exist', () => {
it('should call console.log with the given arguments', () => { (fs.existsSync as jest.Mock)
const arg1 = 'Argument 1'; .mockReturnValueOnce(false) // Check /path/to (does not exist)
const arg2 = 'Argument 2'; .mockReturnValueOnce(false) // Check /path (does not exist)
.mockReturnValueOnce(true); // Check / (root, exists)
console.log = jest.fn(); const result = ensureDir('/path/to/file.txt');
expect(result).toBe(true);
expect(fs.existsSync).toHaveBeenCalledTimes(3); // /path/to, /path, /
expect(fs.mkdirSync).toHaveBeenCalledTimes(2); // /path, /path/to
});
});
log(arg1, arg2); describe('normalizeJsonOpt', () => {
it('should return the object if the option is already an object', () => {
const opts = { babel: { presets: ['@babel/preset-env'] } };
const result = normalizeJsonOpt(opts, 'babel');
expect(result).toEqual(opts.babel);
});
expect(console.log).toHaveBeenCalledWith(arg1, arg2); it('should parse and return the object if the option is a valid JSON string', () => {
}); const opts = { babel: '{"presets":["@babel/preset-env"]}' };
const result = normalizeJsonOpt(opts, 'babel');
expect(result).toEqual({ presets: ['@babel/preset-env'] });
}); });
describe('ensureDir', () => { it('should return an empty object if the option is an invalid JSON string', () => {
it('should return true when the directory already exists', () => { const opts = { babel: '{"presets":["@babel/preset-env"]' }; // Invalid JSON
(fs.existsSync as jest.Mock).mockReturnValue(true); const result = normalizeJsonOpt(opts, 'babel');
expect(result).toEqual({});
const result = ensureDir('/path/to/file.txt');
expect(result).toBe(true);
expect(fs.existsSync).toHaveBeenCalledWith('/path/to');
expect(fs.mkdirSync).not.toHaveBeenCalled();
});
it('should create the directory when it does not exist', () => {
(fs.existsSync as jest.Mock).mockReturnValueOnce(false).mockReturnValueOnce(true);
const result = ensureDir('/path/to/file.txt');
expect(result).toBe(true);
expect(fs.existsSync).toHaveBeenCalledWith('/path/to');
expect(fs.mkdirSync).toHaveBeenCalledWith('/path/to');
});
it('should create parent directories recursively when they do not exist', () => {
(fs.existsSync as jest.Mock)
.mockReturnValueOnce(false) // Check /path/to (does not exist)
.mockReturnValueOnce(false) // Check /path (does not exist)
.mockReturnValueOnce(true); // Check / (root, exists)
const result = ensureDir('/path/to/file.txt');
expect(result).toBe(true);
expect(fs.existsSync).toHaveBeenCalledTimes(3); // /path/to, /path, /
expect(fs.mkdirSync).toHaveBeenCalledTimes(2); // /path, /path/to
});
}); });
describe('normalizeJsonOpt', () => { it('should return an empty object if the option is not provided', () => {
it('should return the object if the option is already an object', () => { const opts = {};
const opts = { babel: { presets: ['@babel/preset-env'] } }; const result = normalizeJsonOpt(opts, 'babel');
const result = normalizeJsonOpt(opts, 'babel'); expect(result).toEqual({});
expect(result).toEqual(opts.babel); });
}); });
it('should parse and return the object if the option is a valid JSON string', () => { describe('buildWebpackArgs', () => {
const opts = { babel: '{"presets":["@babel/preset-env"]}' }; it('should return the options with normalized JSON options for babel and htmlWebpack', () => {
const result = normalizeJsonOpt(opts, 'babel'); const opts = {
expect(result).toEqual({ presets: ['@babel/preset-env'] }); babel: '{"presets":["@babel/preset-env"]}',
}); htmlWebpack: '{"template":"./src/index.html"}',
otherOption: 'someValue',
it('should return an empty object if the option is an invalid JSON string', () => { };
const opts = { babel: '{"presets":["@babel/preset-env"]' }; // Invalid JSON
const result = normalizeJsonOpt(opts, 'babel'); const result = buildWebpackArgs(opts);
expect(result).toEqual({}); expect(result).toEqual({
}); babel: { presets: ['@babel/preset-env'] },
htmlWebpack: { template: './src/index.html' },
it('should return an empty object if the option is not provided', () => { otherOption: 'someValue',
const opts = {}; });
const result = normalizeJsonOpt(opts, 'babel');
expect(result).toEqual({});
});
}); });
describe('buildWebpackArgs', () => { it('should return empty objects for babel and htmlWebpack if they are invalid JSON strings', () => {
it('should return the options with normalized JSON options for babel and htmlWebpack', () => { const opts = {
const opts = { babel: '{"presets":["@babel/preset-env"]', // Invalid JSON
babel: '{"presets":["@babel/preset-env"]}', htmlWebpack: '{"template":"./src/index.html', // Invalid JSON
htmlWebpack: '{"template":"./src/index.html"}', };
otherOption: 'someValue'
}; const result = buildWebpackArgs(opts);
expect(result).toEqual({
const result = buildWebpackArgs(opts); babel: {},
expect(result).toEqual({ htmlWebpack: {},
babel: { presets: ['@babel/preset-env'] }, });
htmlWebpack: { template: './src/index.html' },
otherOption: 'someValue',
});
});
it('should return empty objects for babel and htmlWebpack if they are invalid JSON strings', () => {
const opts = {
babel: '{"presets":["@babel/preset-env"]', // Invalid JSON
htmlWebpack: '{"template":"./src/index.html', // Invalid JSON
};
const result = buildWebpackArgs(opts);
expect(result).toEqual({
babel: {},
htmlWebpack: {},
});
});
it('should return the original objects if babel and htmlWebpack are already objects', () => {
const opts = {
babel: { presets: ['@babel/preset-env'] },
htmlWebpack: { template: './src/index.html' },
};
const result = buildWebpackArgs(opts);
expect(result).toEqual({
babel: opts.babel,
htmlWebpack: opts.htmlWebpack,
});
});
it('should handle missing babel and htmlWebpack keys gracefully', () => {
const opts = { otherOption: 'someValue' };
const result = buildWebpackArgs(opts);
expect(result).toEqual({
babel: {},
htmlWebpack: {},
otherOption: 'someValue',
});
});
}); });
describe('copyRecursiveSync', () => { it('should return the original objects if babel and htmlWebpack are already objects', () => {
// TODO: Maybe this test case is a bit complex and we should think of an easier solution const opts = {
it('should copy a directory and its contents recursively', () => { babel: { presets: ['@babel/preset-env'] },
/** htmlWebpack: { template: './src/index.html' },
* First call: Mock as a directory with two files };
* Subsequent calls: Mock as a file
*/ const result = buildWebpackArgs(opts);
const existsSyncMock = (fs.existsSync as jest.Mock).mockReturnValue(true); expect(result).toEqual({
const statSyncMock = (fs.statSync as jest.Mock).mockReturnValueOnce({ isDirectory: () => true }).mockReturnValue({ isDirectory: () => false }); babel: opts.babel,
const readdirSyncMock = (fs.readdirSync as jest.Mock).mockReturnValueOnce(['file1.txt', 'file2.txt']).mockReturnValue([]); htmlWebpack: opts.htmlWebpack,
const copyFileSyncMock = (fs.copyFileSync as jest.Mock).mockImplementation(() => { }); });
copyRecursiveSync('/src', '/dest');
expect(existsSyncMock).toHaveBeenCalledWith('/src');
expect(statSyncMock).toHaveBeenCalledWith('/src');
expect(fs.mkdirSync).toHaveBeenCalledWith('/dest');
expect(readdirSyncMock).toHaveBeenCalledWith('/src');
expect(copyFileSyncMock).toHaveBeenCalledWith(path.normalize('/src/file1.txt'), path.normalize('/dest/file1.txt'));
expect(copyFileSyncMock).toHaveBeenCalledWith(path.normalize('/src/file2.txt'), path.normalize('/dest/file2.txt'));
});
it('should copy a file when source is a file', () => {
(fs.existsSync as jest.Mock).mockReturnValue(true);
(fs.statSync as jest.Mock).mockReturnValue({ isDirectory: () => false });
copyRecursiveSync('/src/file.txt', '/dest/file.txt');
expect(fs.existsSync).toHaveBeenCalledWith('/src/file.txt');
expect(fs.statSync).toHaveBeenCalledWith('/src/file.txt');
expect(fs.copyFileSync).toHaveBeenCalledWith('/src/file.txt', '/dest/file.txt');
});
// Maybe we can change the behavior to throw an error if the `src` doesn't exist
it('should do nothing when source does not exist', () => {
(fs.existsSync as jest.Mock).mockReturnValue(false);
copyRecursiveSync('/src/file.txt', '/dest/file.txt');
expect(fs.existsSync).toHaveBeenCalledWith('/src/file.txt');
expect(fs.statSync).not.toHaveBeenCalled();
expect(fs.mkdirSync).not.toHaveBeenCalled();
expect(fs.copyFileSync).not.toHaveBeenCalled();
});
}); });
describe('rootResolve', () => { it('should handle missing babel and htmlWebpack keys gracefully', () => {
it('should resolve a relative path to an absolute path', () => { const opts = { otherOption: 'someValue' };
const result = rootResolve('src/index.js');
expect(result).toBe(path.join(process.cwd(), 'src/index.js')); const result = buildWebpackArgs(opts);
}); expect(result).toEqual({
babel: {},
htmlWebpack: {},
otherOption: 'someValue',
});
});
});
describe('copyRecursiveSync', () => {
// TODO: Maybe this test case is a bit complex and we should think of an easier solution
it('should copy a directory and its contents recursively', () => {
/**
* First call: Mock as a directory with two files
* Subsequent calls: Mock as a file
*/
const existsSyncMock = (fs.existsSync as jest.Mock).mockReturnValue(true);
const statSyncMock = (fs.statSync as jest.Mock)
.mockReturnValueOnce({ isDirectory: () => true })
.mockReturnValue({ isDirectory: () => false });
const readdirSyncMock = (fs.readdirSync as jest.Mock)
.mockReturnValueOnce(['file1.txt', 'file2.txt'])
.mockReturnValue([]);
const copyFileSyncMock = (fs.copyFileSync as jest.Mock).mockImplementation(() => {});
copyRecursiveSync('/src', '/dest');
expect(existsSyncMock).toHaveBeenCalledWith('/src');
expect(statSyncMock).toHaveBeenCalledWith('/src');
expect(fs.mkdirSync).toHaveBeenCalledWith('/dest');
expect(readdirSyncMock).toHaveBeenCalledWith('/src');
expect(copyFileSyncMock).toHaveBeenCalledWith(
path.normalize('/src/file1.txt'),
path.normalize('/dest/file1.txt'),
);
expect(copyFileSyncMock).toHaveBeenCalledWith(
path.normalize('/src/file2.txt'),
path.normalize('/dest/file2.txt'),
);
}); });
describe('originalRequire', () => { it('should copy a file when source is a file', () => {
it('should return the original require.resolve function', () => { (fs.existsSync as jest.Mock).mockReturnValue(true);
const originalRequireMock = jest.fn(); (fs.statSync as jest.Mock).mockReturnValue({ isDirectory: () => false });
global.__non_webpack_require__ = originalRequireMock;
const result = originalRequire(); copyRecursiveSync('/src/file.txt', '/dest/file.txt');
expect(result).toBe(originalRequireMock); expect(fs.existsSync).toHaveBeenCalledWith('/src/file.txt');
}); expect(fs.statSync).toHaveBeenCalledWith('/src/file.txt');
expect(fs.copyFileSync).toHaveBeenCalledWith('/src/file.txt', '/dest/file.txt');
}); });
describe('resolve', () => { // Maybe we can change the behavior to throw an error if the `src` doesn't exist
it('should resolve a module path using the original require.resolve', () => { it('should do nothing when source does not exist', () => {
const originalRequireMock = { (fs.existsSync as jest.Mock).mockReturnValue(false);
resolve: jest.fn().mockReturnValue('resolved/path'),
};
global.__non_webpack_require__ = originalRequireMock;
const result = resolve('my-module'); copyRecursiveSync('/src/file.txt', '/dest/file.txt');
expect(result).toBe('resolved/path'); expect(fs.existsSync).toHaveBeenCalledWith('/src/file.txt');
expect(originalRequireMock.resolve).toHaveBeenCalledWith('my-module'); expect(fs.statSync).not.toHaveBeenCalled();
}); expect(fs.mkdirSync).not.toHaveBeenCalled();
expect(fs.copyFileSync).not.toHaveBeenCalled();
}); });
});
describe('rootResolve', () => {
it('should resolve a relative path to an absolute path', () => {
const result = rootResolve('src/index.js');
expect(result).toBe(path.join(process.cwd(), 'src/index.js'));
});
});
describe('originalRequire', () => {
it('should return the original require.resolve function', () => {
const originalRequireMock = jest.fn();
global.__non_webpack_require__ = originalRequireMock;
const result = originalRequire();
expect(result).toBe(originalRequireMock);
});
});
describe('resolve', () => {
it('should resolve a module path using the original require.resolve', () => {
const originalRequireMock = {
resolve: jest.fn().mockReturnValue('resolved/path'),
};
global.__non_webpack_require__ = originalRequireMock;
const result = resolve('my-module');
expect(result).toBe('resolved/path');
expect(originalRequireMock.resolve).toHaveBeenCalledWith('my-module');
});
});
describe('babelConfig', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('should return a Babel configuration object with specified presets and plugins', () => {
const result = babelConfig();
expect(result).toEqual({
presets: [[resolve('@babel/preset-env'), { targets: undefined }]],
plugins: [resolve('@babel/plugin-transform-runtime')],
});
});
it('should include the specified targets in the Babel configuration', () => {
const result = babelConfig({ targets: 'node 14' });
describe('babelConfig', () => { expect(result).toEqual({
afterEach(() => { presets: [[resolve('@babel/preset-env'), { targets: 'node 14' }]],
jest.restoreAllMocks(); plugins: [resolve('@babel/plugin-transform-runtime')],
}); });
it('should return a Babel configuration object with specified presets and plugins', () => {
const result = babelConfig();
expect(result).toEqual({
presets: [
[resolve('@babel/preset-env'), { targets: undefined }],
],
plugins: [resolve('@babel/plugin-transform-runtime')],
});
});
it('should include the specified targets in the Babel configuration', () => {
const result = babelConfig({ targets: 'node 14' });
expect(result).toEqual({
presets: [
[resolve('@babel/preset-env'), { targets: 'node 14' }],
],
plugins: [resolve('@babel/plugin-transform-runtime')],
});
});
}); });
}); });
});

4
packages/cli/webpack.cli.ts

@ -56,9 +56,7 @@ const config: Configuration = {
}), }),
], ],
externalsPresets: { node: true }, externalsPresets: { node: true },
externals: [ externals: [NodeExternals()],
NodeExternals(),
],
}; };
export default config; export default config;

Loading…
Cancel
Save