@norejs/ssr-builder 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/norejs-ssr.js +13 -0
- package/package.json +21 -0
- package/plugins/react-scripts-ssr/config/webpack.ssr.config.js +75 -0
- package/plugins/react-scripts-ssr/index.js +64 -0
- package/scripts/start.js +11 -0
- package/utils/project.js +128 -0
- package/utils/runWebpack.js +140 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const program = require('commander');
|
|
4
|
+
|
|
5
|
+
program.command('start').action((options, command) => {
|
|
6
|
+
require('../scripts/start')(options, 'development');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
program.command('build').action((options, command) => {
|
|
10
|
+
require('../scripts/start')(options, 'production');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@norejs/ssr-builder",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "SSR builder for NoreJS",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"norejs-ssr-builder": "./bin/norejs-ssr.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"author": "",
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@norejs/ssr-server": "^0.0.1",
|
|
16
|
+
"commander": "10",
|
|
17
|
+
"fs-extra": "^11.1.1",
|
|
18
|
+
"webpack-merge": "^5.9.0",
|
|
19
|
+
"webpack-node-externals": "^3.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const nodeExternals = require('webpack-node-externals');
|
|
3
|
+
|
|
4
|
+
const { merge } = require('webpack-merge');
|
|
5
|
+
const { requireNpmFromCwd } = require('../../../utils/project');
|
|
6
|
+
// 去掉HTMLWebpackPlugin
|
|
7
|
+
const ssrDisablePlugins = [
|
|
8
|
+
'HtmlWebpackPlugin',
|
|
9
|
+
'InlineChunkHtmlPlugin',
|
|
10
|
+
'HotModuleReplacementPlugin',
|
|
11
|
+
'ManifestPlugin',
|
|
12
|
+
'ReactRefreshWebpackPlugin',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const MiniCssExtractPlugin = requireNpmFromCwd('mini-css-extract-plugin');
|
|
16
|
+
|
|
17
|
+
module.exports = function (baseConfig, ssrConfig, webpackEnv = 'development') {
|
|
18
|
+
// 去掉多余的插件
|
|
19
|
+
baseConfig.plugins = baseConfig.plugins.filter((plugin) => {
|
|
20
|
+
return !ssrDisablePlugins.includes(plugin.constructor.name);
|
|
21
|
+
});
|
|
22
|
+
if (
|
|
23
|
+
baseConfig.plugins.find(
|
|
24
|
+
(plugin) => plugin.constructor.name === 'MiniCssExtractPlugin'
|
|
25
|
+
)
|
|
26
|
+
) {
|
|
27
|
+
baseConfig.plugins.push(
|
|
28
|
+
new MiniCssExtractPlugin({
|
|
29
|
+
// Options similar to the same options in webpackOptions.output
|
|
30
|
+
// both options are optional
|
|
31
|
+
filename: 'static/css/[name].[contenthash:8].css',
|
|
32
|
+
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const projectRoot = process.cwd();
|
|
38
|
+
// 替换style-loader
|
|
39
|
+
const rules = baseConfig.module.rules[1].oneOf;
|
|
40
|
+
rules.forEach((rule) => {
|
|
41
|
+
if (rule.test?.toString().includes('css')) {
|
|
42
|
+
rule.use.forEach((use, index) => {
|
|
43
|
+
if (typeof use === 'string' && use.includes('style-loader')) {
|
|
44
|
+
rule.use[index] = {
|
|
45
|
+
loader: MiniCssExtractPlugin.loader,
|
|
46
|
+
options: {},
|
|
47
|
+
};
|
|
48
|
+
} else if (use.loader?.includes('style-loader')) {
|
|
49
|
+
rule.use[index] = {
|
|
50
|
+
loader: MiniCssExtractPlugin.loader,
|
|
51
|
+
options: {},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return rule;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// 读取env文件,或者自定义
|
|
60
|
+
return merge(baseConfig, {
|
|
61
|
+
entry: { main: '/' + ssrConfig.entry },
|
|
62
|
+
output: {
|
|
63
|
+
filename: 'server.js',
|
|
64
|
+
path: path.resolve(projectRoot, ssrConfig.dist),
|
|
65
|
+
libraryTarget: 'commonjs',
|
|
66
|
+
},
|
|
67
|
+
optimization: {
|
|
68
|
+
minimize: false,
|
|
69
|
+
splitChunks: false,
|
|
70
|
+
runtimeChunk: false,
|
|
71
|
+
},
|
|
72
|
+
target: 'node',
|
|
73
|
+
externals: [nodeExternals()],
|
|
74
|
+
});
|
|
75
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const {
|
|
3
|
+
getNpmPathFromCwd,
|
|
4
|
+
getFilePathFromCwd,
|
|
5
|
+
} = require('../../utils/project');
|
|
6
|
+
|
|
7
|
+
const runWebpack = require('../../utils/runWebpack');
|
|
8
|
+
|
|
9
|
+
module.exports = function start(
|
|
10
|
+
options,
|
|
11
|
+
ssrConfig,
|
|
12
|
+
webpackEnv = 'development'
|
|
13
|
+
) {
|
|
14
|
+
process.env.NODE_ENV = webpackEnv;
|
|
15
|
+
process.env.BABEL_ENV = webpackEnv;
|
|
16
|
+
// 获取react-scripts 的配置
|
|
17
|
+
const projectWebpackConfigPath = getWebpackFile('webpack.config.js');
|
|
18
|
+
const baseWebpackConfig = require(projectWebpackConfigPath)(webpackEnv);
|
|
19
|
+
const ssrWebpackConfigFactory = getSSRWebpackConfig();
|
|
20
|
+
if (typeof ssrWebpackConfigFactory !== 'function') {
|
|
21
|
+
throw new Error('webpack.ssr.config.js should export a function');
|
|
22
|
+
}
|
|
23
|
+
const webpackConfig = ssrWebpackConfigFactory(
|
|
24
|
+
baseWebpackConfig,
|
|
25
|
+
ssrConfig,
|
|
26
|
+
webpackEnv
|
|
27
|
+
);
|
|
28
|
+
return runWebpack(webpackConfig, webpackEnv);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 获取ssr的webpack配置
|
|
32
|
+
* @returns
|
|
33
|
+
*/
|
|
34
|
+
function getSSRWebpackConfig() {
|
|
35
|
+
const webpackConfigPath = getFilePathFromCwd(
|
|
36
|
+
'config/webpack.ssr.config.js'
|
|
37
|
+
);
|
|
38
|
+
if (fs.existsSync(webpackConfigPath)) {
|
|
39
|
+
return require(webpackConfigPath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return require('./config/webpack.ssr.config');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 获取项目中的webpack配置
|
|
47
|
+
* */
|
|
48
|
+
function getWebpackFile(webpackFileName) {
|
|
49
|
+
const projectWebpackConfigPath = getFilePathFromCwd(
|
|
50
|
+
'config/' + webpackFileName
|
|
51
|
+
);
|
|
52
|
+
if (fs.existsSync(projectWebpackConfigPath)) {
|
|
53
|
+
return projectWebpackConfigPath;
|
|
54
|
+
}
|
|
55
|
+
const webpackConfigPath = getNpmPathFromCwd(
|
|
56
|
+
'react-scripts/config/' + webpackFileName
|
|
57
|
+
);
|
|
58
|
+
if (!fs.existsSync(webpackConfigPath)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'react-scripts is not installed in your project please make sure you have installed it'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return webpackConfigPath;
|
|
64
|
+
}
|
package/scripts/start.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// 读取项目中的配置,监测当前项目是否react-scripts 项目
|
|
2
|
+
const { getProjectConfig } = require('../utils/project');
|
|
3
|
+
|
|
4
|
+
module.exports = function start(options, webpackEnv = 'development') {
|
|
5
|
+
const config = getProjectConfig();
|
|
6
|
+
if (config.ssr) {
|
|
7
|
+
const builderName = config.ssr.builder || 'react-scripts-ssr';
|
|
8
|
+
const builder = require(`../plugins/${builderName}`);
|
|
9
|
+
return builder(options, config.ssr, webpackEnv);
|
|
10
|
+
}
|
|
11
|
+
};
|
package/utils/project.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const defaultConfig = {
|
|
4
|
+
ssr: {
|
|
5
|
+
entry: 'src/server.js',
|
|
6
|
+
dist: 'build-ssr',
|
|
7
|
+
builder: 'react-scripts-ssr',
|
|
8
|
+
},
|
|
9
|
+
csr: {
|
|
10
|
+
dist: 'build',
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* 获取项目中的npm包路径
|
|
15
|
+
* @param {*} npmPkgName
|
|
16
|
+
* @param {*} filePath
|
|
17
|
+
* @returns
|
|
18
|
+
*/
|
|
19
|
+
function getNpmPathFromCwd(npmPkgName) {
|
|
20
|
+
const projectRoot = process.cwd();
|
|
21
|
+
const nodeModules = path.resolve(projectRoot, 'node_modules');
|
|
22
|
+
const npmPkgPath = path.resolve(nodeModules, npmPkgName);
|
|
23
|
+
return npmPkgPath;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
*
|
|
28
|
+
* @param {*} filePath
|
|
29
|
+
* @returns
|
|
30
|
+
*/
|
|
31
|
+
function getFilePathFromCwd(filePath) {
|
|
32
|
+
const projectRoot = process.cwd();
|
|
33
|
+
return path.resolve(projectRoot, filePath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function isReactScriptsInstalled() {
|
|
37
|
+
const reactScriptsPath = getNpmPathFromCwd('react-scripts');
|
|
38
|
+
return await fs.exists(reactScriptsPath);
|
|
39
|
+
}
|
|
40
|
+
function getProjectConfig() {
|
|
41
|
+
const projectConfigPath = getFilePathFromCwd('nore.config.js');
|
|
42
|
+
let customConfig = {};
|
|
43
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
44
|
+
customConfig = require(projectConfigPath) || {};
|
|
45
|
+
}
|
|
46
|
+
// 深度合并
|
|
47
|
+
// 深度合并
|
|
48
|
+
|
|
49
|
+
return deepAssign({}, defaultConfig, customConfig);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isPlainObject(obj) {
|
|
53
|
+
return (
|
|
54
|
+
typeof obj === 'object' &&
|
|
55
|
+
Object.prototype.toString.call(obj) === '[object Object]'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
function deepAssign() {
|
|
59
|
+
let len = arguments.length,
|
|
60
|
+
target = arguments[0];
|
|
61
|
+
if (!isPlainObject(target)) {
|
|
62
|
+
target = {};
|
|
63
|
+
}
|
|
64
|
+
for (let i = 1; i < len; i++) {
|
|
65
|
+
let source = arguments[i];
|
|
66
|
+
if (isPlainObject(source)) {
|
|
67
|
+
for (let s in source) {
|
|
68
|
+
if (s === '__proto__' || target === source[s]) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (isPlainObject(source[s])) {
|
|
72
|
+
target[s] = deepAssign(target[s], source[s]);
|
|
73
|
+
} else {
|
|
74
|
+
target[s] = source[s];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return target;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 当前项目是否运行过eject
|
|
84
|
+
* @param {*} projectRoot
|
|
85
|
+
* @returns
|
|
86
|
+
*/
|
|
87
|
+
async function isEjected(projectRoot) {
|
|
88
|
+
const configPath = path.resolve(projectRoot, 'config');
|
|
89
|
+
const scriptsPath = path.resolve(projectRoot, 'scripts');
|
|
90
|
+
return (await fs.exists(configPath)) && (await fs.exists(scriptsPath));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 从项目中加载npm包
|
|
94
|
+
* @param {*} npmPkgName
|
|
95
|
+
* @returns
|
|
96
|
+
*/
|
|
97
|
+
function requireNpmFromCwd(npmPkgName) {
|
|
98
|
+
const nodeModules = getNpmPathFromCwd(npmPkgName);
|
|
99
|
+
try {
|
|
100
|
+
return require(nodeModules);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 从项目中加载某个文件
|
|
108
|
+
* @param {*} filename
|
|
109
|
+
* @returns
|
|
110
|
+
*/
|
|
111
|
+
function requireFromCwd(filename) {
|
|
112
|
+
const nodeModules = getFilePathFromCwd(filename);
|
|
113
|
+
try {
|
|
114
|
+
return require(nodeModules);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
getNpmPathFromCwd,
|
|
122
|
+
isEjected,
|
|
123
|
+
requireNpmFromCwd,
|
|
124
|
+
getFilePathFromCwd,
|
|
125
|
+
requireFromCwd,
|
|
126
|
+
getProjectConfig,
|
|
127
|
+
isReactScriptsInstalled,
|
|
128
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const { requireNpmFromCwd } = require('./project');
|
|
2
|
+
const formatWebpackMessages = requireNpmFromCwd(
|
|
3
|
+
'react-dev-utils/formatWebpackMessages'
|
|
4
|
+
);
|
|
5
|
+
const chalk = requireNpmFromCwd('react-dev-utils/chalk');
|
|
6
|
+
const printBuildError = requireNpmFromCwd('react-dev-utils/printBuildError');
|
|
7
|
+
const webpack = requireNpmFromCwd('webpack');
|
|
8
|
+
|
|
9
|
+
// TODO:展示进度条
|
|
10
|
+
// TODO: 展示结果
|
|
11
|
+
module.exports = function runWebpack(config, webpackEnv = 'development') {
|
|
12
|
+
const isDev = webpackEnv === 'development';
|
|
13
|
+
isDev ? start(config) : build(config);
|
|
14
|
+
};
|
|
15
|
+
function start(config) {
|
|
16
|
+
const compiler = webpack(config);
|
|
17
|
+
compiler.watch({}, (err, stats) => {
|
|
18
|
+
if (err) {
|
|
19
|
+
return printBuildError(err);
|
|
20
|
+
}
|
|
21
|
+
const messages = formatWebpackMessages(
|
|
22
|
+
stats.toJson({ all: false, warnings: true, errors: true })
|
|
23
|
+
);
|
|
24
|
+
if (messages.errors.length) {
|
|
25
|
+
return printBuildError(new Error(messages.errors.join('\n\n')));
|
|
26
|
+
}
|
|
27
|
+
if (messages.warnings.length) {
|
|
28
|
+
console.log(chalk.yellow(messages.warnings.join('\n\n')));
|
|
29
|
+
}
|
|
30
|
+
console.log(chalk.green('SSR Compiled successfully.'));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function build(config) {
|
|
35
|
+
const compiler = webpack(config);
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
console.log('SSR Compiling...');
|
|
38
|
+
compiler.run((err, stats) => {
|
|
39
|
+
let messages;
|
|
40
|
+
if (err) {
|
|
41
|
+
if (!err.message) {
|
|
42
|
+
return reject(err);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let errMessage = err.message;
|
|
46
|
+
|
|
47
|
+
// Add additional information for postcss errors
|
|
48
|
+
if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
|
|
49
|
+
errMessage +=
|
|
50
|
+
'\nCompileError: Begins at CSS selector ' +
|
|
51
|
+
err['postcssNode'].selector;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
messages = formatWebpackMessages({
|
|
55
|
+
errors: [errMessage],
|
|
56
|
+
warnings: [],
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
messages = formatWebpackMessages(
|
|
60
|
+
stats.toJson({
|
|
61
|
+
all: false,
|
|
62
|
+
warnings: true,
|
|
63
|
+
errors: true,
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (messages.errors.length) {
|
|
68
|
+
// Only keep the first error. Others are often indicative
|
|
69
|
+
// of the same problem, but confuse the reader with noise.
|
|
70
|
+
if (messages.errors.length > 1) {
|
|
71
|
+
messages.errors.length = 1;
|
|
72
|
+
}
|
|
73
|
+
return reject(new Error(messages.errors.join('\n\n')));
|
|
74
|
+
}
|
|
75
|
+
if (
|
|
76
|
+
process.env.CI &&
|
|
77
|
+
(typeof process.env.CI !== 'string' ||
|
|
78
|
+
process.env.CI.toLowerCase() !== 'false') &&
|
|
79
|
+
messages.warnings.length
|
|
80
|
+
) {
|
|
81
|
+
console.log(
|
|
82
|
+
chalk.yellow(
|
|
83
|
+
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
|
84
|
+
'Most CI servers set it automatically.\n'
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
return reject(new Error(messages.warnings.join('\n\n')));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const resolveArgs = {
|
|
91
|
+
stats,
|
|
92
|
+
previousFileSizes: 0,
|
|
93
|
+
warnings: messages.warnings,
|
|
94
|
+
};
|
|
95
|
+
return resolve(resolveArgs);
|
|
96
|
+
});
|
|
97
|
+
})
|
|
98
|
+
.then(
|
|
99
|
+
({ warnings }) => {
|
|
100
|
+
if (warnings.length) {
|
|
101
|
+
console.log(chalk.yellow('Compiled with warnings.\n'));
|
|
102
|
+
console.log(warnings.join('\n\n'));
|
|
103
|
+
console.log(
|
|
104
|
+
'\nSearch for the ' +
|
|
105
|
+
chalk.underline(chalk.yellow('keywords')) +
|
|
106
|
+
' to learn more about each warning.'
|
|
107
|
+
);
|
|
108
|
+
console.log(
|
|
109
|
+
'To ignore, add ' +
|
|
110
|
+
chalk.cyan('// eslint-disable-next-line') +
|
|
111
|
+
' to the line before.\n'
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
console.log(chalk.green('Compiled successfully.\n'));
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
(err) => {
|
|
118
|
+
const tscCompileOnError =
|
|
119
|
+
process.env.TSC_COMPILE_ON_ERROR === 'true';
|
|
120
|
+
if (tscCompileOnError) {
|
|
121
|
+
console.log(
|
|
122
|
+
chalk.yellow(
|
|
123
|
+
'Compiled with the following type errors (you may want to check these before deploying your app):\n'
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
printBuildError(err);
|
|
127
|
+
} else {
|
|
128
|
+
console.log(chalk.red('Failed to compile.\n'));
|
|
129
|
+
printBuildError(err);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
.catch((err) => {
|
|
135
|
+
if (err && err.message) {
|
|
136
|
+
console.log(err.message);
|
|
137
|
+
}
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
|
140
|
+
}
|