'use strict'; const path = require('path'); const _ = require('lodash'); const fs = require('fs-extra'); const webpack = require('webpack'); const WebpackDevServer = require('webpack-dev-server'); const chalk = require('chalk'); const chokidar = require('chokidar'); const getWebpackConfig = require('./webpack.config'); const getPkgPath = name => path.dirname(require.resolve(`${name}/package.json`)); const DEFAULT_PLUGINS = [ 'content-type-builder', 'content-manager', 'upload', 'email', 'i18n', 'users-permissions', ]; function getCustomWebpackConfig(dir, config) { const adminConfigPath = path.join(dir, 'src', 'admin', 'webpack.config.js'); let webpackConfig = getWebpackConfig(config); if (fs.existsSync(adminConfigPath)) { const webpackAdminConfig = require(path.resolve(adminConfigPath)); if (_.isFunction(webpackAdminConfig)) { // Expose the devServer configuration if (config.devServer) { webpackConfig.devServer = config.devServer; } webpackConfig = webpackAdminConfig(webpackConfig, webpack); if (!webpackConfig) { console.error( `${chalk.red('Error:')} Nothing was returned from your custom webpack configuration` ); process.exit(1); } } } return webpackConfig; } async function build({ plugins, dir, env, options, optimize, forceBuild }) { const buildAdmin = await shouldBuildAdmin({ dir, plugins }); if (!buildAdmin && !forceBuild) { return; } // Create the cache dir containing the front-end files. await createCacheDir({ dir, plugins }); const cacheDir = path.resolve(dir, '.cache'); const entry = path.resolve(cacheDir, 'admin', 'src'); const dest = path.resolve(dir, 'build'); // Roots for the @strapi/babel-plugin-switch-ee-ce const roots = { eeRoot: path.resolve(cacheDir, 'ee', 'admin'), ceRoot: path.resolve(cacheDir, 'admin', 'src'), }; const pluginsPath = Object.keys(plugins).map(pluginName => plugins[pluginName].pathToPlugin); const config = getCustomWebpackConfig(dir, { entry, pluginsPath, cacheDir, dest, env, options, optimize, roots, }); const compiler = webpack(config); return new Promise((resolve, reject) => { compiler.run((err, stats) => { if (err) { console.error(err.stack || err); if (err.details) { console.error(err.details); } return reject(err); } const info = stats.toJson(); if (stats.hasErrors()) { console.error(info.errors); } return resolve({ stats, warnings: info.warnings, }); }); }); } async function createPluginsJs(plugins, dest) { const pluginsArray = plugins.map(({ pathToPlugin, name }) => { const shortName = _.camelCase(name); /** * path.join, on windows, it uses backslashes to resolve path. * The problem is that Webpack does not windows paths * With this tool, we need to rely on "/" and not "\". * This is the reason why '..\\..\\..\\node_modules\\@strapi\\plugin-content-type-builder/strapi-admin.js' was not working. * The regexp at line 105 aims to replace the windows backslashes by standard slash so that webpack can deal with them. * Backslash looks to work only for absolute paths with webpack => https://webpack.js.org/concepts/module-resolution/#absolute-paths */ const realPath = path .join(path.relative(path.resolve(dest, 'admin', 'src'), pathToPlugin), 'strapi-admin.js') .replace(/\\/g, '/'); return { name, pathToPlugin: realPath, shortName, }; }); const content = ` ${pluginsArray .map(({ pathToPlugin, shortName }) => { const req = `'${pathToPlugin}'`; return `import ${shortName} from ${req};`; }) .join('\n')} const plugins = { ${[...pluginsArray] .map(({ name, shortName }) => { return ` '${name}': ${shortName},`; }) .join('\n')} }; export default plugins; `; return fs.writeFile(path.resolve(dest, 'admin', 'src', 'plugins.js'), content); } async function clean({ dir }) { const buildDir = path.join(dir, 'build'); const cacheDir = path.join(dir, '.cache'); fs.removeSync(buildDir); fs.removeSync(cacheDir); } async function copyAdmin(dest) { const adminPath = getPkgPath('@strapi/admin'); // TODO copy ee folders for plugins await fs.copy(path.resolve(adminPath, 'ee', 'admin'), path.resolve(dest, 'ee', 'admin')); await fs.ensureDir(path.resolve(dest, 'config')); await fs.copy(path.resolve(adminPath, 'admin'), path.resolve(dest, 'admin')); // Copy package.json await fs.copy(path.resolve(adminPath, 'package.json'), path.resolve(dest, 'package.json')); } async function createCacheDir({ dir, plugins }) { const cacheDir = path.resolve(dir, '.cache'); const pluginsWithFront = Object.keys(plugins) .filter(pluginName => { const pluginInfo = plugins[pluginName]; return fs.existsSync(path.resolve(pluginInfo.pathToPlugin, 'strapi-admin.js')); }) .map(name => ({ name, ...plugins[name] })); // create .cache dir await fs.emptyDir(cacheDir); // copy admin core code await copyAdmin(cacheDir); // Copy app.js const customAdminConfigFilePath = path.join(dir, 'src', 'admin', 'app.js'); if (fs.existsSync(customAdminConfigFilePath)) { await fs.copy(customAdminConfigFilePath, path.resolve(cacheDir, 'admin', 'src', 'app.js')); } // Copy admin extensions folder const adminExtensionFolder = path.join(dir, 'src', 'admin', 'extensions'); if (fs.existsSync(adminExtensionFolder)) { await fs.copy(adminExtensionFolder, path.resolve(cacheDir, 'admin', 'src', 'extensions')); } // create plugins.js with plugins requires await createPluginsJs(pluginsWithFront, cacheDir); } async function watchAdmin({ plugins, dir, host, port, browser, options }) { // Create the cache dir containing the front-end files. const cacheDir = path.join(dir, '.cache'); await createCacheDir({ dir, plugins }); const entry = path.join(cacheDir, 'admin', 'src'); const dest = path.join(dir, 'build'); const env = 'development'; // Roots for the @strapi/babel-plugin-switch-ee-ce const roots = { eeRoot: path.resolve(cacheDir, 'ee', 'admin'), ceRoot: path.resolve(cacheDir, 'admin', 'src'), }; const pluginsPath = Object.keys(plugins).map(pluginName => plugins[pluginName].pathToPlugin); const args = { entry, cacheDir, pluginsPath, dest, env, port, options, roots, devServer: { port, client: { logging: 'none', overlay: { errors: true, warnings: false, }, }, open: browser === 'true' ? true : browser, devMiddleware: { publicPath: options.adminPath, }, historyApiFallback: { index: options.adminPath, disableDotRule: true, }, }, }; const webpackConfig = getCustomWebpackConfig(dir, args); const compiler = webpack(webpackConfig); const server = new WebpackDevServer(compiler.options.devServer, compiler); const runServer = async () => { console.log(chalk.green('Starting the development server...')); console.log(); console.log(chalk.green(`Admin development at http://${host}:${port}${options.adminPath}`)); await server.start(); }; runServer(); watchFiles(dir); } /** * Listen to files change and copy the changed files in the .cache/admin folder * when using the dev mode * @param {string} dir */ async function watchFiles(dir) { const cacheDir = path.join(dir, '.cache'); const appExtensionFile = path.join(dir, 'src', 'admin', 'app.js'); const extensionsPath = path.join(dir, 'src', 'admin', 'extensions'); // Only watch the admin/app.js file and the files that are in the ./admin/extensions/folder const filesToWatch = [appExtensionFile, extensionsPath]; const watcher = chokidar.watch(filesToWatch, { ignoreInitial: true, ignorePermissionErrors: true, }); watcher.on('all', async (event, filePath) => { const isAppFile = filePath.includes(appExtensionFile); // The app.js file needs to be copied in the .cache/admin/src/app.js and the other ones needs to // be copied in the .cache/admin/src/extensions folder const targetPath = isAppFile ? path.join(path.normalize(filePath.split(appExtensionFile)[1]), 'app.js') : path.join('extensions', path.normalize(filePath.split(extensionsPath)[1])); const destFolder = path.join(cacheDir, 'admin', 'src'); if (event === 'unlink' || event === 'unlinkDir') { // Remove the file or folder // We need to copy the original files when deleting an override one try { fs.removeSync(path.join(destFolder, targetPath)); } catch (err) { console.log('An error occured while deleting the file', err); } } else { // In any other case just copy the file into the .cache/admin/src folder try { await fs.copy(filePath, path.join(destFolder, targetPath)); } catch (err) { console.log(err); } } }); } const hasCustomAdminCode = async dir => { const customAdminPath = path.join(dir, 'src', 'admin'); const customAdminConfigFile = path.join(customAdminPath, 'app.js'); const customAdminWebpackFile = path.join(customAdminPath, 'webpack.config.js'); const hasCustomConfigFile = await fs.pathExists(customAdminConfigFile); const hasCustomWebpackFile = await fs.pathExists(customAdminWebpackFile); return hasCustomConfigFile || hasCustomWebpackFile; }; /** * Checks if the project's installed plugins are not the same as a default one. * @param {Object} plugins * @returns {boolean} */ const hasNonDefaultPlugins = plugins => { // List of plugins that are not the ones installed in a generated app const installedPlugins = Object.keys(plugins).filter(x => !DEFAULT_PLUGINS.includes(x)); // List of default plugins uninstalled from a generated app const missingPlugins = DEFAULT_PLUGINS.filter(x => !Object.keys(plugins).includes(x)); const diff = [...installedPlugins, ...missingPlugins]; return diff.length > 0; }; async function shouldBuildAdmin({ dir, plugins }) { const appHasCustomAdminCode = await hasCustomAdminCode(dir); const appHasNonDefaultPlugins = hasNonDefaultPlugins(plugins); return appHasCustomAdminCode || appHasNonDefaultPlugins; } module.exports = { clean, build, watchAdmin, };