diff --git a/.eslintrc.js b/.eslintrc.js index 26227a912..b9dbff49a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,5 +52,5 @@ module.exports = { 'max-len': ['error', { code: 300 }], 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1 }], }, - ignorePatterns: ['*/docs/api/*', 'dist'], + ignorePatterns: ['*/docs/api/*', 'dist', 'packages/cli/src/template/**/*.*'], }; diff --git a/.prettierignore b/.prettierignore index 19fba71ff..87321f295 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ docs/**/*.md dist/ -pnpm-lock.yaml \ No newline at end of file +pnpm-lock.yaml +packages/cli/src/template/**/*.* \ No newline at end of file diff --git a/package.json b/package.json index 0fd0ddef6..8e72aa4ec 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "lint": "eslint .", "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", - "format": "prettier . --write", - "format:check": "prettier . --check" + "format": "prettier . --write --ignore-path .prettierignore", + "format:check": "prettier . --check --ignore-path .prettierignore" }, "devDependencies": { "@babel/cli": "7.24.8", diff --git a/packages/cli/README.md b/packages/cli/README.md index 5840694e9..6a05f4b7a 100644 --- a/packages/cli/README.md +++ b/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. -* Fast project scaffolding -* No need to touch Babel and Webpack configurations +- Fast project scaffolding +- No need to touch Babel and Webpack configurations ## Plugin from 0 to 100 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 mkdir grapesjs-my-plugin @@ -24,13 +24,13 @@ npm init -y git init ``` -* Install the package +- Install the package ```sh npm i -D grapesjs-cli ``` -* Init your plugin project by following few steps +- Init your plugin project by following few steps ```sh 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 ``` -* 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 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}' ``` -* 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 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. -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. +- 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. ## Customization @@ -80,16 +80,21 @@ If you need to customize the webpack configuration, you can create `webpack.conf // config is the default configuration export default ({ config }) => { - // This is how you can distinguish the `build` command from the `serve` - const isBuild = config.mode === 'production'; - - return { - ...config, - module: { - rules: [ { /* extra rule */ }, ...config.module.rules ], + // This is how you can distinguish the `build` command from the `serve` + const isBuild = config.mode === 'production'; + + return { + ...config, + module: { + rules: [ + { + /* extra rule */ }, - }; -} + ...config.module.rules, + ], + }, + }; +}; ``` ## Generic CLI usage @@ -112,7 +117,6 @@ Run the command grapesjs-cli COMMAND --OPT1 --OPT2=VALUE ``` - ## License MIT diff --git a/packages/cli/babel.config.js b/packages/cli/babel.config.js index 6432e2bbb..e6ffbd417 100644 --- a/packages/cli/babel.config.js +++ b/packages/cli/babel.config.js @@ -1,6 +1,3 @@ module.exports = { - presets: [ - ['@babel/preset-env', { targets: { node: 'current' } }], - '@babel/preset-typescript', - ], -}; \ No newline at end of file + presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], +}; diff --git a/packages/cli/index.html b/packages/cli/index.html index 911f28184..5a62789c4 100644 --- a/packages/cli/index.html +++ b/packages/cli/index.html @@ -1,9 +1,12 @@ - + <%= title %> - + -
-
- This is a demo content generated from GrapesJS CLI. - For the development, you should create a _index.html template file (might be a copy of this one) and on the next server start - the new file will be served, and it will be ignored by git. +
+
+ This is a demo content generated from GrapesJS CLI. For the development, you should create a _index.html + template file (might be a copy of this one) and on the next server start the new file will be served, and it + will be ignored by git.
- diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index 629f6831f..2afb77f0b 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -1,29 +1,29 @@ import { - printRow, - printError, - buildWebpackArgs, - normalizeJsonOpt, - copyRecursiveSync, - rootResolve, - babelConfig, - log, - writeFile, + printRow, + printError, + buildWebpackArgs, + normalizeJsonOpt, + copyRecursiveSync, + rootResolve, + babelConfig, + log, + writeFile, } from './utils'; -import { generateDtsBundle } from "dts-bundle-generator"; +import { generateDtsBundle } from 'dts-bundle-generator'; import webpack from 'webpack'; import fs from 'fs'; import webpackConfig from './webpack.config'; import { exec } from 'child_process'; import chalk from 'chalk'; -import rimraf from 'rimraf' +import rimraf from 'rimraf'; import { transformFileSync } from '@babel/core'; interface BuildOptions { - verbose?: boolean; - patch?: boolean; - statsOutput?: string; - localePath?: string; - dts?: 'include' | 'skip' | 'only'; + verbose?: boolean; + patch?: boolean; + statsOutput?: string; + localePath?: string; + dts?: 'include' | 'skip' | 'only'; } /** @@ -31,114 +31,113 @@ interface BuildOptions { * @param {Object} opts */ export const buildLocale = async (opts: BuildOptions = {}) => { - const { localePath } = opts; - if (!fs.existsSync(rootResolve(localePath))) return; - printRow('Start building locale files...', { lineDown: 0 }); - - await rimraf('locale'); - - const localDst = rootResolve('locale'); - copyRecursiveSync(rootResolve(localePath), localDst); - - // Create locale/index.js file - let result = ''; - fs.readdirSync(localDst).forEach(file => { - const name = file.split('.')[0]; - result += `export { default as ${name} } from './${name}'\n`; - }); - fs.writeFileSync(`${localDst}/index.js`, result); - - // Compile files - const babelOpts = { ...babelConfig(buildWebpackArgs(opts) as any) }; - fs.readdirSync(localDst).forEach(file => { - const filePath = `${localDst}/${file}`; - const compiled = transformFileSync(filePath, babelOpts).code; - fs.writeFileSync(filePath, compiled); - }); - - printRow('Locale files building completed successfully!'); -} + const { localePath } = opts; + if (!fs.existsSync(rootResolve(localePath))) return; + printRow('Start building locale files...', { lineDown: 0 }); + + await rimraf('locale'); + + const localDst = rootResolve('locale'); + copyRecursiveSync(rootResolve(localePath), localDst); + + // Create locale/index.js file + let result = ''; + fs.readdirSync(localDst).forEach((file) => { + const name = file.split('.')[0]; + result += `export { default as ${name} } from './${name}'\n`; + }); + fs.writeFileSync(`${localDst}/index.js`, result); + + // Compile files + const babelOpts = { ...babelConfig(buildWebpackArgs(opts) as any) }; + fs.readdirSync(localDst).forEach((file) => { + const filePath = `${localDst}/${file}`; + const compiled = transformFileSync(filePath, babelOpts).code; + fs.writeFileSync(filePath, compiled); + }); + + printRow('Locale files building completed successfully!'); +}; /** * Build TS declaration file * @param {Object} opts */ - export const buildDeclaration = async (opts: BuildOptions = {}) => { - const filePath = rootResolve('src/index.ts'); - if (!fs.existsSync(filePath)) return; +export const buildDeclaration = async (opts: BuildOptions = {}) => { + const filePath = rootResolve('src/index.ts'); + 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 bundleOptions = { preferredConfigPath: rootResolve('tsconfig.json') }; - const result = generateDtsBundle([entry], bundleOptions)[0]; - await writeFile(rootResolve('dist/index.d.ts'), result); + const entry = { filePath, output: { noBanner: true } }; + const bundleOptions = { preferredConfigPath: rootResolve('tsconfig.json') }; + const result = generateDtsBundle([entry], bundleOptions)[0]; + 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 * @param {Object} opts */ export default (opts: BuildOptions = {}) => { - printRow('Start building the library...'); - const isVerb = opts.verbose; - const { dts } = opts; - isVerb && log(chalk.yellow('Build config:\n'), opts, '\n'); - - const buildWebpack = () => { - const buildConf = { - ...webpackConfig({ - production: 1, - args: buildWebpackArgs(opts), - cmdOpts: opts, - }), - ...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!'); - } - }); + printRow('Start building the library...'); + const isVerb = opts.verbose; + const { dts } = opts; + isVerb && log(chalk.yellow('Build config:\n'), opts, '\n'); + + const buildWebpack = () => { + const buildConf = { + ...webpackConfig({ + production: 1, + args: buildWebpackArgs(opts), + cmdOpts: opts, + }), + ...normalizeJsonOpt(opts, 'config'), }; - if (opts.patch) { - isVerb && log(chalk.yellow('Patch the version'), '\n'); - exec('npm version --no-git-tag-version patch', buildWebpack); - } else { - buildWebpack(); + 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) { + isVerb && log(chalk.yellow('Patch the version'), '\n'); + exec('npm version --no-git-tag-version patch', buildWebpack); + } else { + buildWebpack(); + } }; diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index fec96b58c..20ecd1803 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -6,158 +6,169 @@ import chalk from 'chalk'; import { printError } from './utils'; import { version } from '../package.json'; -yargs.usage( - chalk.green.bold( - fs.readFileSync(path.resolve(__dirname, './banner.txt'), 'utf8') + - `\nv${version}` - ) -); +yargs.usage(chalk.green.bold(fs.readFileSync(path.resolve(__dirname, './banner.txt'), 'utf8') + `\nv${version}`)); -const webpackOptions = yargs => { - yargs.positional('config', { - describe: 'webpack configuration options', - type: 'string', - default: '{}', +const webpackOptions = (yargs) => { + yargs + .positional('config', { + describe: 'webpack configuration options', + type: 'string', + default: '{}', }) .positional('babel', { - describe: 'Babel configuration object', - type: 'string', - default: '{}', + describe: 'Babel configuration object', + type: 'string', + default: '{}', }) .positional('targets', { - describe: 'Browser targets in browserslist query', - type: 'string', - default: '> 0.25%, not dead', + describe: 'Browser targets in browserslist query', + type: 'string', + default: '> 0.25%, not dead', }) .positional('entry', { - describe: 'Library entry point', - type: 'string', - default: 'src/index', + describe: 'Library entry point', + type: 'string', + default: 'src/index', }) .positional('output', { - describe: 'Build destination directory', - type: 'string', - default: 'dist', - }) -} + describe: 'Build destination directory', + type: 'string', + default: 'dist', + }); +}; export const createCommands = (yargs) => { - return yargs - .command(['serve [port]', 'server'], 'Start the server', (yargs) => { + return yargs + .command( + ['serve [port]', 'server'], + 'Start the server', + (yargs) => { yargs - .positional('devServer', { + .positional('devServer', { describe: 'webpack-dev-server options', type: 'string', default: '{}', - }) - .positional('host', { + }) + .positional('host', { alias: 'h', describe: 'Host to bind on', type: 'string', default: 'localhost', - }) - .positional('port', { + }) + .positional('port', { alias: 'p', describe: 'Port to bind on', type: 'number', default: 8080, - }) - .positional('htmlWebpack', { + }) + .positional('htmlWebpack', { describe: 'html-webpack-plugin options', type: 'string', default: '{}', - }) + }); webpackOptions(yargs); - }, (argv) => serve(argv)) - .command('build', 'Build the source', (yargs) => { + }, + (argv) => serve(argv), + ) + .command( + 'build', + 'Build the source', + (yargs) => { yargs - .positional('stats', { + .positional('stats', { describe: 'Options for webpack Stats instance', type: 'string', default: '{}', - }) - .positional('statsOutput', { + }) + .positional('statsOutput', { describe: 'Specify the path where to output webpack stats file (eg. "stats.json")', type: 'string', default: '', - }) - .positional('patch', { + }) + .positional('patch', { describe: 'Increase automatically the patch version', type: 'boolean', default: true, - }) - .positional('localePath', { + }) + .positional('localePath', { describe: 'Path to the directory containing locale files', type: 'string', default: 'src/locale', - }) - .positional('dts', { + }) + .positional('dts', { describe: 'Generate typescript dts file ("include", "skip", "only")', type: 'string', default: 'include', - }); + }); webpackOptions(yargs); - }, (argv) => build(argv)) - .command('init', 'Init GrapesJS plugin project', (yargs) => { + }, + (argv) => build(argv), + ) + .command( + 'init', + 'Init GrapesJS plugin project', + (yargs) => { yargs - .positional('yes', { + .positional('yes', { alias: 'y', describe: 'All default answers', type: 'boolean', default: false, - }) - .positional('name', { + }) + .positional('name', { describe: 'Name of the project', type: 'string', - }) - .positional('rName', { + }) + .positional('rName', { describe: 'Repository name', type: 'string', - }) - .positional('user', { + }) + .positional('user', { describe: 'Repository username', type: 'string', - }) - .positional('components', { + }) + .positional('components', { describe: 'Indicate to include custom component types API', type: 'boolean', - }) - .positional('blocks', { + }) + .positional('blocks', { describe: 'Indicate to include blocks API', type: 'boolean', - }) - .positional('i18n', { + }) + .positional('i18n', { describe: 'Indicate to include the support for i18n', type: 'boolean', - }) - .positional('license', { + }) + .positional('license', { describe: 'License of the project', type: 'string', - }) - }, (argv) => init(argv)) + }); + }, + (argv) => init(argv), + ) .options({ - verbose: { - alias: 'v', - description: 'Run with verbose logging', - type: 'boolean', // boolean | number | string - default: false, - }, + verbose: { + alias: 'v', + description: 'Run with verbose logging', + type: 'boolean', // boolean | number | string + default: false, + }, }) .recommendCommands() - .strict() -} + .strict(); +}; export const argsToOpts = async () => { - return await createCommands(yargs).parse(); + return await createCommands(yargs).parse(); }; export const run = async (opts = {}) => { - try { - let options = await argsToOpts(); - if (!options._.length) yargs.showHelp(); - } catch (error) { - printError((error.stack || error).toString()) - } -} + try { + let options = await argsToOpts(); + if (!options._.length) yargs.showHelp(); + } catch (error) { + printError((error.stack || error).toString()); + } +}; -run(); \ No newline at end of file +run(); diff --git a/packages/cli/src/init.ts b/packages/cli/src/init.ts index 779b1f362..95e223dbe 100644 --- a/packages/cli/src/init.ts +++ b/packages/cli/src/init.ts @@ -8,203 +8,210 @@ import template from 'lodash.template'; import { version } from '../package.json'; interface InitOptions { - license?: string; - name?: string; - components?: boolean; - blocks?: boolean; - i18n?: boolean; - verbose?: boolean, - rName?: string, - user?: string, - yes?: boolean, -}; + license?: string; + name?: string; + components?: boolean; + blocks?: boolean; + i18n?: boolean; + verbose?: boolean; + rName?: string; + user?: string; + yes?: boolean; +} const tmpPath = './template'; const rootPath = process.cwd(); -const getName = (str: string) => str +const getName = (str: string) => + str .replace(/\_/g, '-') .split('-') - .filter(i => i) - .map(i => i[0].toUpperCase() + i.slice(1)) + .filter((i) => i) + .map((i) => i[0].toUpperCase() + i.slice(1)) .join(' '); const getTemplateFileContent = (pth: string) => { - const pt = path.resolve(__dirname, `${tmpPath}/${pth}`); - return fs.readFileSync(pt, 'utf8'); + const pt = path.resolve(__dirname, `${tmpPath}/${pth}`); + return fs.readFileSync(pt, 'utf8'); }; const resolveRoot = (pth: string) => { - return path.resolve(rootPath, pth); + return path.resolve(rootPath, pth); }; const resolveLocal = (pth: string) => { - return path.resolve(__dirname, `${tmpPath}/${pth}`); + return path.resolve(__dirname, `${tmpPath}/${pth}`); }; const createSourceFiles = async (opts: InitOptions = {}) => { - const rdmSrc = getTemplateFileContent('README.md') - const rdmDst = resolveRoot('README.md'); - const indxSrc = getTemplateFileContent('src/index.js'); - const indxDst = resolveRoot('src/index.js'); - const indexCnt = getTemplateFileContent('_index.html'); - const indexDst = resolveRoot('_index.html'); - const license = spdxLicenseList[opts.license]; - const licenseTxt = license && (license.licenseText || '') - .replace('', `${new Date().getFullYear()}-current`) - .replace('', opts.name); - ensureDir(indxDst); - // write src/_index.html - fs.writeFileSync(indxDst, template(indxSrc)(opts).trim()); - // write _index.html - fs.writeFileSync(indexDst, template(indexCnt)(opts)); - // Write README.md - fs.writeFileSync(rdmDst, template(rdmSrc)(opts)); - // write LICENSE - licenseTxt && fs.writeFileSync(resolveRoot('LICENSE'), licenseTxt); - // Copy files - fs.copyFileSync(resolveLocal('.gitignore-t'), resolveRoot('.gitignore')); - fs.copyFileSync(resolveLocal('.npmignore-t'), resolveRoot('.npmignore')); - fs.copyFileSync(resolveLocal('tsconfig.json'), resolveRoot('tsconfig.json')); + const rdmSrc = getTemplateFileContent('README.md'); + const rdmDst = resolveRoot('README.md'); + const indxSrc = getTemplateFileContent('src/index.js'); + const indxDst = resolveRoot('src/index.js'); + const indexCnt = getTemplateFileContent('_index.html'); + const indexDst = resolveRoot('_index.html'); + const license = spdxLicenseList[opts.license]; + const licenseTxt = + license && + (license.licenseText || '') + .replace('', `${new Date().getFullYear()}-current`) + .replace('', opts.name); + ensureDir(indxDst); + // write src/_index.html + fs.writeFileSync(indxDst, template(indxSrc)(opts).trim()); + // write _index.html + fs.writeFileSync(indexDst, template(indexCnt)(opts)); + // Write README.md + fs.writeFileSync(rdmDst, template(rdmSrc)(opts)); + // write LICENSE + licenseTxt && fs.writeFileSync(resolveRoot('LICENSE'), licenseTxt); + // Copy files + 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 filepath = 'src/components.js'; - const cmpSrc = resolveLocal(filepath); - const cmpDst = resolveRoot(filepath); - opts.components && fs.copyFileSync(cmpSrc, cmpDst); + const filepath = 'src/components.js'; + const cmpSrc = resolveLocal(filepath); + const cmpDst = resolveRoot(filepath); + opts.components && fs.copyFileSync(cmpSrc, cmpDst); }; const createFileBlocks = (opts: InitOptions = {}) => { - const filepath = 'src/blocks.js'; - const blkSrc = resolveLocal(filepath); - const blkDst = resolveRoot(filepath); - opts.blocks && fs.copyFileSync(blkSrc, blkDst); + const filepath = 'src/blocks.js'; + const blkSrc = resolveLocal(filepath); + const blkDst = resolveRoot(filepath); + opts.blocks && fs.copyFileSync(blkSrc, blkDst); }; const createI18n = (opts = {}) => { - const enPath = 'src/locale/en.js'; - const tmpEn = getTemplateFileContent(enPath); - const dstEn = resolveRoot(enPath); - ensureDir(dstEn); - fs.writeFileSync(dstEn, template(tmpEn)(opts)); + const enPath = 'src/locale/en.js'; + const tmpEn = getTemplateFileContent(enPath); + const dstEn = resolveRoot(enPath); + ensureDir(dstEn); + fs.writeFileSync(dstEn, template(tmpEn)(opts)); }; const createPackage = (opts = {}) => { - const filepath = 'package.json'; - const cnt = getTemplateFileContent(filepath); - const dst = resolveRoot(filepath); - fs.writeFileSync(dst, template(cnt)({ - ...opts, - version, - })); + const filepath = 'package.json'; + const cnt = getTemplateFileContent(filepath); + const dst = resolveRoot(filepath); + fs.writeFileSync( + dst, + template(cnt)({ + ...opts, + version, + }), + ); }; -const checkBoolean = value => value && value !== 'false' ? true : false; - -export const initPlugin = async(opts: InitOptions = {}) => { - printRow('Start project creation...'); - opts.components = checkBoolean(opts.components); - opts.blocks = checkBoolean(opts.blocks); - opts.i18n = checkBoolean(opts.i18n); - - const tasks = new Listr([ - { - title: 'Creating initial source files', - task: () => createSourceFiles(opts), - }, { - title: 'Creating custom Component Type file', - task: () => createFileComponents(opts), - enabled: () => opts.components, - }, { - title: 'Creating Blocks file', - task: () => createFileBlocks(opts), - enabled: () => opts.blocks, - }, { - title: 'Creating i18n structure', - task: () => createI18n(opts), - enabled: () => opts.i18n, - }, { - title: 'Update package.json', - task: () => createPackage(opts), - }, - ]); - await tasks.run(); -} +const checkBoolean = (value) => (value && value !== 'false' ? true : false); + +export const initPlugin = async (opts: InitOptions = {}) => { + printRow('Start project creation...'); + opts.components = checkBoolean(opts.components); + opts.blocks = checkBoolean(opts.blocks); + opts.i18n = checkBoolean(opts.i18n); + + const tasks = new Listr([ + { + title: 'Creating initial source files', + task: () => createSourceFiles(opts), + }, + { + title: 'Creating custom Component Type file', + task: () => createFileComponents(opts), + enabled: () => opts.components, + }, + { + title: 'Creating Blocks file', + task: () => createFileBlocks(opts), + enabled: () => opts.blocks, + }, + { + title: 'Creating i18n structure', + task: () => createI18n(opts), + enabled: () => opts.i18n, + }, + { + title: 'Update package.json', + task: () => createPackage(opts), + }, + ]); + await tasks.run(); +}; export default async (opts: InitOptions = {}) => { - const rootDir = path.basename(process.cwd()); - const questions = []; - const { - verbose, - name, - rName, - user, - yes, - components, - blocks, - i18n, - license, - } = opts; - let results = { - name: name || getName(rootDir), - rName: rName || rootDir, - user: user || 'YOUR-USERNAME', - components: isUndefined(components) ? true : components, - blocks: isUndefined(blocks) ? true : blocks, - i18n: isUndefined(i18n) ? true : i18n, - license: license || 'MIT', - }; - printRow(`Init the project${verbose ? ' (verbose)' : ''}...`); - - if (!yes) { - !name && questions.push({ - name: 'name', - message: 'Name of the project', - default: results.name, - }); - !rName && questions.push({ - name: 'rName', - message: 'Repository name (used also as the plugin name)', - default: results.rName, - }); - !user && questions.push({ - name: 'user', - message: 'Repository username (eg. on GitHub/Bitbucket)', - default: results.user, - }); - isUndefined(components) && questions.push({ - type: 'boolean', - name: 'components', - message: 'Will you need to add custom Component Types?', - default: results.components, - }); - isUndefined(blocks) && questions.push({ - type: 'boolean', - name: 'blocks', - message: 'Will you need to add Blocks?', - default: results.blocks, - }); - isUndefined(i18n) && questions.push({ - type: 'boolean', - name: 'i18n', - message: 'Do you want to setup i18n structure in this plugin?', - default: results.i18n, - }); - !license && questions.push({ - name: 'license', - message: 'License of the project', - default: results.license, - }); - } - - const answers = await inquirer.prompt(questions); - results = { - ...results, - ...answers, - } - - verbose && log({ results, opts }); - await initPlugin(results); - printRow('Project created! Happy coding'); -} \ No newline at end of file + const rootDir = path.basename(process.cwd()); + const questions = []; + const { verbose, name, rName, user, yes, components, blocks, i18n, license } = opts; + let results = { + name: name || getName(rootDir), + rName: rName || rootDir, + user: user || 'YOUR-USERNAME', + components: isUndefined(components) ? true : components, + blocks: isUndefined(blocks) ? true : blocks, + i18n: isUndefined(i18n) ? true : i18n, + license: license || 'MIT', + }; + printRow(`Init the project${verbose ? ' (verbose)' : ''}...`); + + if (!yes) { + !name && + questions.push({ + name: 'name', + message: 'Name of the project', + default: results.name, + }); + !rName && + questions.push({ + name: 'rName', + message: 'Repository name (used also as the plugin name)', + default: results.rName, + }); + !user && + questions.push({ + name: 'user', + message: 'Repository username (eg. on GitHub/Bitbucket)', + default: results.user, + }); + isUndefined(components) && + questions.push({ + type: 'boolean', + name: 'components', + message: 'Will you need to add custom Component Types?', + default: results.components, + }); + isUndefined(blocks) && + questions.push({ + type: 'boolean', + name: 'blocks', + message: 'Will you need to add Blocks?', + default: results.blocks, + }); + isUndefined(i18n) && + questions.push({ + type: 'boolean', + name: 'i18n', + message: 'Do you want to setup i18n structure in this plugin?', + default: results.i18n, + }); + !license && + questions.push({ + name: 'license', + message: 'License of the project', + default: results.license, + }); + } + + const answers = await inquirer.prompt(questions); + results = { + ...results, + ...answers, + }; + + verbose && log({ results, opts }); + await initPlugin(results); + printRow('Project created! Happy coding'); +}; diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index 7970ff208..2cfdd3396 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -1,3 +1,3 @@ export { default as init } from './init'; export { default as build } from './build'; -export { default as serve } from './serve'; \ No newline at end of file +export { default as serve } from './serve'; diff --git a/packages/cli/src/serve.ts b/packages/cli/src/serve.ts index 4af037ae5..3dfba4ae9 100644 --- a/packages/cli/src/serve.ts +++ b/packages/cli/src/serve.ts @@ -5,9 +5,9 @@ import webpackConfig from './webpack.config'; import chalk from 'chalk'; interface ServeOptions { - host?: string; - port?: number; - verbose?: boolean; + host?: string; + port?: number; + verbose?: boolean; } /** @@ -15,34 +15,34 @@ interface ServeOptions { * @param {Object} opts */ export default (opts: ServeOptions = {}) => { - printRow('Start the development server...'); - const { host, port } = opts; - const isVerb = opts.verbose; - const resultWebpackConf = { - ...webpackConfig({ args: buildWebpackArgs(opts), cmdOpts: opts }), - ...normalizeJsonOpt(opts, 'webpack'), - }; - const devServerConf = { - ...resultWebpackConf.devServer, - open: true, - ...normalizeJsonOpt(opts, 'devServer'), - }; + printRow('Start the development server...'); + const { host, port } = opts; + const isVerb = opts.verbose; + const resultWebpackConf = { + ...webpackConfig({ args: buildWebpackArgs(opts), cmdOpts: opts }), + ...normalizeJsonOpt(opts, 'webpack'), + }; + const devServerConf = { + ...resultWebpackConf.devServer, + open: true, + ...normalizeJsonOpt(opts, 'devServer'), + }; - if (host !== 'localhost') { - devServerConf.host = host; - } + if (host !== 'localhost') { + devServerConf.host = host; + } - if (port !== 8080) { - devServerConf.port = port; - } + if (port !== 8080) { + devServerConf.port = port; + } - if (isVerb) { - log(chalk.yellow('Server config:\n'), opts, '\n'); - log(chalk.yellow('DevServer config:\n'), devServerConf, '\n'); - } + if (isVerb) { + log(chalk.yellow('Server config:\n'), opts, '\n'); + log(chalk.yellow('DevServer config:\n'), devServerConf, '\n'); + } - const compiler = webpack(resultWebpackConf); - const server = new webpackDevServer(devServerConf, compiler); + const compiler = webpack(resultWebpackConf); + const server = new webpackDevServer(devServerConf, compiler); - server.start(); -}; \ No newline at end of file + server.start(); +}; diff --git a/packages/cli/src/template/README.md b/packages/cli/src/template/README.md index adb56eab6..311b16fec 100644 --- a/packages/cli/src/template/README.md +++ b/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 - + @@ -22,7 +22,7 @@ Below, you'll find the necessary HTML, CSS, and JavaScript. Copy and paste this ```js const editor = grapesjs.init({ - container: '#gjs', + container: '#gjs', height: '100%', fromElement: true, storageManager: false, @@ -33,7 +33,8 @@ const editor = grapesjs.init({ ### CSS ```css -body, html { +body, +html { margin: 0; height: 100%; } @@ -41,39 +42,37 @@ body, html { ## Summary -* Plugin name: `<%= rName %>` -* Components - * `component-id-1` - * `component-id-2` - * ... -* Blocks - * `block-id-1` - * `block-id-2` - * ... +- Plugin name: `<%= rName %>` +- Components + - `component-id-1` + - `component-id-2` + - ... +- Blocks + - `block-id-1` + - `block-id-2` + - ... ## Options -| Option | Description | Default | -|-|-|-| +| Option | Description | Default | +| --------- | ------------------ | --------------- | | `option1` | Description option | `default value` | ## Download -* CDN - * `https://unpkg.com/<%= rName %>` -* NPM - * `npm i <%= rName %>` -* GIT - * `git clone https://github.com/<%= user %>/<%= rName %>.git` - - +- CDN + - `https://unpkg.com/<%= rName %>` +- NPM + - `npm i <%= rName %>` +- GIT + - `git clone https://github.com/<%= user %>/<%= rName %>.git` ## Usage Directly in the browser ```html - + @@ -81,12 +80,14 @@ Directly in the browser ``` diff --git a/packages/cli/src/template/src/blocks.js b/packages/cli/src/template/src/blocks.js index e8c0e83b2..9a889ec4d 100644 --- a/packages/cli/src/template/src/blocks.js +++ b/packages/cli/src/template/src/blocks.js @@ -6,4 +6,4 @@ export default (editor, opts = {}) => { content: { type: 'MY-COMPONENT' }, // media: '...', }); -} +}; diff --git a/packages/cli/src/template/src/components.js b/packages/cli/src/template/src/components.js index 70c691791..78ef54ca0 100644 --- a/packages/cli/src/template/src/components.js +++ b/packages/cli/src/template/src/components.js @@ -7,8 +7,6 @@ export default (editor, opts = {}) => { // Default props }, }, - view: { - - }, + view: {}, }); }; diff --git a/packages/cli/src/template/tsconfig.json b/packages/cli/src/template/tsconfig.json index e56e47d36..f9599dd33 100644 --- a/packages/cli/src/template/tsconfig.json +++ b/packages/cli/src/template/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "sourceMap": true, "skipLibCheck": true, @@ -20,7 +16,5 @@ "isolatedModules": true, "noEmit": false }, - "include": [ - "src" - ] + "include": ["src"] } diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index 897509836..a3dfcb4ac 100644 --- a/packages/cli/src/utils.ts +++ b/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 printRow = (str: string, { - color = 'green', - lineDown = 1, -} = {}) => { - console.log(''); - console.log(chalk[color].bold(str)); - lineDown && console.log(''); -} +export const printRow = (str: string, { color = 'green', lineDown = 1 } = {}) => { + console.log(''); + console.log(chalk[color].bold(str)); + lineDown && console.log(''); +}; 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 ensureDir = (filePath: string) => { - const dirname = path.dirname(filePath); - if (fs.existsSync(dirname)) return true; - fs.mkdirSync(dirname); - return ensureDir(dirname); -} + const dirname = path.dirname(filePath); + if (fs.existsSync(dirname)) return true; + fs.mkdirSync(dirname); + return ensureDir(dirname); +}; /** * Normalize JSON options @@ -40,85 +37,87 @@ export const ensureDir = (filePath: string) => { * @returns {Object} */ export const normalizeJsonOpt = (opts: Record, key: string) => { - let devServerOpt = opts[key] || {}; - - if (isString(devServerOpt)) { - try { - devServerOpt = JSON.parse(devServerOpt); - } catch (e) { - printError(`Error while parsing "${key}" option`); - printError(e); - devServerOpt = {} - } + let devServerOpt = opts[key] || {}; + + if (isString(devServerOpt)) { + try { + devServerOpt = JSON.parse(devServerOpt); + } catch (e) { + printError(`Error while parsing "${key}" option`); + printError(e); + devServerOpt = {}; } + } - return devServerOpt; -} + return devServerOpt; +}; export const buildWebpackArgs = (opts: Record) => { - return { - ...opts, - babel: normalizeJsonOpt(opts, 'babel'), - htmlWebpack: normalizeJsonOpt(opts, 'htmlWebpack'), - } -} + return { + ...opts, + babel: normalizeJsonOpt(opts, 'babel'), + htmlWebpack: normalizeJsonOpt(opts, 'htmlWebpack'), + }; +}; export const copyRecursiveSync = (src: string, dest: string) => { - const exists = fs.existsSync(src); - const isDir = exists && fs.statSync(src).isDirectory(); - - if (isDir) { - fs.mkdirSync(dest); - fs.readdirSync(src).forEach((file) => { - copyRecursiveSync(path.join(src, file), path.join(dest, file)); - }); - } else if (exists) { - fs.copyFileSync(src, dest); - } + const exists = fs.existsSync(src); + const isDir = exists && fs.statSync(src).isDirectory(); + + if (isDir) { + fs.mkdirSync(dest); + fs.readdirSync(src).forEach((file) => { + copyRecursiveSync(path.join(src, file), path.join(dest, file)); + }); + } else if (exists) { + fs.copyFileSync(src, dest); + } }; export const isPathExists = async (path: string) => { - try { - await fsp.access(path); - return true; - } catch { - return false; - } - }; - + try { + await fsp.access(path); + return true; + } catch { + return false; + } +}; export const writeFile = async (filePath: string, data: string) => { - try { - const dirname = path.dirname(filePath); - const exist = await isPathExists(dirname); - if (!exist) { - await fsp.mkdir(dirname, { recursive: true }); - } - - await fsp.writeFile(filePath, data, 'utf8'); - } catch (err) { - throw new Error(err); + try { + const dirname = path.dirname(filePath); + const exist = await isPathExists(dirname); + if (!exist) { + await fsp.mkdir(dirname, { recursive: true }); } -} + + await fsp.writeFile(filePath, data, 'utf8'); + } catch (err) { + throw new Error(err); + } +}; export const rootResolve = (val: string) => path.resolve(process.cwd(), val); export const originalRequire = () => { - // @ts-ignore need this to use the original 'require.resolve' as it's replaced by webpack - return __non_webpack_require__; + // @ts-ignore need this to use the original 'require.resolve' as it's replaced by webpack + return __non_webpack_require__; }; export const resolve = (value: string) => { - return originalRequire().resolve(value); + return originalRequire().resolve(value); }; export const babelConfig = (opts: { targets?: string } = {}) => ({ - presets: [ - [ resolve('@babel/preset-env'), { - targets: opts.targets, - // useBuiltIns: 'usage', // this makes the build much bigger - // corejs: 3, - } ] + presets: [ + [ + resolve('@babel/preset-env'), + { + targets: opts.targets, + // useBuiltIns: 'usage', // this makes the build much bigger + // corejs: 3, + }, ], - plugins: [ resolve('@babel/plugin-transform-runtime') ], -}) \ No newline at end of file + ], + plugins: [resolve('@babel/plugin-transform-runtime')], +}); diff --git a/packages/cli/src/webpack.config.ts b/packages/cli/src/webpack.config.ts index 72fca71e2..32069f05f 100644 --- a/packages/cli/src/webpack.config.ts +++ b/packages/cli/src/webpack.config.ts @@ -31,60 +31,66 @@ export default (opts: Record = {}) => { template = index; } - plugins.push(new HtmlWebpackPlugin({ - inject: 'head', - template, - ...htmlWebpack, - templateParameters: { - name, - title: name, - gjsVersion: 'latest', - pathGjs: '', - pathGjsCss: '', - ...htmlWebpack.templateParameters || {}, - }, - })); + plugins.push( + new HtmlWebpackPlugin({ + inject: 'head', + template, + ...htmlWebpack, + templateParameters: { + name, + title: name, + gjsVersion: 'latest', + pathGjs: '', + pathGjsCss: '', + ...(htmlWebpack.templateParameters || {}), + }, + }), + ); } 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 = { entry: path.resolve(dirCwd, args.entry), mode: isProd ? 'production' : 'development', devtool: isProd ? 'source-map' : 'eval', optimization: { - minimizer: [new TerserPlugin({ - extractComments: false, - terserOptions: { - compress: { - evaluate: false, // Avoid breaking gjs scripts + minimizer: [ + new TerserPlugin({ + extractComments: false, + terserOptions: { + 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: { - path: outPath, - filename: 'index.js', - library: name, - libraryTarget: 'umd', - globalObject: `typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this)`, + path: outPath, + filename: 'index.js', + library: name, + libraryTarget: 'umd', + globalObject: `typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this)`, }, module: { - rules: [{ - test: /\.tsx?$/, - loader: resolve('ts-loader'), - exclude: /node_modules/, - options: { - context: rootResolve(''), - configFile: rootResolve('tsconfig.json'), - } - }, { + rules: [ + { + test: /\.tsx?$/, + loader: resolve('ts-loader'), + exclude: /node_modules/, + options: { + context: rootResolve(''), + configFile: rootResolve('tsconfig.json'), + }, + }, + { test: /\.js$/, loader: resolve('babel-loader'), include: /src/, @@ -93,7 +99,8 @@ export default (opts: Record = {}) => { cacheDirectory: true, ...args.babel, }, - }], + }, + ], }, resolve: { extensions: ['.tsx', '.ts', '.js'], @@ -112,11 +119,11 @@ export default (opts: Record = {}) => { } if (isFunction(localWebpackConf)) { - const fnRes = localWebpackConf({ config, webpack, pkg }); - config = isObject(fnRes) ? fnRes : config; + const fnRes = localWebpackConf({ config, webpack, pkg }); + config = isObject(fnRes) ? fnRes : config; } cmdOpts.verbose && log(chalk.yellow('Webpack config:\n'), config, '\n'); return config; -} +}; diff --git a/packages/cli/test/utils.spec.ts b/packages/cli/test/utils.spec.ts index c295d918c..d01d2f75c 100644 --- a/packages/cli/test/utils.spec.ts +++ b/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 fs from 'fs'; import path from 'path'; import * as process from 'process'; const typeTestValues = { - undefinedValue: undefined, - nullValue: null, - stringValue: 'hello', - emptyObject: {}, - nonEmptyObject: { key: 'value' }, - emptyArray: [], - functionValue: () => { }, - numberValue: 42, - booleanValue: true, - dateValue: new Date(), + undefinedValue: undefined, + nullValue: null, + stringValue: 'hello', + emptyObject: {}, + nonEmptyObject: { key: 'value' }, + emptyArray: [], + functionValue: () => {}, + numberValue: 42, + booleanValue: true, + dateValue: new Date(), }; function runTypeCheck(typeCheckFunction: (value: any) => boolean) { - const keysWithPassingTypeChecks = Object.keys(typeTestValues).filter(key => { - const value = typeTestValues[key]; - return typeCheckFunction(value); - }); + const keysWithPassingTypeChecks = Object.keys(typeTestValues).filter((key) => { + const value = typeTestValues[key]; + return typeCheckFunction(value); + }); - return keysWithPassingTypeChecks; + return keysWithPassingTypeChecks; } jest.mock('fs'); jest.mock('fs/promises'); describe('utils', () => { - afterEach(() => { - jest.clearAllMocks(); + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('isString', () => { + it('should correctly identify strings', () => { + const result = runTypeCheck(isString); + expect(result).toEqual(['stringValue']); }); + }); - describe('isString', () => { - it('should correctly identify strings', () => { - const result = runTypeCheck(isString); - expect(result).toEqual(['stringValue']); - }); + describe('isUndefined', () => { + it('should correctly identify undefined values', () => { + const result = runTypeCheck(isUndefined); + expect(result).toEqual(['undefinedValue']); }); + }); - describe('isUndefined', () => { - it('should correctly identify undefined values', () => { - const result = runTypeCheck(isUndefined); - expect(result).toEqual(['undefinedValue']); - }); + describe('isFunction', () => { + it('should correctly identify functions', () => { + const result = runTypeCheck(isFunction); + expect(result).toEqual(['functionValue']); }); + }); - describe('isFunction', () => { - it('should correctly identify functions', () => { - const result = runTypeCheck(isFunction); - expect(result).toEqual(['functionValue']); - }); + describe('isObject', () => { + it('should correctly identify objects', () => { + const result = runTypeCheck(isObject); + 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', () => { - it('should correctly identify objects', () => { - const result = runTypeCheck(isObject); - expect(result).toEqual(['emptyObject', 'nonEmptyObject', 'dateValue']); - }); + expect(console.log).toHaveBeenCalledTimes(3); // 1 for empty line, 1 for colored string, 1 for line break + expect(console.log.mock.calls[1][0]).toEqual(chalk[color].bold(str)); }); + it('should not add a line break if lineDown is false', () => { + const str = 'Test string'; + const color = 'green'; + const lineDown = 0; - 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(); - 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 - expect(console.log.mock.calls[1][0]).toEqual(chalk[color].bold(str)); - }); + console.log = jest.fn(); - it('should not add a line break if lineDown is false', () => { - const str = 'Test string'; - const color = 'green'; - const lineDown = 0; + printError(str); + + expect(console.log).toHaveBeenCalledTimes(3); // 1 for empty line, 1 for red string, 1 for line break + 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', () => { - it('should print the given string in red', () => { - const str = 'Error message'; + describe('ensureDir', () => { + it('should return true when the directory already exists', () => { + (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 - expect(console.log.mock.calls[1][0]).toEqual(chalk.red.bold(str)); - }); + const result = ensureDir('/path/to/file.txt'); + expect(result).toBe(true); + expect(fs.existsSync).toHaveBeenCalledWith('/path/to'); + expect(fs.mkdirSync).toHaveBeenCalledWith('/path/to'); }); - describe('log', () => { - it('should call console.log with the given arguments', () => { - const arg1 = 'Argument 1'; - const arg2 = 'Argument 2'; + 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) - 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 true when the directory already exists', () => { - (fs.existsSync as jest.Mock).mockReturnValue(true); - - 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 - }); + 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'); + expect(result).toEqual({}); }); - 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); - }); - - 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'] }); - }); - - 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'); - expect(result).toEqual({}); - }); - - it('should return an empty object if the option is not provided', () => { - const opts = {}; - const result = normalizeJsonOpt(opts, 'babel'); - expect(result).toEqual({}); - }); + it('should return an empty object if the option is not provided', () => { + const opts = {}; + const result = normalizeJsonOpt(opts, 'babel'); + expect(result).toEqual({}); + }); + }); + + describe('buildWebpackArgs', () => { + it('should return the options with normalized JSON options for babel and htmlWebpack', () => { + const opts = { + babel: '{"presets":["@babel/preset-env"]}', + htmlWebpack: '{"template":"./src/index.html"}', + otherOption: 'someValue', + }; + + const result = buildWebpackArgs(opts); + expect(result).toEqual({ + babel: { presets: ['@babel/preset-env'] }, + htmlWebpack: { template: './src/index.html' }, + otherOption: 'someValue', + }); }); - describe('buildWebpackArgs', () => { - it('should return the options with normalized JSON options for babel and htmlWebpack', () => { - const opts = { - babel: '{"presets":["@babel/preset-env"]}', - htmlWebpack: '{"template":"./src/index.html"}', - otherOption: 'someValue' - }; - - const result = buildWebpackArgs(opts); - expect(result).toEqual({ - 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', - }); - }); + 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: {}, + }); }); - 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')); - }); - - 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(); - }); + 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, + }); }); - describe('rootResolve', () => { - it('should resolve a relative path to an absolute path', () => { - const result = rootResolve('src/index.js'); + it('should handle missing babel and htmlWebpack keys gracefully', () => { + const opts = { otherOption: 'someValue' }; - 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 return the original require.resolve function', () => { - const originalRequireMock = jest.fn(); - global.__non_webpack_require__ = originalRequireMock; + 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 }); - 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', () => { - it('should resolve a module path using the original require.resolve', () => { - const originalRequireMock = { - resolve: jest.fn().mockReturnValue('resolved/path'), - }; - global.__non_webpack_require__ = originalRequireMock; + // 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); - const result = resolve('my-module'); + copyRecursiveSync('/src/file.txt', '/dest/file.txt'); - expect(result).toBe('resolved/path'); - expect(originalRequireMock.resolve).toHaveBeenCalledWith('my-module'); - }); + 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 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', () => { - 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' }); - - expect(result).toEqual({ - presets: [ - [resolve('@babel/preset-env'), { targets: 'node 14' }], - ], - plugins: [resolve('@babel/plugin-transform-runtime')], - }); - }); + expect(result).toEqual({ + presets: [[resolve('@babel/preset-env'), { targets: 'node 14' }]], + plugins: [resolve('@babel/plugin-transform-runtime')], + }); }); -}); \ No newline at end of file + }); +}); diff --git a/packages/cli/webpack.cli.ts b/packages/cli/webpack.cli.ts index 75f07d570..3ef6c994f 100644 --- a/packages/cli/webpack.cli.ts +++ b/packages/cli/webpack.cli.ts @@ -56,9 +56,7 @@ const config: Configuration = { }), ], externalsPresets: { node: true }, - externals: [ - NodeExternals(), - ], + externals: [NodeExternals()], }; export default config;