Merge pull request #4086 from strapi/front/admin-development

Enable easy admin development
This commit is contained in:
Alexandre BODIN 2019-09-26 14:27:52 +02:00 committed by GitHub
commit 893e514a85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 372 additions and 24 deletions

View File

@ -29,9 +29,101 @@ By default, the administration panel is exposed via [http://localhost:1337/admin
The panel will be available through [http://localhost:1337/dashboard](http://localhost:1337/dashboard) with the configurations above.
---
### Development mode
**_Currently not available_**
to enable the front-end development mode you need to start your application using the `--watch-admin` flag.
```bash
cd my-app
strapi develop --watch-admin
```
With this option you can
#### Customizing the `strapi-admin` package
All files added in `my-app/admin/src/` will either be replaced or added
**Example: Changing the available locales of your application**
```bash
# Create both the admin and admin/src folders
cd my-app && mkdir -p admin/src
# Change the available locales of the administration panel
touch admin/src/i18n.js
```
**Path: `my-app/admin/src/i18n.js**
```js
import { addLocaleData } from 'react-intl';
import { reduce } from 'lodash';
import en from 'react-intl/locale-data/en';
import fr from 'react-intl/locale-data/fr';
// We dismiss pt-BR and zh-Hans locales since they are not supported by react-intl
const locales = {
en,
fr,
};
const languages = Object.keys(trads);
/**
* Dynamically generate `translationsMessages object`.
*/
const translationMessages = reduce(
languages,
(result, language) => {
const obj = result;
obj[language] = trads[language];
if (locales[language]) {
addLocaleData(locales[language]);
}
return obj;
},
{}
);
export { languages, translationMessages };
```
> With this modification only English and French will be available in your admin
#### Customizing a plugin
Similarly to the back-end override system any file added in `my-app/extensions/<plugin-name>/admin/` will be copied and used instead of the original one (use with care).
**Example: Changing the current WYSIWYG**
```bash
cd my-app/extensions
# Create the content manager folder
mkdir content-manager && cd content-manager
# Create the admin folder
mkdir -p admin/src
# Create the components folder and the WysiwygWithErrors one
cd admin/src && mkdir -p components/WysiwygWithErrors
# Create the index.js so the original file is overridden
touch components/WysiwygWithErrors/index.js
```
**Path: `my-app/extensions/content-manager/admin/src/components/WysiwygWithErrors/index.js**
```js
import React from 'react';
import MyNewWYSIWYG from 'my-awesome-lib';
// This is a dummy example
const WysiwygWithErrors = props => <MyNewWYSIWYG {...props} />;
export default WysiwygWithErrors;
```
---
### Styles
@ -47,6 +139,8 @@ To apply your changes you need to rebuild your admin panel
npm run build
```
---
### Logo
To change the top-left displayed admin panel's logo, add your custom image at `./admin/src/assets/images/logo-strapi.png`.

View File

@ -37,6 +37,18 @@ Start a Strapi application with autoReload enabled.
Strapi modifies/creates files at runtime and needs to restart when new files are created. To achieve this, `strapi develop` adds a file watcher and restarts the application when necessary.
```
strapi develop
options: [--no-build |--watch-admin ]
```
- **strapi develop**<br/>
Starts your application with the autoReload enabled
- **strapi develop --no-build**<br/>
Starts your application with the autoReload enabled and skip the administration panel build process
- **strapi develop --watch-admin**<br/>
Starts your application with the autoReload enabled and the front-end development server. It allows you to customize the administration panel.
::: note
You should never use this command to run a Strapi application in production.
:::

View File

@ -1,7 +1,11 @@
/* eslint-disable no-useless-escape */
const path = require('path');
const fs = require('fs-extra');
const webpack = require('webpack');
const getWebpackConfig = require('./webpack.config.js');
const WebpackDevServer = require('webpack-dev-server');
const chalk = require('chalk');
const chokidar = require('chokidar');
const getPkgPath = name =>
path.dirname(require.resolve(`${name}/package.json`));
@ -135,11 +139,12 @@ async function createCacheDir(dir) {
}
async function build({ dir, env, options }) {
const cacheDir = path.resolve(dir, '.cache');
// 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 = getWebpackConfig({ entry, dest, env, options });
const compiler = webpack(config);
@ -176,8 +181,147 @@ async function build({ dir, env, options }) {
});
}
async function watchAdmin({ dir, port, 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',
hot: true,
quiet: true,
publicPath: options.publicPath,
historyApiFallback: {
index: options.publicPath,
},
};
const server = new WebpackDevServer(webpack(getWebpackConfig(args)), opts);
server.listen(port, 'localhost', 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://localhost:${port}/admin`)
);
});
watchFiles(dir);
}
async function watchFiles(dir) {
const cacheDir = path.join(dir, '.cache');
const pkgJSON = require(path.join(dir, 'package.json'));
const admin = path.join(dir, 'admin');
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(
dir,
'extensions',
plugin.replace(/^strapi-plugin-/i, ''),
'admin'
)
);
const filesToWatch = [admin, ...pluginsToWatch];
const watcher = chokidar.watch(filesToWatch, {
ignoreInitial: true,
ignorePermissionErrors: true,
});
watcher.on('all', async (event, filePath) => {
const re = /\/extensions\/([^\/]*)\/.*$/gm;
const matched = re.exec(filePath);
const isExtension = matched !== null;
const pluginName = isExtension ? matched[1] : '';
const packageName = isExtension
? `strapi-plugin-${pluginName}`
: 'strapi-admin';
const targetPath = isExtension
? filePath.split('/extensions/')[1].replace(pluginName, '')
: 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 = {
build,
createPluginsJs,
createCacheDir,
watchAdmin,
};

View File

@ -102,6 +102,7 @@
"license": "MIT",
"gitHead": "c85658a19b8fef0f3164c19693a45db305dc07a9",
"devDependencies": {
"chokidar": "^3.1.1",
"webpack": "^4.40.1",
"webpack-cli": "^3.3.2",
"webpack-dev-server": "^3.4.1"

View File

@ -37,12 +37,15 @@ module.exports = ({
filename: '[name].[chunkhash].css',
chunkFilename: '[name].[chunkhash].chunkhash.css',
}),
new WebpackBar(),
]
: [
new DuplicatePckgChecker({
verbose: true,
}),
new FriendlyErrorsWebpackPlugin(),
new FriendlyErrorsWebpackPlugin({
clearConsole: false,
}),
];
const scssLoader = isProduction
@ -237,7 +240,6 @@ module.exports = ({
mainFields: ['browser', 'jsnext:main', 'main'],
},
plugins: [
new WebpackBar(),
new HtmlWebpackPlugin({
inject: true,
template: path.resolve(__dirname, 'index.html'),

View File

@ -112,6 +112,7 @@ program
.command('develop')
.alias('dev')
.option('--no-build', 'Disable build', false)
.option('--watch-admin', 'Enable watch', true)
.description('Start your Strapi application in development mode')
.action(getLocalScript('develop'));
@ -190,6 +191,12 @@ program
.option('-d, --delete-files', 'Delete files', false)
.action(getLocalScript('uninstall'));
// `$ strapi watch-admin`
program
.command('watch-admin')
.description('Starts the admin dev server')
.action(getLocalScript('watchAdmin'));
/**
* Normalize help argument
*/

View File

@ -6,7 +6,7 @@ const _ = require('lodash');
const { green, yellow } = require('chalk');
const strapiAdmin = require('strapi-admin');
const loadConfigFile = require('../load/load-config-files');
const addSlash = require('../utils/addSlash');
/**
* `$ strapi build`
*/
@ -34,9 +34,6 @@ module.exports = async () => {
console.log(`Building your admin UI with ${green(env)} configuration ...`);
// Create the .cache folder containing the front-end files
await strapiAdmin.createCacheDir(dir);
return strapiAdmin
.build({
dir,
@ -55,12 +52,3 @@ module.exports = async () => {
process.exit(1);
});
};
function addSlash(path) {
if (typeof path !== 'string') throw new Error('admin.path must be a string');
if (path === '' || path === '/') return '/';
if (path[0] != '/') path = '/' + path;
if (path[path.length - 1] != '/') path = path + '/';
return path;
}

View File

@ -7,16 +7,18 @@ const chokidar = require('chokidar');
const execa = require('execa');
const { logger } = require('strapi-utils');
const strapi = require('../index');
/**
* `$ strapi develop`
*
*/
module.exports = async function({ build }) {
module.exports = async function({ build, watchAdmin }) {
const dir = process.cwd();
if (build && !fs.existsSync(path.join(dir, 'build'))) {
// Don't run the build process if the admin is in watch mode
if (build && !watchAdmin && !fs.existsSync(path.join(dir, 'build'))) {
try {
execa.shellSync('npm run -s build', {
stdio: 'inherit',
@ -30,6 +32,17 @@ module.exports = async function({ build }) {
const strapiInstance = strapi({ dir, autoReload: true });
if (cluster.isMaster) {
// Start the front-end dev server
if (watchAdmin) {
try {
execa('npm', ['run', '-s', 'strapi', 'watch-admin'], {
stdio: 'inherit',
});
} catch (err) {
process.exit(1);
}
}
cluster.on('message', (worker, message) => {
switch (message) {
case 'reload':

View File

@ -0,0 +1,32 @@
/* eslint-disable no-useless-escape */
const path = require('path');
const strapiAdmin = require('strapi-admin');
const _ = require('lodash');
const loadConfigFile = require('../load/load-config-files');
const addSlash = require('../utils/addSlash');
module.exports = async function() {
const dir = process.cwd();
const envConfigDir = path.join(dir, 'config', 'environments', 'development');
const serverConfig = await loadConfigFile(envConfigDir, 'server.+(js|json)');
const port = _.get(serverConfig, 'port', 1337);
const host = _.get(serverConfig, 'host', 'localhost');
const adminPort = _.get(serverConfig, 'admin.port', 8000);
const adminBackend = _.get(
serverConfig,
'admin.build.backend',
`http://${host}:${port}`
);
const adminPath = _.get(serverConfig, 'admin.path', '/admin');
strapiAdmin.watchAdmin({
dir,
port: adminPort,
options: {
backend: adminBackend,
publicPath: addSlash(adminPath),
},
});
};

View File

@ -0,0 +1,8 @@
module.exports = path => {
if (typeof path !== 'string') throw new Error('admin.path must be a string');
if (path === '' || path === '/') return '/';
if (path[0] != '/') path = '/' + path;
if (path[path.length - 1] != '/') path = path + '/';
return path;
};

View File

@ -3095,6 +3095,14 @@ anymatch@^2.0.0:
micromatch "^3.1.4"
normalize-path "^2.1.1"
anymatch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.0.tgz#e609350e50a9313b472789b2f14ef35808ee14d6"
integrity sha512-Ozz7l4ixzI7Oxj2+cw+p0tVUt27BpaJ+1+q1TCeANWxHpvyn2+Un+YamBdfKu0uh8xLodGhoa1v7595NhKDAuA==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
apollo-cache-control@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.4.tgz#a3650d5e4173953e2a3af995bea62147f1ffe4d7"
@ -3851,6 +3859,11 @@ binary-extensions@^1.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
binary-extensions@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
bindings@^1.3.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
@ -4018,7 +4031,7 @@ braces@^2.3.1, braces@^2.3.2:
split-string "^3.0.2"
to-regex "^3.0.1"
braces@^3.0.1:
braces@^3.0.1, braces@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@ -4541,6 +4554,21 @@ chokidar@^2.0.2, chokidar@^2.1.2, chokidar@^2.1.6:
optionalDependencies:
fsevents "^1.2.7"
chokidar@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.1.1.tgz#27e953f3950336efcc455fd03e240c7299062003"
integrity sha512-df4o16uZmMHzVQwECZRHwfguOt5ixpuQVaZHjYMvYisgKhE+JXwcj/Tcr3+3bu/XeOJQ9ycYmzu7Mv8XrGxJDQ==
dependencies:
anymatch "^3.1.0"
braces "^3.0.2"
glob-parent "^5.0.0"
is-binary-path "^2.1.0"
is-glob "^4.0.1"
normalize-path "^3.0.0"
readdirp "^3.1.1"
optionalDependencies:
fsevents "^2.0.6"
chownr@^1.1.1, chownr@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6"
@ -7721,6 +7749,11 @@ fsevents@^1.2.7:
nan "^2.12.1"
node-pre-gyp "^0.12.0"
fsevents@^2.0.6:
version "2.0.7"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.0.7.tgz#382c9b443c6cbac4c57187cdda23aa3bf1ccfc2a"
integrity sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ==
fstream@^1.0.0, fstream@^1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
@ -9379,6 +9412,13 @@ is-binary-path@^1.0.0:
dependencies:
binary-extensions "^1.0.0"
is-binary-path@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-bluebird@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-bluebird/-/is-bluebird-1.0.2.tgz#096439060f4aa411abee19143a84d6a55346d6e2"
@ -13560,7 +13600,7 @@ pgpass@1.x:
dependencies:
split "^1.0.0"
picomatch@^2.0.5:
picomatch@^2.0.4, picomatch@^2.0.5:
version "2.0.7"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6"
integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==
@ -15510,6 +15550,13 @@ readdirp@^2.2.1:
micromatch "^3.1.10"
readable-stream "^2.0.2"
readdirp@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.1.2.tgz#fa85d2d14d4289920e4671dead96431add2ee78a"
integrity sha512-8rhl0xs2cxfVsqzreYCvs8EwBfn/DhVdqtoLmw19uI3SC5avYX9teCurlErfpPXGmYtMHReGaP2RsLnFvz/lnw==
dependencies:
picomatch "^2.0.4"
realpath-native@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c"