mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-30 17:37:26 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			374 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			374 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| /* eslint-disable no-useless-escape */
 | |
| 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');
 | |
| // eslint-disable-next-line node/no-extraneous-require
 | |
| const hasEE = require('strapi/lib/utils/ee');
 | |
| const getWebpackConfig = require('./webpack.config.js');
 | |
| 
 | |
| const getPkgPath = name => path.dirname(require.resolve(`${name}/package.json`));
 | |
| 
 | |
| function getCustomWebpackConfig(dir, config) {
 | |
|   const adminConfigPath = path.join(dir, 'admin', 'admin.config.js');
 | |
| 
 | |
|   let webpackConfig = getWebpackConfig({ useEE: hasEE({ dir }), ...config });
 | |
| 
 | |
|   if (fs.existsSync(adminConfigPath)) {
 | |
|     const adminConfig = require(path.resolve(adminConfigPath));
 | |
| 
 | |
|     if (_.isFunction(adminConfig.webpack)) {
 | |
|       webpackConfig = adminConfig.webpack(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({ dir, env, options, optimize }) {
 | |
|   // Create the cache dir containing the front-end files.
 | |
|   await createCacheDir(dir);
 | |
| 
 | |
|   const cacheDir = path.resolve(dir, '.cache');
 | |
|   const entry = path.resolve(cacheDir, 'admin', 'src', 'app.js');
 | |
|   const dest = path.resolve(dir, 'build');
 | |
|   const config = getCustomWebpackConfig(dir, { entry, dest, env, options, optimize });
 | |
| 
 | |
|   const compiler = webpack(config);
 | |
| 
 | |
|   return new Promise((resolve, reject) => {
 | |
|     compiler.run((err, stats) => {
 | |
|       let messages;
 | |
|       if (err) {
 | |
|         if (!err.message) {
 | |
|           return reject(err);
 | |
|         }
 | |
|         messages = {
 | |
|           errors: [err.message],
 | |
|           warnings: [],
 | |
|         };
 | |
|       } else {
 | |
|         messages = stats.toJson({ all: false, warnings: true, errors: true });
 | |
|       }
 | |
| 
 | |
|       if (messages.errors.length) {
 | |
|         // Only keep the first error. Others are often indicative
 | |
|         // of the same problem, but confuse the reader with noise.
 | |
|         if (messages.errors.length > 1) {
 | |
|           messages.errors.length = 1;
 | |
|         }
 | |
|         return reject(new Error(messages.errors.join('\n\n')));
 | |
|       }
 | |
| 
 | |
|       return resolve({
 | |
|         stats,
 | |
|         warnings: messages.warnings,
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| }
 | |
| 
 | |
| async function createPluginsJs(plugins, localPlugins, dest) {
 | |
|   const content = `
 | |
| const injectReducer = require('./utils/injectReducer').default;
 | |
| const injectSaga = require('./utils/injectSaga').default;
 | |
| const useInjectReducer = require('./utils/injectReducer').useInjectReducer;
 | |
| const useInjectSaga = require('./utils/injectSaga').useInjectSaga;
 | |
| const { languages } = require('./i18n');
 | |
| 
 | |
| window.strapi = Object.assign(window.strapi || {}, {
 | |
|   node: MODE || 'host',
 | |
|   backendURL: BACKEND_URL === '/' ? window.location.origin : BACKEND_URL,
 | |
|   languages,
 | |
|   currentLanguage:
 | |
|   window.localStorage.getItem('strapi-admin-language') ||
 | |
|   window.navigator.language ||
 | |
|   window.navigator.userLanguage ||
 | |
|   'en',
 | |
|   injectReducer,
 | |
|   injectSaga,
 | |
|   useInjectReducer,
 | |
|   useInjectSaga,
 | |
| });
 | |
| 
 | |
| module.exports = {
 | |
|   ${plugins
 | |
|     .map(name => {
 | |
|       const shortName = name.replace(/^strapi-plugin-/i, '');
 | |
|       const req = `require('../../plugins/${name}/admin/src').default`;
 | |
|       return `'${shortName}': ${req},`;
 | |
|     })
 | |
|     .join('\n')}
 | |
|   ${localPlugins
 | |
|     .map(name => {
 | |
|       const shortName = name.replace(/^strapi-plugin-/i, '');
 | |
|       const req = `require('../../../plugins/${name}/admin/src').default`;
 | |
|       return `'${shortName}': ${req}`;
 | |
|     })
 | |
|     .join(',\n')}
 | |
| }
 | |
|   `;
 | |
| 
 | |
|   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 copyPlugin(name, dest) {
 | |
|   const pkgFilePath = getPkgPath(name);
 | |
| 
 | |
|   const resolveDepPath = (...args) => path.resolve(pkgFilePath, ...args);
 | |
|   const resolveDest = (...args) => path.resolve(dest, 'plugins', name, ...args);
 | |
| 
 | |
|   const copy = (...args) => {
 | |
|     return fs.copy(resolveDepPath(...args), resolveDest(...args));
 | |
|   };
 | |
| 
 | |
|   // Copy the entire admin folder
 | |
|   await copy('admin');
 | |
| 
 | |
|   // Copy the layout.js if it exists
 | |
|   if (await fs.exists(path.resolve(pkgFilePath, 'config', 'layout.js'))) {
 | |
|     await fs.ensureDir(resolveDest('config'));
 | |
|     await copy('config', 'layout.js');
 | |
|   }
 | |
| 
 | |
|   await copy('package.json');
 | |
| }
 | |
| 
 | |
| 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'));
 | |
|   await fs.copy(
 | |
|     path.resolve(adminPath, 'config', 'layout.js'),
 | |
|     path.resolve(dest, 'config', 'layout.js')
 | |
|   );
 | |
| 
 | |
|   // Copy package.json
 | |
|   await fs.copy(path.resolve(adminPath, 'package.json'), path.resolve(dest, 'package.json'));
 | |
| }
 | |
| 
 | |
| async function copyCustomAdmin(src, dest) {
 | |
|   await fs.copy(src, path.resolve(dest, 'admin'));
 | |
| }
 | |
| 
 | |
| async function createCacheDir(dir) {
 | |
|   const cacheDir = path.resolve(dir, '.cache');
 | |
| 
 | |
|   const pkgJSON = require(path.join(dir, 'package.json'));
 | |
| 
 | |
|   const pluginsToCopy = Object.keys(pkgJSON.dependencies).filter(
 | |
|     dep =>
 | |
|       dep.startsWith('strapi-plugin') &&
 | |
|       fs.existsSync(path.resolve(getPkgPath(dep), 'admin', 'src', 'index.js'))
 | |
|   );
 | |
| 
 | |
|   let localPluginsToCopy = [];
 | |
|   if (fs.existsSync(path.join(dir, 'plugins'))) {
 | |
|     localPluginsToCopy = fs
 | |
|       .readdirSync(path.join(dir, 'plugins'))
 | |
|       .filter(plugin =>
 | |
|         fs.existsSync(path.resolve(dir, 'plugins', plugin, 'admin', 'src', 'index.js'))
 | |
|       );
 | |
|   }
 | |
| 
 | |
|   // TODO: add logic to avoid copying files if not necessary
 | |
| 
 | |
|   // create .cache dir
 | |
|   await fs.emptyDir(cacheDir);
 | |
| 
 | |
|   // copy admin core code
 | |
|   await copyAdmin(cacheDir);
 | |
| 
 | |
|   // copy plugins code
 | |
|   await Promise.all(pluginsToCopy.map(name => copyPlugin(name, cacheDir)));
 | |
| 
 | |
|   // override admin code with user customizations
 | |
|   if (fs.pathExistsSync(path.join(dir, 'admin'))) {
 | |
|     await copyCustomAdmin(path.join(dir, 'admin'), cacheDir);
 | |
|   }
 | |
| 
 | |
|   // create plugins.js with plugins requires
 | |
|   await createPluginsJs(pluginsToCopy, localPluginsToCopy, cacheDir);
 | |
| 
 | |
|   // override plugins' admin code with user customizations
 | |
|   const pluginsToOverride = pluginsToCopy.reduce((acc, current) => {
 | |
|     const pluginName = current.replace(/^strapi-plugin-/i, '');
 | |
| 
 | |
|     if (fs.pathExistsSync(path.join(dir, 'extensions', pluginName, 'admin'))) {
 | |
|       acc.push(pluginName);
 | |
|     }
 | |
| 
 | |
|     return acc;
 | |
|   }, []);
 | |
| 
 | |
|   await Promise.all(
 | |
|     pluginsToOverride.map(plugin =>
 | |
|       copyCustomAdmin(
 | |
|         path.join(dir, 'extensions', plugin, 'admin'),
 | |
|         path.join(cacheDir, 'plugins', `strapi-plugin-${plugin}`)
 | |
|       )
 | |
|     )
 | |
|   );
 | |
| }
 | |
| 
 | |
| async function watchAdmin({ dir, host, port, browser, options }) {
 | |
|   // Create the cache dir containing the front-end files.
 | |
|   await createCacheDir(dir);
 | |
| 
 | |
|   const entry = path.join(dir, '.cache', 'admin', 'src', 'app.js');
 | |
|   const dest = path.join(dir, 'build');
 | |
|   const env = 'development';
 | |
| 
 | |
|   const args = {
 | |
|     entry,
 | |
|     dest,
 | |
|     env,
 | |
|     port,
 | |
|     options,
 | |
|   };
 | |
| 
 | |
|   const opts = {
 | |
|     clientLogLevel: 'silent',
 | |
|     quiet: true,
 | |
|     open: browser === 'true' ? true : browser,
 | |
|     publicPath: options.publicPath,
 | |
|     historyApiFallback: {
 | |
|       index: options.publicPath,
 | |
|       disableDotRule: true,
 | |
|     },
 | |
|   };
 | |
| 
 | |
|   const webpackConfig = getCustomWebpackConfig(dir, args);
 | |
|   const server = new WebpackDevServer(webpack(webpackConfig), opts);
 | |
| 
 | |
|   server.listen(port, host, function(err) {
 | |
|     if (err) {
 | |
|       console.log(err);
 | |
|     }
 | |
| 
 | |
|     console.log(chalk.green('Starting the development server...'));
 | |
|     console.log();
 | |
|     console.log(chalk.green(`Admin development at http://${host}:${port}${opts.publicPath}`));
 | |
|   });
 | |
| 
 | |
|   watchFiles(dir, options.watchIgnoreFiles);
 | |
| }
 | |
| 
 | |
| async function watchFiles(dir, ignoreFiles = []) {
 | |
|   const cacheDir = path.join(dir, '.cache');
 | |
|   const pkgJSON = require(path.join(dir, 'package.json'));
 | |
|   const admin = path.join(dir, 'admin');
 | |
|   const extensionsPath = path.join(dir, 'extensions');
 | |
| 
 | |
|   const appPlugins = Object.keys(pkgJSON.dependencies).filter(
 | |
|     dep =>
 | |
|       dep.startsWith('strapi-plugin') &&
 | |
|       fs.existsSync(path.resolve(getPkgPath(dep), 'admin', 'src', 'index.js'))
 | |
|   );
 | |
|   const pluginsToWatch = appPlugins.map(plugin =>
 | |
|     path.join(extensionsPath, plugin.replace(/^strapi-plugin-/i, ''), 'admin')
 | |
|   );
 | |
|   const filesToWatch = [admin, ...pluginsToWatch];
 | |
| 
 | |
|   const watcher = chokidar.watch(filesToWatch, {
 | |
|     ignoreInitial: true,
 | |
|     ignorePermissionErrors: true,
 | |
|     ignored: [...ignoreFiles],
 | |
|   });
 | |
| 
 | |
|   watcher.on('all', async (event, filePath) => {
 | |
|     const isExtension = filePath.includes(extensionsPath);
 | |
|     const pluginName = isExtension ? filePath.replace(extensionsPath, '').split(path.sep)[1] : '';
 | |
| 
 | |
|     const packageName = isExtension ? `strapi-plugin-${pluginName}` : 'strapi-admin';
 | |
| 
 | |
|     const targetPath = isExtension
 | |
|       ? path.normalize(filePath.split(extensionsPath)[1].replace(pluginName, ''))
 | |
|       : path.normalize(filePath.split(admin)[1]);
 | |
| 
 | |
|     const destFolder = isExtension
 | |
|       ? path.join(cacheDir, 'plugins', packageName)
 | |
|       : path.join(cacheDir, 'admin');
 | |
| 
 | |
|     if (event === 'unlink' || event === 'unlinkDir') {
 | |
|       const originalFilePathInNodeModules = path.join(
 | |
|         getPkgPath(packageName),
 | |
|         isExtension ? '' : 'admin',
 | |
|         targetPath
 | |
|       );
 | |
| 
 | |
|       // 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);
 | |
|       }
 | |
| 
 | |
|       // Check if the file or folder exists in node_modules
 | |
|       // If so copy the old one
 | |
|       if (fs.pathExistsSync(path.resolve(originalFilePathInNodeModules))) {
 | |
|         try {
 | |
|           await fs.copy(
 | |
|             path.resolve(originalFilePathInNodeModules),
 | |
|             path.join(destFolder, targetPath)
 | |
|           );
 | |
| 
 | |
|           // The plugins.js file needs to be recreated
 | |
|           // when we delete either the admin folder
 | |
|           // the admin/src folder
 | |
|           // or the plugins.js file
 | |
|           // since the path are different when developing inside the monorepository or inside an app
 | |
|           const shouldCopyPluginsJSFile =
 | |
|             filePath.split('/admin/src').filter(p => !!p).length === 1;
 | |
| 
 | |
|           if (
 | |
|             (event === 'unlinkDir' && !isExtension && shouldCopyPluginsJSFile) ||
 | |
|             (!isExtension && filePath.includes('plugins.js'))
 | |
|           ) {
 | |
|             await createPluginsJs(appPlugins, path.join(cacheDir));
 | |
|           }
 | |
|         } catch (err) {
 | |
|           // Do nothing
 | |
|         }
 | |
|       }
 | |
|     } else {
 | |
|       // In any other case just copy the file into the .cache folder
 | |
|       try {
 | |
|         await fs.copy(filePath, path.join(destFolder, targetPath));
 | |
|       } catch (err) {
 | |
|         console.log(err);
 | |
|       }
 | |
|     }
 | |
|   });
 | |
| }
 | |
| 
 | |
| module.exports = {
 | |
|   clean,
 | |
|   build,
 | |
|   watchAdmin,
 | |
| };
 | 
