refactor: 抽离 webpack 构建

This commit is contained in:
winixt 2022-03-26 20:24:14 +08:00
parent c04148f84d
commit 99db87e400
105 changed files with 1271 additions and 1434 deletions

View File

@ -1,4 +1,3 @@
module.exports = { module.exports = {
// 需要编译的包 // 需要编译的包
pkgs: [ pkgs: [
@ -21,7 +20,8 @@ module.exports = {
'fes-preset-built-in', 'fes-preset-built-in',
'fes-plugin-windicss', 'fes-plugin-windicss',
'fes-runtime', 'fes-runtime',
'fes-utils' 'fes-utils',
'fes-build-webpack',
], ],
copy: [] copy: [],
}; };

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present webank
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,5 @@
# Fes.js vite 构建

View File

@ -0,0 +1,35 @@
{
"name": "@fesjs/build-vite",
"version": "2.0.22",
"description": "@fesjs/build-vite",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/WeBankFinTech/fes.js.git",
"directory": "packages/fes-build-vite"
},
"keywords": [
"fes"
],
"author": "qlin",
"license": "MIT",
"bugs": {
"url": "https://github.com/WeBankFinTech/fes.js/issues"
},
"homepage": "https://github.com/WeBankFinTech/fes.js#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@vitejs/plugin-vue": "^2.2.4",
"@vitejs/plugin-vue-jsx": "^1.3.8",
"vite": "^2.8.6"
},
"peerDependencies": {
"@vue/compiler-sfc": "^3.0.5"
}
}

View File

@ -0,0 +1,100 @@
import { createServer } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import SFCConfigBlockPlugin from './SFCConfigBlockPlugin';
const assert = require('assert');
export default (api) => {
const {
env,
paths,
utils: { chalk },
} = api;
const unwatchs = [];
let port;
let hostname;
let server;
function destroy() {
for (const unwatch of unwatchs) {
unwatch();
}
server?.close();
}
api.registerCommand({
command: 'dev',
description: 'start a local http service for development',
options: [
{
name: '--port',
description: 'http service port, like 8080',
},
{
name: '--https',
description: 'whether to turn on the https service',
},
],
async fn() {
server = await createServer({
mode: 'development',
plugins: [vue(), SFCConfigBlockPlugin, vueJsx()],
configFile: false,
resolve: {
alias: {
'@': paths.absSrcPath,
'@@': paths.absTmpPath,
'@fesInner': '/',
},
},
server: {
port: 8000,
},
});
await server.listen();
server.printUrls();
return {
destroy,
};
},
});
api.registerMethod({
name: 'getPort',
fn() {
assert(env === 'development', 'api.getPort() is only valid in development.');
return port;
},
});
api.registerMethod({
name: 'getHostname',
fn() {
assert(env === 'development', 'api.getHostname() is only valid in development.');
return hostname;
},
});
api.registerMethod({
name: 'getServer',
fn() {
assert(env === 'development', 'api.getServer() is only valid in development.');
return server;
},
});
api.registerMethod({
name: 'restartServer',
fn() {
console.log(chalk.gray('Try to restart dev server...'));
destroy();
process.send({
type: 'RESTART',
});
},
});
};

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present webank
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,5 @@
# Fes.js webpack 构建

View File

@ -0,0 +1,76 @@
{
"name": "@fesjs/build-webpack",
"version": "1.0.0",
"description": "@fesjs/build-webpack",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/WeBankFinTech/fes.js.git",
"directory": "packages/fes-build-webpack"
},
"keywords": [
"fes"
],
"author": "qlin",
"license": "MIT",
"bugs": {
"url": "https://github.com/WeBankFinTech/fes.js/issues"
},
"homepage": "https://github.com/WeBankFinTech/fes.js#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@babel/core": "^7.12.13",
"@babel/plugin-proposal-do-expressions": "^7.12.13",
"@babel/plugin-proposal-export-default-from": "^7.12.13",
"@babel/plugin-proposal-function-bind": "^7.12.13",
"@babel/plugin-proposal-pipeline-operator": "^7.12.13",
"@babel/plugin-transform-runtime": "^7.12.13",
"@babel/preset-env": "^7.12.13",
"@babel/preset-typescript": "^7.15.0",
"@fesjs/compiler": "^2.0.5",
"@fesjs/utils": "^2.0.4",
"@soda/friendly-errors-webpack-plugin": "^1.8.0",
"@vue/babel-plugin-jsx": "^1.0.2",
"autoprefixer": "^10.2.4",
"babel-loader": "^8.2.2",
"babel-plugin-import": "1.13.3",
"cli-highlight": "^2.1.4",
"cliui": "7.0.4",
"connect-history-api-fallback": "^1.6.0",
"copy-webpack-plugin": "^7.0.0",
"css-loader": "^5.0.1",
"css-minimizer-webpack-plugin": "^3.0.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.0.0",
"html-webpack-tags-plugin": "^3.0.0",
"less": "3.9.0",
"less-loader": "^8.0.0",
"mini-css-extract-plugin": "^1.3.5",
"postcss": "8.3.0",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^4.2.0",
"postcss-safe-parser": "^5.0.2",
"raw-loader": "^4.0.2",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"vue-loader": "^16.1.2",
"webpack": "^5.24.2",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-chain": "^6.5.1",
"webpack-dev-server": "^3.11.2",
"webpackbar": "^5.0.0-3"
},
"peerDependencies": {
"@vue/compiler-sfc": "^3.0.5",
"core-js": "^3.8.3"
},
"devDependencies": {
"core-js": "^3.8.3"
}
}

View File

@ -0,0 +1,37 @@
export default function () {
return {
plugins: [
// bundle configs
require.resolve('./plugins/features/alias'),
require.resolve('./plugins/features/analyze'),
require.resolve('./plugins/features/autoprefixer'),
require.resolve('./plugins/features/chainWebpack'),
require.resolve('./plugins/features/cssLoader'),
require.resolve('./plugins/features/copy'),
require.resolve('./plugins/features/define'),
require.resolve('./plugins/features/devServer'),
require.resolve('./plugins/features/devtool'),
require.resolve('./plugins/features/externals'),
require.resolve('./plugins/features/exportStatic'),
require.resolve('./plugins/features/extraBabelPlugins'),
require.resolve('./plugins/features/extraBabelPresets'),
require.resolve('./plugins/features/extraPostCSSPlugins'),
require.resolve('./plugins/features/html'),
require.resolve('./plugins/features/inlineLimit'),
require.resolve('./plugins/features/lessLoader'),
require.resolve('./plugins/features/outputPath'),
require.resolve('./plugins/features/postcssLoader'),
require.resolve('./plugins/features/publicPath'),
require.resolve('./plugins/features/runtimePublicPath'),
require.resolve('./plugins/features/targets'),
require.resolve('./plugins/features/terserOptions'),
require.resolve('./plugins/features/nodeModulesTransform'),
require.resolve('./plugins/features/vueLoader'),
// commands
require.resolve('./plugins/commands/build'),
require.resolve('./plugins/commands/dev'),
require.resolve('./plugins/commands/webpack'),
],
};
}

View File

@ -1,15 +1,10 @@
import webpack from 'webpack'; import webpack from 'webpack';
export async function build({ export async function build({ bundleConfig }) {
bundleConfig
}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const compiler = webpack(bundleConfig); const compiler = webpack(bundleConfig);
compiler.run((err, stats) => { compiler.run((err, stats) => {
if (err || stats.hasErrors()) { if (err || stats.hasErrors()) {
try {
console.log(stats.toString('errors-only'));
} catch (e) {}
console.error(err); console.error(err);
return reject(new Error('build failed')); return reject(new Error('build failed'));
} }

View File

@ -3,32 +3,27 @@
* https://github.com/umijs/umi/blob/master/packages/preset-built-in/src/plugins/commands/build/build.ts * https://github.com/umijs/umi/blob/master/packages/preset-built-in/src/plugins/commands/build/build.ts
*/ */
import { relative } from 'path';
import { existsSync } from 'fs';
import { Logger } from '@fesjs/compiler'; import { Logger } from '@fesjs/compiler';
const logger = new Logger('fes:plugin-built-in'); const logger = new Logger('fes:build-webpack');
export default function (api) { export default function (api) {
const { const {
paths, paths,
utils: { rimraf } utils: { rimraf, generateFiles },
} = api; } = api;
api.registerCommand({ api.registerCommand({
command: 'build', command: 'build',
description: 'build application for production', description: 'build application for production',
async fn() { async fn() {
const { relative } = require('path'); const { cleanTmpPathExceptCache, getBundleAndConfigs, printFileSizes } = require('../buildDevUtils');
const { existsSync } = require('fs');
const {
cleanTmpPathExceptCache,
getBundleAndConfigs,
printFileSizes
} = require('../buildDevUtils');
const generateFiles = require('../../../utils/generateFiles').default;
const { build } = require('./build'); const { build } = require('./build');
cleanTmpPathExceptCache({ cleanTmpPathExceptCache({
absTmpPath: paths.absTmpPath absTmpPath: paths.absTmpPath,
}); });
// generate files // generate files
@ -54,20 +49,20 @@ export default function (api) {
key: 'onBuildComplete', key: 'onBuildComplete',
type: api.ApplyPluginsType.event, type: api.ApplyPluginsType.event,
args: { args: {
stats stats,
} },
}); });
} catch (err) { } catch (err) {
await api.applyPlugins({ await api.applyPlugins({
key: 'onBuildComplete', key: 'onBuildComplete',
type: api.ApplyPluginsType.event, type: api.ApplyPluginsType.event,
args: { args: {
err err,
} },
}); });
// throw build error // throw build error
throw err; throw err;
} }
} },
}); });
} }

View File

@ -9,9 +9,7 @@ import { rimraf, chalk } from '@fesjs/utils';
import zlib from 'zlib'; import zlib from 'zlib';
import getConfig from './webpackConfig'; import getConfig from './webpackConfig';
export async function getBundleAndConfigs({ export async function getBundleAndConfigs({ api }) {
api
}) {
// get config // get config
const env = api.env === 'production' ? 'production' : 'development'; const env = api.env === 'production' ? 'production' : 'development';
const getConfigOpts = await api.applyPlugins({ const getConfigOpts = await api.applyPlugins({
@ -22,21 +20,21 @@ export async function getBundleAndConfigs({
config: api.config, config: api.config,
env, env,
entry: { entry: {
index: join(api.paths.absTmpPath, 'fes.js') index: join(api.paths.absTmpPath, 'fes.js'),
}, },
// @ts-ignore // @ts-ignore
async modifyBabelOpts(opts) { async modifyBabelOpts(opts) {
return api.applyPlugins({ return api.applyPlugins({
type: api.ApplyPluginsType.modify, type: api.ApplyPluginsType.modify,
key: 'modifyBabelOpts', key: 'modifyBabelOpts',
initialValue: opts initialValue: opts,
}); });
}, },
async modifyBabelPresetOpts(opts) { async modifyBabelPresetOpts(opts) {
return api.applyPlugins({ return api.applyPlugins({
type: api.ApplyPluginsType.modify, type: api.ApplyPluginsType.modify,
key: 'modifyBabelPresetOpts', key: 'modifyBabelPresetOpts',
initialValue: opts initialValue: opts,
}); });
}, },
async chainWebpack(webpackConfig, opts) { async chainWebpack(webpackConfig, opts) {
@ -45,44 +43,38 @@ export async function getBundleAndConfigs({
key: 'chainWebpack', key: 'chainWebpack',
initialValue: webpackConfig, initialValue: webpackConfig,
args: { args: {
...opts ...opts,
} },
}); });
}, },
async headScripts() { async headScripts() {
return api.applyPlugins({ return api.applyPlugins({
key: 'addHTMLHeadScripts', key: 'addHTMLHeadScripts',
type: api.ApplyPluginsType.add, type: api.ApplyPluginsType.add,
initialState: [] initialState: [],
}); });
}, },
publicPath: await api.applyPlugins({ publicPath: await api.applyPlugins({
key: 'modifyPublicPathStr', key: 'modifyPublicPathStr',
type: api.ApplyPluginsType.modify, type: api.ApplyPluginsType.modify,
initialValue: api.config.publicPath || '', initialValue: api.config.publicPath || '',
args: { args: {},
} }),
})
}, },
args: { args: {},
}
}); });
const bundleConfig = await api.applyPlugins({ const bundleConfig = await api.applyPlugins({
type: api.ApplyPluginsType.modify, type: api.ApplyPluginsType.modify,
key: 'modifyBundleConfig', key: 'modifyBundleConfig',
initialValue: await getConfig({ api, ...getConfigOpts }), initialValue: await getConfig({ api, ...getConfigOpts }),
args: { args: {},
}
}); });
return { bundleConfig }; return { bundleConfig };
} }
export function cleanTmpPathExceptCache({ export function cleanTmpPathExceptCache({ absTmpPath }) {
absTmpPath
}) {
if (!existsSync(absTmpPath)) return; if (!existsSync(absTmpPath)) return;
readdirSync(absTmpPath).forEach((file) => { readdirSync(absTmpPath).forEach((file) => {
if (file === '.cache') return; if (file === '.cache') return;
@ -99,7 +91,7 @@ export function printFileSizes(stats, dir) {
const json = stats.toJson({ const json = stats.toJson({
hash: false, hash: false,
modules: false, modules: false,
chunks: false chunks: false,
}); });
const filesize = (bytes) => { const filesize = (bytes) => {
@ -116,27 +108,24 @@ export function printFileSizes(stats, dir) {
return `${bytes.toFixed(1)} ${unit[loop]}`; return `${bytes.toFixed(1)} ${unit[loop]}`;
}; };
const assets = json.assets const assets = json.assets ? json.assets : json?.children?.reduce((acc, child) => acc.concat(child?.assets), []);
? json.assets
: json?.children?.reduce((acc, child) => acc.concat(child?.assets), []);
const seenNames = new Map(); const seenNames = new Map();
const isJS = val => /\.js$/.test(val); const isJS = (val) => /\.js$/.test(val);
const isCSS = val => /\.css$/.test(val); const isCSS = (val) => /\.css$/.test(val);
const orderedAssets = assets.map((a) => { const orderedAssets = assets
a.name = a.name.split('?')[0]; .map((a) => {
// These sizes are pretty large a.name = a.name.split('?')[0];
const isMainBundle = a.name.indexOf('fes.') === 0; // These sizes are pretty large
const maxRecommendedSize = isMainBundle const isMainBundle = a.name.indexOf('fes.') === 0;
? WARN_AFTER_BUNDLE_GZIP_SIZE const maxRecommendedSize = isMainBundle ? WARN_AFTER_BUNDLE_GZIP_SIZE : WARN_AFTER_CHUNK_GZIP_SIZE;
: WARN_AFTER_CHUNK_GZIP_SIZE; const isLarge = maxRecommendedSize && a.size > maxRecommendedSize;
const isLarge = maxRecommendedSize && a.size > maxRecommendedSize; return {
return { ...a,
...a, suggested: isLarge && isJS(a.name),
suggested: isLarge && isJS(a.name) };
}; })
})
.filter((a) => { .filter((a) => {
if (seenNames.has(a.name)) { if (seenNames.has(a.name)) {
return false; return false;
@ -164,43 +153,30 @@ export function printFileSizes(stats, dir) {
} }
ui.div( ui.div(
`${makeRow( `${makeRow(chalk.cyan.bold('File'), chalk.cyan.bold('Size'), chalk.cyan.bold('Gzipped'))}\n\n${orderedAssets
chalk.cyan.bold('File'), .map((asset) =>
chalk.cyan.bold('Size'), makeRow(
chalk.cyan.bold('Gzipped') /js$/.test(asset.name)
) ? asset.suggested
}\n\n${ ? chalk.yellow(join(dir, asset.name))
// eslint-disable-next-line : chalk.green(join(dir, asset.name))
orderedAssets.map(asset => makeRow(/js$/.test(asset.name) ? (asset.suggested ? chalk.yellow(join(dir, asset.name)) : chalk.green(join(dir, asset.name))) : chalk.blue(join(dir, asset.name)), : chalk.blue(join(dir, asset.name)),
filesize(asset.size), filesize(asset.size),
getGzippedSize(asset))) getGzippedSize(asset),
.join('\n')}` ),
)
.join('\n')}`,
); );
console.log(`${ui.toString()}\n\n ${chalk.gray('Images and other types of assets omitted.')}\n`);
console.log( if (orderedAssets?.some((asset) => asset.suggested)) {
`${ui.toString()}\n\n ${chalk.gray( // We'll warn for bundles exceeding them.
'Images and other types of assets omitted.' // TODO: use umi docs
)}\n`
);
if (orderedAssets?.some(asset => asset.suggested)) {
// We'll warn for bundles exceeding them.
// TODO: use umi docs
console.log(); console.log();
console.log( console.log(chalk.yellow('The bundle size is significantly larger than recommended.'));
chalk.yellow('The bundle size is significantly larger than recommended.') console.log(chalk.yellow('Consider reducing it with code splitting'));
); console.log(chalk.yellow('You can also analyze the project dependencies using ANALYZE=1'));
console.log(
chalk.yellow(
'Consider reducing it with code splitting'
)
);
console.log(
chalk.yellow(
'You can also analyze the project dependencies using ANALYZE=1'
)
);
console.log(); console.log();
} }
} }

View File

@ -1,12 +1,11 @@
import { extname, join } from 'path'; import { extname, join } from 'path';
import historyFallback from 'connect-history-api-fallback'; import historyFallback from 'connect-history-api-fallback';
const ASSET_EXT_NAMES = ['.ico', '.png', '.jpg', '.jpeg', '.gif', '.svg']; const ASSET_EXT_NAMES = ['.ico', '.png', '.jpg', '.jpeg', '.gif', '.svg'];
export default api => (req, res, next) => { export default (api) => (req, res, next) => {
const proxyConfig = api.config.proxy; const proxyConfig = api.config.proxy;
if (proxyConfig && Object.keys(proxyConfig).some(path => req.path.startsWith(path))) { if (proxyConfig && Object.keys(proxyConfig).some((path) => req.path.startsWith(path))) {
return next(); return next();
} }
if (req.path === '/favicon.ico') { if (req.path === '/favicon.ico') {

View File

@ -3,17 +3,7 @@ import webpack from 'webpack';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
export function startDevServer({ webpackConfig, host, port, proxy, https, beforeMiddlewares, afterMiddlewares, customerDevServerConfig }) {
export function startDevServer({
webpackConfig,
host,
port,
proxy,
https,
beforeMiddlewares,
afterMiddlewares,
customerDevServerConfig
}) {
const options = { const options = {
contentBase: webpackConfig.output.path, contentBase: webpackConfig.output.path,
hot: true, hot: true,
@ -37,9 +27,9 @@ export function startDevServer({
}); });
}, },
headers: { headers: {
'access-control-allow-origin': '*' 'access-control-allow-origin': '*',
}, },
...(customerDevServerConfig || {}) ...(customerDevServerConfig || {}),
}; };
if (https) { if (https) {
options.https = true; options.https = true;

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -9,7 +9,7 @@ export default (api) => {
const { const {
env, env,
paths, paths,
utils: { chalk, portfinder }, utils: { chalk, portfinder, generateFiles },
} = api; } = api;
const unwatchs = []; const unwatchs = [];
@ -39,9 +39,7 @@ export default (api) => {
], ],
async fn({ args = {} }) { async fn({ args = {} }) {
const { cleanTmpPathExceptCache, getBundleAndConfigs } = require('../buildDevUtils'); const { cleanTmpPathExceptCache, getBundleAndConfigs } = require('../buildDevUtils');
const { delay } = require('@fesjs/utils');
const createRouteMiddleware = require('./createRouteMiddleware').default; const createRouteMiddleware = require('./createRouteMiddleware').default;
const generateFiles = require('../../../utils/generateFiles').default;
const { watchPkg } = require('./watchPkg'); const { watchPkg } = require('./watchPkg');
const defaultPort = process.env.PORT || args.port || api.config.devServer?.port; const defaultPort = process.env.PORT || args.port || api.config.devServer?.port;
@ -135,11 +133,6 @@ export default (api) => {
unwatchs.push(unwatchConfig); unwatchs.push(unwatchConfig);
} }
// delay dev server 启动,避免重复 compile
// https://github.com/webpack/watchpack/issues/25
// https://github.com/yessky/webpack-mild-compile
await delay(500);
// dev // dev
const { bundleConfig } = await getBundleAndConfigs({ api }); const { bundleConfig } = await getBundleAndConfigs({ api });

View File

@ -11,11 +11,8 @@ import { isPluginOrPreset, PluginType } from '@fesjs/compiler';
function getPlugins(opts) { function getPlugins(opts) {
return Object.keys({ return Object.keys({
...opts.pkg.dependencies, ...opts.pkg.dependencies,
...opts.pkg.devDependencies ...opts.pkg.devDependencies,
}).filter(name => ( }).filter((name) => isPluginOrPreset(PluginType.plugin, name) || isPluginOrPreset(PluginType.preset, name));
isPluginOrPreset(PluginType.plugin, name)
|| isPluginOrPreset(PluginType.preset, name)
));
} }
function getPluginsFromPkgPath(opts) { function getPluginsFromPkgPath(opts) {
@ -34,7 +31,7 @@ export function watchPkg(opts) {
const pkgPath = join(opts.cwd, 'package.json'); const pkgPath = join(opts.cwd, 'package.json');
const plugins = getPluginsFromPkgPath({ pkgPath }); const plugins = getPluginsFromPkgPath({ pkgPath });
const watcher = chokidar.watch(pkgPath, { const watcher = chokidar.watch(pkgPath, {
ignoreInitial: true ignoreInitial: true,
}); });
watcher.on('all', () => { watcher.on('all', () => {
const newPlugins = getPluginsFromPkgPath({ pkgPath }); const newPlugins = getPluginsFromPkgPath({ pkgPath });

View File

@ -0,0 +1,50 @@
export default function (api) {
api.registerCommand({
command: 'webpack',
description: 'inspect webpack configurations',
options: [
{
name: '--rule <ruleName>',
description: 'inspect a specific module rule',
},
{
name: '--plugin <pluginName>',
description: 'inspect a specific plugin',
},
{
name: '--rules',
description: 'list all module rule names',
},
{
name: '--plugins',
description: 'list all plugin names',
},
{
name: '--verbose',
description: 'show full function definitions in output',
},
],
async fn({ options }) {
const assert = require('assert');
const { getBundleAndConfigs } = require('../buildDevUtils');
const { toString } = require('webpack-chain');
const { highlight } = require('cli-highlight');
const { bundleConfig } = await getBundleAndConfigs({ api });
let config = bundleConfig;
assert(config, 'No valid config found with fes entry.');
if (options.rule) {
config = config.module.rules.find((r) => r.__ruleNames[0] === options.rule);
} else if (options.plugin) {
config = config.plugins.find((p) => p.__pluginName === options.plugin);
} else if (options.rules) {
config = config.module.rules.map((r) => r.__ruleNames[0]);
} else if (options.plugins) {
config = config.plugins.map((p) => p.__pluginName || p.constructor.name);
}
console.log(highlight(toString(config, { verbose: options.verbose }), { language: 'js' }));
},
});
}

View File

@ -0,0 +1,112 @@
// css less post-css mini-css css 压缩
// extraPostCSSPlugins
// postcssLoader
// lessLoader
// css-loader
// 支持 热加载
// 性能优化
// css 压缩 https://github.com/webpack-contrib/css-minimizer-webpack-plugin
// 根据 entry 进行代码块拆分
// 根据 entry 将文件输出到不同的文件夹
import { deepmerge } from '@fesjs/utils';
function createRules({ isDev, webpackConfig, config, lang, test, loader, options, browserslist, styleLoaderOption }) {
function applyLoaders(rule, cssLoaderOption = {}) {
if (isDev) {
rule.use('extra-css-loader').loader(require.resolve('style-loader')).options(Object.assign({}, styleLoaderOption));
} else {
rule.use('extra-css-loader').loader(require('mini-css-extract-plugin').loader).options({});
}
rule.use('css-loader')
.loader(require.resolve('css-loader'))
.options(
deepmerge(
{
importLoaders: 1,
...cssLoaderOption,
},
config.cssLoader || {},
),
);
rule.use('postcss-loader')
.loader(require.resolve('postcss-loader'))
.options(
deepmerge(
{
postcssOptions: () => ({
plugins: [
// https://github.com/luisrudge/postcss-flexbugs-fixes
require('postcss-flexbugs-fixes'),
require('postcss-safe-parser'),
[require('autoprefixer'), { ...config.autoprefixer, overrideBrowserslist: browserslist }],
...(config.extraPostCSSPlugins ? config.extraPostCSSPlugins : []),
],
}),
},
config.postcssLoader || {},
),
);
if (loader) {
rule.use(loader).loader(require.resolve(loader)).options(options);
}
}
const rule = webpackConfig.module.rule(lang).test(test);
applyLoaders(rule.oneOf('css-modules').resourceQuery(/module/), {
modules: {
localIdentName: '[local]___[hash:base64:5]',
},
});
applyLoaders(rule.oneOf('css'));
}
export default function createCssWebpackConfig({ isDev, config, webpackConfig, browserslist }) {
createRules({
isDev,
webpackConfig,
config,
lang: 'css',
test: /\.css$/,
browserslist,
});
createRules({
isDev,
webpackConfig,
config,
lang: 'less',
test: /\.less$/,
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true,
...config.lessLoader,
},
},
browserslist,
});
if (!isDev) {
webpackConfig.plugin('extra-css').use(require.resolve('mini-css-extract-plugin'), [
{
filename: '[name].[contenthash:8].css',
chunkFilename: '[id].[contenthash:8].css',
},
]);
webpackConfig.optimization.minimizer('css').use(require.resolve('css-minimizer-webpack-plugin'), [{}]);
}
return (options) => {
createRules({
isDev,
config,
webpackConfig,
browserslist,
...options,
});
};
}

View File

@ -0,0 +1,6 @@
import webpack from 'webpack';
import resolveDefine from './resolveDefine';
export default function createDefineWebpackConfig({ config, webpackConfig }) {
webpackConfig.plugin('define').use(webpack.DefinePlugin, [resolveDefine(config)]);
}

View File

@ -1,13 +1,6 @@
import { import { winPath } from '@fesjs/utils';
winPath
} from '@fesjs/utils';
function getBabelOpts({ function getBabelOpts({ cwd, targets, config, presetOpts }) {
cwd,
targets,
config,
presetOpts
}) {
const presets = [ const presets = [
[ [
require.resolve('@babel/preset-env'), require.resolve('@babel/preset-env'),
@ -16,10 +9,10 @@ function getBabelOpts({
useBuiltIns: 'usage', useBuiltIns: 'usage',
corejs: { corejs: {
version: 3, version: 3,
proposals: true proposals: true,
}, },
modules: false modules: false,
} },
], ],
[ [
// FEATURE 实现类型安全检查 // FEATURE 实现类型安全检查
@ -28,18 +21,18 @@ function getBabelOpts({
// https://babeljs.io/docs/en/babel-plugin-transform-typescript#impartial-namespace-support // https://babeljs.io/docs/en/babel-plugin-transform-typescript#impartial-namespace-support
allowNamespaces: true, allowNamespaces: true,
isTSX: true, isTSX: true,
allExtensions: true allExtensions: true,
} },
], ],
...(config.extraBabelPresets || []) ...(config.extraBabelPresets || []),
]; ];
const plugins = [ const plugins = [
require('@babel/plugin-proposal-export-default-from').default, require('@babel/plugin-proposal-export-default-from').default,
[ [
require('@babel/plugin-proposal-pipeline-operator').default, require('@babel/plugin-proposal-pipeline-operator').default,
{ {
proposal: 'minimal' proposal: 'minimal',
} },
], ],
require('@babel/plugin-proposal-do-expressions').default, require('@babel/plugin-proposal-do-expressions').default,
require('@babel/plugin-proposal-function-bind').default, require('@babel/plugin-proposal-function-bind').default,
@ -47,18 +40,12 @@ function getBabelOpts({
require.resolve('@babel/plugin-transform-runtime'), require.resolve('@babel/plugin-transform-runtime'),
{ {
useESModules: true, useESModules: true,
...presetOpts.transformRuntime ...presetOpts.transformRuntime,
} },
], ],
...(presetOpts.import ...(presetOpts.import ? presetOpts.import.map((importOpts) => [require.resolve('babel-plugin-import'), importOpts, importOpts.libraryName]) : []),
? presetOpts.import.map(importOpts => [
require.resolve('babel-plugin-import'),
importOpts,
importOpts.libraryName
])
: []),
require.resolve('@vue/babel-plugin-jsx'), require.resolve('@vue/babel-plugin-jsx'),
...(config.extraBabelPlugins || []) ...(config.extraBabelPlugins || []),
]; ];
return { return {
babelrc: false, babelrc: false,
@ -66,23 +53,18 @@ function getBabelOpts({
cacheDirectory: process.env.BABEL_CACHE !== 'none' ? winPath(`${cwd}/.cache/babel-loader`) : false, cacheDirectory: process.env.BABEL_CACHE !== 'none' ? winPath(`${cwd}/.cache/babel-loader`) : false,
presets, presets,
plugins, plugins,
overrides: [{ overrides: [
test: [/[\\/]node_modules[\\/]/, /\.fes/], {
sourceType: 'unambiguous' test: [/[\\/]node_modules[\\/]/, /\.fes/],
}] sourceType: 'unambiguous',
},
],
}; };
} }
export default async ({ cwd, config, modifyBabelOpts, modifyBabelPresetOpts, targets }) => {
export default async ({
cwd,
config,
modifyBabelOpts,
modifyBabelPresetOpts,
targets
}) => {
let presetOpts = { let presetOpts = {
transformRuntime: {} transformRuntime: {},
}; };
if (modifyBabelPresetOpts) { if (modifyBabelPresetOpts) {
presetOpts = await modifyBabelPresetOpts(presetOpts); presetOpts = await modifyBabelPresetOpts(presetOpts);
@ -91,7 +73,7 @@ export default async ({
cwd, cwd,
config, config,
presetOpts, presetOpts,
targets targets,
}); });
if (modifyBabelOpts) { if (modifyBabelOpts) {
babelOpts = await modifyBabelOpts(babelOpts); babelOpts = await modifyBabelOpts(babelOpts);

View File

@ -1,24 +1,15 @@
import { join, resolve } from 'path'; import { join, resolve } from 'path';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { import { winPath } from '@fesjs/utils';
winPath
} from '@fesjs/utils';
import resolveDefine from './resolveDefine'; import resolveDefine from './resolveDefine';
export default async function createHtmlWebpackConfig({ export default async function createHtmlWebpackConfig({ api, cwd, config, webpackConfig, headScripts, isProd }) {
api,
cwd,
config,
webpackConfig,
headScripts,
isProd
}) {
const htmlOptions = { const htmlOptions = {
title: 'fes.js', title: 'fes.js',
filename: '[name].html', filename: '[name].html',
...config.html, ...config.html,
templateParameters: resolveDefine(config, true), templateParameters: resolveDefine(config, true),
mountElementId: config.mountElementId mountElementId: config.mountElementId,
}; };
if (isProd) { if (isProd) {
@ -27,10 +18,10 @@ export default async function createHtmlWebpackConfig({
removeComments: true, removeComments: true,
collapseWhitespace: true, collapseWhitespace: true,
collapseBooleanAttributes: true, collapseBooleanAttributes: true,
removeScriptTypeAttributes: true removeScriptTypeAttributes: true,
// more options: // more options:
// https://github.com/kangax/html-minifier#options-quick-reference // https://github.com/kangax/html-minifier#options-quick-reference
} },
}); });
} }
@ -39,15 +30,11 @@ export default async function createHtmlWebpackConfig({
const publicCopyIgnore = []; const publicCopyIgnore = [];
// default, single page setup. // default, single page setup.
htmlOptions.template = existsSync(htmlPath) htmlOptions.template = existsSync(htmlPath) ? htmlPath : defaultHtmlPath;
? htmlPath
: defaultHtmlPath;
publicCopyIgnore.push(winPath(htmlOptions.template)); publicCopyIgnore.push(winPath(htmlOptions.template));
webpackConfig webpackConfig.plugin('html').use(require.resolve('html-webpack-plugin'), [htmlOptions]);
.plugin('html')
.use(require.resolve('html-webpack-plugin'), [htmlOptions]);
// 如果需要导出html则根据路由生成对应的html文件 // 如果需要导出html则根据路由生成对应的html文件
if (config.exportStatic) { if (config.exportStatic) {
@ -62,11 +49,9 @@ export default async function createHtmlWebpackConfig({
title: route?.meta?.title || config.html.title || 'fes.js', title: route?.meta?.title || config.html.title || 'fes.js',
filename: _fileName, filename: _fileName,
templateParameters: resolveDefine(config, true), templateParameters: resolveDefine(config, true),
mountElementId: config.mountElementId mountElementId: config.mountElementId,
}; };
webpackConfig webpackConfig.plugin(_fileName).use(require.resolve('html-webpack-plugin'), [_htmlOptions]);
.plugin(_fileName)
.use(require.resolve('html-webpack-plugin'), [_htmlOptions]);
} }
if (route.children && route.children.length) { if (route.children && route.children.length) {
addHtml(route.children); addHtml(route.children);
@ -79,16 +64,16 @@ export default async function createHtmlWebpackConfig({
if (headScripts) { if (headScripts) {
const headScriptsMap = await headScripts(); const headScriptsMap = await headScripts();
webpackConfig webpackConfig.plugin('html-tags').use(require.resolve('html-webpack-tags-plugin'), [
.plugin('html-tags') {
.use(require.resolve('html-webpack-tags-plugin'), [{
append: false, append: false,
scripts: headScriptsMap.map(script => ({ scripts: headScriptsMap.map((script) => ({
path: script.src path: script.src,
})) })),
}]); },
]);
} }
return { return {
publicCopyIgnore publicCopyIgnore,
}; };
} }

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
</head>
<body>
<div id="<%= htmlWebpackPlugin.options.mountElementId %>"></div>
</body>
</html>

View File

@ -13,18 +13,17 @@ function getTargetsAndBrowsersList({ config }) {
let targets = config.targets || {}; let targets = config.targets || {};
targets = Object.keys(targets) targets = Object.keys(targets)
.filter(key => targets[key] !== false) .filter((key) => targets[key] !== false)
.reduce((memo, key) => { .reduce((memo, key) => {
memo[key] = targets[key]; memo[key] = targets[key];
return memo; return memo;
}, {}); }, {});
const browserslist = targets.browsers const browserslist = targets.browsers || Object.keys(targets).map((key) => `${key} >= ${targets[key] === true ? '0' : targets[key]}`);
|| Object.keys(targets).map(key => `${key} >= ${targets[key] === true ? '0' : targets[key]}`);
return { return {
targets, targets,
browserslist browserslist,
}; };
} }
@ -38,7 +37,7 @@ const DEFAULT_EXCLUDE_NODE_MODULES = [
'lodash-es', 'lodash-es',
'webpack-dev-server', 'webpack-dev-server',
'ansi-html', 'ansi-html',
'html-entities' 'html-entities',
]; ];
function genTranspileDepRegex(exclude) { function genTranspileDepRegex(exclude) {
@ -47,7 +46,8 @@ function genTranspileDepRegex(exclude) {
if (typeof dep === 'string') { if (typeof dep === 'string') {
const depPath = join('node_modules', dep, '/'); const depPath = join('node_modules', dep, '/');
return /^win/.test(require('os').platform()) ? depPath.replace(/\\/g, '\\\\') : depPath; return /^win/.test(require('os').platform()) ? depPath.replace(/\\/g, '\\\\') : depPath;
} if (dep instanceof RegExp) { }
if (dep instanceof RegExp) {
return dep.source; return dep.source;
} }
@ -56,19 +56,7 @@ function genTranspileDepRegex(exclude) {
return deps.length ? new RegExp(deps.join('|')) : null; return deps.length ? new RegExp(deps.join('|')) : null;
} }
export default async function getConfig({ api, cwd, config, env, entry = {}, modifyBabelOpts, modifyBabelPresetOpts, chainWebpack, headScripts, publicPath }) {
export default async function getConfig({
api,
cwd,
config,
env,
entry = {},
modifyBabelOpts,
modifyBabelPresetOpts,
chainWebpack,
headScripts,
publicPath
}) {
const isDev = env === 'development'; const isDev = env === 'development';
const isProd = env === 'production'; const isProd = env === 'production';
const webpackConfig = new Config(); const webpackConfig = new Config();
@ -77,13 +65,13 @@ export default async function getConfig({
webpackConfig.mode(env); webpackConfig.mode(env);
webpackConfig.stats('verbose'); webpackConfig.stats('verbose');
webpackConfig.externals(config.externals || {}); webpackConfig.externals(config.externals || {});
webpackConfig.devtool(isDev ? (config.devtool || 'cheap-module-source-map') : config.devtool); webpackConfig.devtool(isDev ? config.devtool || 'cheap-module-source-map' : config.devtool);
// --------------- cache ----------- // --------------- cache -----------
webpackConfig.cache({ webpackConfig.cache({
type: 'filesystem', type: 'filesystem',
version: require('../../../../package.json').version, version: require('../../../../package.json').version,
cacheDirectory: join(cwd, '.cache/webpack') cacheDirectory: join(cwd, '.cache/webpack'),
}); });
// --------------- entry ----------- // --------------- entry -----------
@ -93,19 +81,14 @@ export default async function getConfig({
}); });
// --------------- output ----------- // --------------- output -----------
webpackConfig.output webpackConfig.output.path(absoluteOutput).publicPath(publicPath).filename('[name].[contenthash:8].js').chunkFilename('[name].[contenthash:8].chunk.js');
.path(absoluteOutput)
.publicPath(publicPath)
.filename('[name].[contenthash:8].js')
.chunkFilename('[name].[contenthash:8].chunk.js');
// --------------- resolve ----------- // --------------- resolve -----------
webpackConfig.resolve.extensions.merge(['.mjs', '.js', '.jsx', '.vue', '.ts', '.tsx', '.json', '.wasm']); webpackConfig.resolve.extensions.merge(['.mjs', '.js', '.jsx', '.vue', '.ts', '.tsx', '.json', '.wasm']);
if (config.alias) { if (config.alias) {
Object.keys(config.alias).forEach((key) => { Object.keys(config.alias).forEach((key) => {
webpackConfig.resolve.alias webpackConfig.resolve.alias.set(key, config.alias[key]);
.set(key, config.alias[key]);
}); });
} }
@ -122,9 +105,9 @@ export default async function getConfig({
loader: require.resolve('file-loader'), loader: require.resolve('file-loader'),
options: { options: {
name: 'static/[name].[hash:8].[ext]', name: 'static/[name].[hash:8].[ext]',
esModule: false esModule: false,
} },
} },
}); });
webpackConfig.module webpackConfig.module
@ -134,7 +117,7 @@ export default async function getConfig({
.loader(require.resolve('file-loader')) .loader(require.resolve('file-loader'))
.options({ .options({
name: 'static/[name].[hash:8].[ext]', name: 'static/[name].[hash:8].[ext]',
esModule: false esModule: false,
}); });
webpackConfig.module webpackConfig.module
@ -144,7 +127,7 @@ export default async function getConfig({
.loader(require.resolve('file-loader')) .loader(require.resolve('file-loader'))
.options({ .options({
name: 'static/[name].[hash:8].[ext]', name: 'static/[name].[hash:8].[ext]',
esModule: false esModule: false,
}); });
webpackConfig.module webpackConfig.module
@ -153,7 +136,7 @@ export default async function getConfig({
.use('raw-loader') .use('raw-loader')
.loader(require.resolve('raw-loader')) .loader(require.resolve('raw-loader'))
.options({ .options({
esModule: false esModule: false,
}); });
const { targets, browserslist } = getTargetsAndBrowsersList({ config }); const { targets, browserslist } = getTargetsAndBrowsersList({ config });
@ -162,7 +145,7 @@ export default async function getConfig({
config, config,
modifyBabelOpts, modifyBabelOpts,
modifyBabelPresetOpts, modifyBabelPresetOpts,
targets targets,
}); });
// --------------- js ----------- // --------------- js -----------
@ -182,7 +165,8 @@ export default async function getConfig({
} }
// Don't transpile node_modules // Don't transpile node_modules
return /node_modules/.test(filepath); return /node_modules/.test(filepath);
}).end() })
.end()
.use('babel-loader') .use('babel-loader')
.loader(require.resolve('babel-loader')) .loader(require.resolve('babel-loader'))
.options(babelOpts); .options(babelOpts);
@ -193,14 +177,16 @@ export default async function getConfig({
webpackConfig.module webpackConfig.module
.rule('js-in-node_modules') .rule('js-in-node_modules')
.test(/\.(js|mjs)$/) .test(/\.(js|mjs)$/)
.include.add(/node_modules/).end() .include.add(/node_modules/)
.end()
.exclude.add((filepath) => { .exclude.add((filepath) => {
if (transpileDepRegex && transpileDepRegex.test(filepath)) { if (transpileDepRegex && transpileDepRegex.test(filepath)) {
return true; return true;
} }
return false; return false;
}).end() })
.end()
.use('babel-loader') .use('babel-loader')
.loader(require.resolve('babel-loader')) .loader(require.resolve('babel-loader'))
.options(babelOpts); .options(babelOpts);
@ -211,13 +197,13 @@ export default async function getConfig({
isDev, isDev,
config, config,
webpackConfig, webpackConfig,
browserslist browserslist,
}); });
// --------------- vue ----------- // --------------- vue -----------
createVueWebpackConfig({ createVueWebpackConfig({
config, config,
webpackConfig webpackConfig,
}); });
// --------------- html ----------- // --------------- html -----------
@ -227,47 +213,50 @@ export default async function getConfig({
config, config,
webpackConfig, webpackConfig,
headScripts, headScripts,
isProd isProd,
}); });
// --------------- copy ----------- // --------------- copy -----------
const copyPatterns = [existsSync(join(cwd, 'public')) && { const copyPatterns = [
from: join(cwd, 'public'), existsSync(join(cwd, 'public')) && {
filter: (resourcePath) => { from: join(cwd, 'public'),
if (resourcePath.indexOf('.DS_Store') !== -1) { filter: (resourcePath) => {
return false; if (resourcePath.indexOf('.DS_Store') !== -1) {
} return false;
if (publicCopyIgnore.includes(resourcePath)) { }
return false; if (publicCopyIgnore.includes(resourcePath)) {
} return false;
return true; }
return true;
},
to: absoluteOutput,
}, },
to: absoluteOutput ...(config.copy || []).map((item) => {
}, ...((config.copy || []).map((item) => { if (typeof item === 'string') {
if (typeof item === 'string') { return {
from: join(cwd, item.from),
to: absoluteOutput,
};
}
return { return {
from: join(cwd, item.from), from: join(cwd, item.from),
to: absoluteOutput to: join(absoluteOutput, item.to),
}; };
} }),
return { ].filter(Boolean);
from: join(cwd, item.from),
to: join(absoluteOutput, item.to)
};
}))].filter(Boolean);
// const publicCopyIgnore = ['.DS_Store']; // const publicCopyIgnore = ['.DS_Store'];
if (copyPatterns.length) { if (copyPatterns.length) {
webpackConfig webpackConfig.plugin('copy').use(require.resolve('copy-webpack-plugin'), [
.plugin('copy') {
.use(require.resolve('copy-webpack-plugin'), [{ patterns: copyPatterns,
patterns: copyPatterns },
}]); ]);
} }
// --------------- define ----------- // --------------- define -----------
createDefineWebpackConfig({ createDefineWebpackConfig({
config, config,
webpackConfig webpackConfig,
}); });
// --------------- 分包 ----------- // --------------- 分包 -----------
@ -278,16 +267,16 @@ export default async function getConfig({
name: 'chunk-vendors', name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/, test: /[\\/]node_modules[\\/]/,
priority: -10, priority: -10,
chunks: 'initial' chunks: 'initial',
}, },
common: { common: {
name: 'chunk-common', name: 'chunk-common',
minChunks: 2, minChunks: 2,
priority: -20, priority: -20,
chunks: 'initial', chunks: 'initial',
reuseExistingChunk: true reuseExistingChunk: true,
} },
} },
}); });
} }
@ -295,23 +284,19 @@ export default async function getConfig({
createMinimizerWebpackConfig({ createMinimizerWebpackConfig({
isProd, isProd,
config, config,
webpackConfig webpackConfig,
}); });
// --------------- 构建输出 ---------- // --------------- 构建输出 ----------
webpackConfig webpackConfig.plugin('progress').use(require.resolve('webpackbar'));
.plugin('progress')
.use(require.resolve('webpackbar'));
webpackConfig webpackConfig.plugin('friendly-errors').use(require('@soda/friendly-errors-webpack-plugin'));
.plugin('friendly-errors')
.use(require('@soda/friendly-errors-webpack-plugin'));
// --------------- chainwebpack ----------- // --------------- chainwebpack -----------
if (chainWebpack) { if (chainWebpack) {
await chainWebpack(webpackConfig, { await chainWebpack(webpackConfig, {
createCSSRule, createCSSRule,
webpack webpack,
}); });
} }
// 用户配置的 chainWebpack 优先级最高 // 用户配置的 chainWebpack 优先级最高
@ -319,7 +304,7 @@ export default async function getConfig({
await config.chainWebpack(webpackConfig, { await config.chainWebpack(webpackConfig, {
createCSSRule, createCSSRule,
env, env,
webpack webpack,
}); });
} }

View File

@ -2,7 +2,7 @@ import { deepmerge } from '@fesjs/utils';
const defaultTerserOptions = { const defaultTerserOptions = {
compress: { compress: {
// turn off flags with small gains to speed up minification // turn off flags with small gains to speed up minification
arrows: false, arrows: false,
collapse_vars: false, // 0.3kb collapse_vars: false, // 0.3kb
comparisons: false, comparisons: false,
@ -30,31 +30,21 @@ const defaultTerserOptions = {
// required features to drop conditional branches // required features to drop conditional branches
conditionals: true, conditionals: true,
dead_code: true, dead_code: true,
evaluate: true evaluate: true,
}, },
mangle: { mangle: {
safari10: true safari10: true,
} },
}; };
const terserOptions = config => ({ const terserOptions = (config) => ({
terserOptions: deepmerge( terserOptions: deepmerge(defaultTerserOptions, config.terserOptions || {}),
defaultTerserOptions, extractComments: false,
config.terserOptions || {}
),
extractComments: false
}); });
export default function createMinimizerWebpackConfig({ isProd, config, webpackConfig }) {
export default function createMinimizerWebpackConfig({
isProd,
config,
webpackConfig
}) {
if (isProd) { if (isProd) {
webpackConfig.optimization webpackConfig.optimization.minimizer('terser').use(require.resolve('terser-webpack-plugin'), [terserOptions(config)]);
.minimizer('terser')
.use(require.resolve('terser-webpack-plugin'), [terserOptions(config)]);
} }
if (process.env.FES_ENV === 'test') { if (process.env.FES_ENV === 'test') {
webpackConfig.optimization.minimize(false); webpackConfig.optimization.minimize(false);

View File

@ -0,0 +1,10 @@
const pitcher = (code) => code;
export const pitch = function () {
const context = this;
if (/&blockType=config/.test(context.resourceQuery)) {
return '';
}
};
export default pitcher;

View File

@ -25,7 +25,7 @@ export default function resolveDefine(opts = {}, raw) {
const define = { const define = {
__VUE_OPTIONS_API__: true, __VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false, __VUE_PROD_DEVTOOLS__: false,
...opts.define ...opts.define,
}; };
for (const key in define) { for (const key in define) {
@ -36,6 +36,6 @@ export default function resolveDefine(opts = {}, raw) {
return { return {
'process.env': env, 'process.env': env,
...define ...define,
}; };
} }

View File

@ -1,9 +1,4 @@
import qs from 'qs'; export default function createVueWebpackConfig({ config, webpackConfig }) {
export default function createVueWebpackConfig({
config,
webpackConfig
}) {
webpackConfig.module webpackConfig.module
.rule('vue') .rule('vue')
.test(/\.vue$/) .test(/\.vue$/)
@ -11,7 +6,7 @@ export default function createVueWebpackConfig({
.loader(require.resolve('vue-loader')) .loader(require.resolve('vue-loader'))
.options({ .options({
babelParserPlugins: ['jsx', 'classProperties', 'decorators-legacy'], babelParserPlugins: ['jsx', 'classProperties', 'decorators-legacy'],
...(config.vueLoader || {}) ...(config.vueLoader || {}),
}) })
.end(); .end();
@ -21,11 +16,10 @@ export default function createVueWebpackConfig({
if (!query) { if (!query) {
return false; return false;
} }
const parsed = qs.parse(query.slice(1)); return query.startsWith('?vue&type=custom');
return parsed.vue != null; })
}).use('vue-custom-loader').loader(require.resolve('./pitcher')); .use('vue-custom-loader')
.loader(require.resolve('./pitcher'));
webpackConfig webpackConfig.plugin('vue-loader-plugin').use(require('vue-loader').VueLoaderPlugin);
.plugin('vue-loader-plugin')
.use(require('vue-loader').VueLoaderPlugin);
} }

View File

@ -7,9 +7,8 @@ export default (api) => {
schema(joi) { schema(joi) {
return joi.object(); return joi.object();
}, },
default: { default: {},
} },
}
}); });
api.chainWebpack(async (memo) => { api.chainWebpack(async (memo) => {

View File

@ -1,4 +1,3 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'analyze', key: 'analyze',
@ -13,7 +12,7 @@ export default (api) => {
generateStatsFile: joi.boolean(), generateStatsFile: joi.boolean(),
statsFilename: joi.string(), statsFilename: joi.string(),
logLevel: joi.string().valid('info', 'warn', 'error', 'silent'), logLevel: joi.string().valid('info', 'warn', 'error', 'silent'),
defaultSizes: joi.string().valid('stat', 'parsed', 'gzip') defaultSizes: joi.string().valid('stat', 'parsed', 'gzip'),
}) })
.unknown(true); .unknown(true);
}, },
@ -25,17 +24,13 @@ export default (api) => {
generateStatsFile: !!process.env.ANALYZE_DUMP, generateStatsFile: !!process.env.ANALYZE_DUMP,
statsFilename: process.env.ANALYZE_DUMP || 'stats.json', statsFilename: process.env.ANALYZE_DUMP || 'stats.json',
logLevel: process.env.ANALYZE_LOG_LEVEL || 'info', logLevel: process.env.ANALYZE_LOG_LEVEL || 'info',
defaultSizes: 'parsed' // stat // gzip defaultSizes: 'parsed', // stat // gzip
} },
}, },
enableBy: () => !!process.env.ANALYZE enableBy: () => !!process.env.ANALYZE,
}); });
api.chainWebpack((webpackConfig) => { api.chainWebpack((webpackConfig) => {
webpackConfig webpackConfig.plugin('bundle-analyzer').use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [api.config?.analyze || {}]);
.plugin('bundle-analyzer')
.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [
api.config?.analyze || {}
]);
return webpackConfig; return webpackConfig;
}); });
}; };

View File

@ -0,0 +1,13 @@
export default (api) => {
api.describe({
key: 'autoprefixer',
config: {
default: {
flexbox: 'no-2009',
},
schema(joi) {
return joi.object().description('postcss autoprefixer, default flexbox: no-2009');
},
},
});
};

View File

@ -4,7 +4,7 @@ export default (api) => {
config: { config: {
schema(joi) { schema(joi) {
return joi.function(); return joi.function();
} },
} },
}); });
}; };

View File

@ -1,4 +1,3 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'copy', key: 'copy',
@ -8,12 +7,12 @@ export default (api) => {
joi.alternatives( joi.alternatives(
joi.object({ joi.object({
from: joi.string(), from: joi.string(),
to: joi.string() to: joi.string(),
}), }),
joi.string() joi.string(),
) ),
); );
} },
} },
}); });
}; };

View File

@ -0,0 +1,22 @@
export default (api) => {
api.describe({
key: 'cssLoader',
config: {
default: {},
schema(joi) {
return joi
.object({
url: joi.alternatives(joi.boolean(), joi.function()),
import: joi.alternatives(joi.boolean(), joi.function()),
modules: joi.alternatives(joi.boolean(), joi.string(), joi.object()),
sourceMap: joi.boolean(),
importLoaders: joi.number(),
onlyLocals: joi.boolean(),
esModule: joi.boolean(),
localsConvention: joi.string().valid('asIs', 'camelCase', 'camelCaseOnly', 'dashes', 'dashesOnly'),
})
.description('more css-loader options see https://webpack.js.org/loaders/css-loader/#options');
},
},
});
};

View File

@ -1,4 +1,3 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'define', key: 'define',
@ -6,8 +5,7 @@ export default (api) => {
schema(joi) { schema(joi) {
return joi.object(); return joi.object();
}, },
default: { default: {},
} },
}
}); });
}; };

View File

@ -1,4 +1,3 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'devServer', key: 'devServer',
@ -13,17 +12,17 @@ export default (api) => {
joi joi
.object({ .object({
key: joi.string(), key: joi.string(),
cert: joi.string() cert: joi.string(),
}) })
.unknown(), .unknown(),
joi.boolean() joi.boolean(),
), ),
headers: joi.object(), headers: joi.object(),
writeToDisk: joi.alternatives(joi.boolean(), joi.function()) writeToDisk: joi.alternatives(joi.boolean(), joi.function()),
}) })
.description('devServer configs') .description('devServer configs')
.unknown(true); .unknown(true);
} },
} },
}); });
}; };

View File

@ -1,11 +1,10 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'devtool', key: 'devtool',
config: { config: {
schema(joi) { schema(joi) {
return joi.string(); return joi.string();
} },
} },
}); });
}; };

View File

@ -5,13 +5,11 @@ export default (api) => {
schema(joi) { schema(joi) {
return joi.object({ return joi.object({
htmlSuffix: joi.boolean(), htmlSuffix: joi.boolean(),
dynamicRoot: joi.boolean() dynamicRoot: joi.boolean(),
}); });
} },
}, },
// TODO: api.EnableBy.config 读取的 userConfigmodifyDefaultConfig hook 修改后对这个判断不起效 // TODO: api.EnableBy.config 读取的 userConfigmodifyDefaultConfig hook 修改后对这个判断不起效
enableBy: () => ('exportStatic' in api.userConfig enableBy: () => ('exportStatic' in api.userConfig ? api.userConfig.exportStatic : api.config?.exportStatic),
? api.userConfig.exportStatic
: api.config?.exportStatic)
}); });
}; };

View File

@ -1,4 +1,3 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'externals', key: 'externals',
@ -6,7 +5,7 @@ export default (api) => {
schema(joi) { schema(joi) {
// https://webpack.js.org/configuration/externals/#externals // https://webpack.js.org/configuration/externals/#externals
return joi.alternatives(joi.object(), joi.string(), joi.function()); return joi.alternatives(joi.object(), joi.string(), joi.function());
} },
} },
}); });
}; };

View File

@ -1,11 +1,10 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'extraBabelPlugins', key: 'extraBabelPlugins',
config: { config: {
schema(joi) { schema(joi) {
return joi.array(); return joi.array();
} },
} },
}); });
}; };

View File

@ -1,11 +1,10 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'extraBabelPresets', key: 'extraBabelPresets',
config: { config: {
schema(joi) { schema(joi) {
return joi.array(); return joi.array();
} },
} },
}); });
}; };

View File

@ -1,11 +1,10 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'extraPostCSSPlugins', key: 'extraPostCSSPlugins',
config: { config: {
schema(joi) { schema(joi) {
return joi.array(); return joi.array();
} },
} },
}); });
}; };

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'html',
config: {
schema(joi) {
return joi.object().description('more html-webpack-plugin options see https://github.com/jantimon/html-webpack-plugin#configuration');
},
default: {},
},
});
};

View File

@ -1,11 +1,10 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'inlineLimit', key: 'inlineLimit',
config: { config: {
schema(joi) { schema(joi) {
return joi.number(); return joi.number();
} },
} },
}); });
}; };

View File

@ -1,4 +1,3 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'lessLoader', key: 'lessLoader',
@ -6,7 +5,7 @@ export default (api) => {
default: {}, default: {},
schema(joi) { schema(joi) {
return joi.object(); return joi.object();
} },
} },
}); });
}; };

View File

@ -1,16 +1,15 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'nodeModulesTransform', key: 'nodeModulesTransform',
config: { config: {
default: { default: {
exclude: [] exclude: [],
}, },
schema(joi) { schema(joi) {
return joi.object({ return joi.object({
exclude: joi.array().items(joi.string()) exclude: joi.array().items(joi.string()),
}); });
} },
} },
}); });
}; };

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'outputPath',
config: {
default: 'dist',
schema(joi) {
return joi.string().not('src', 'public', 'pages', 'mock', 'config').allow('');
},
},
});
};

View File

@ -1,11 +1,10 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'postcssLoader', key: 'postcssLoader',
config: { config: {
schema(joi) { schema(joi) {
return joi.object(); return joi.object();
} },
} },
}); });
}; };

View File

@ -0,0 +1,11 @@
export default (api) => {
api.describe({
key: 'publicPath',
config: {
default: '/',
schema(joi) {
return joi.string().regex(/\/$/).error(new Error('config.publicPath must end with /.'));
},
},
});
};

View File

@ -1,12 +1,11 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'runtimePublicPath', key: 'runtimePublicPath',
config: { config: {
schema(joi) { schema(joi) {
return joi.boolean(); return joi.boolean();
} },
}, },
default: false default: false,
}); });
}; };

View File

@ -1,4 +1,3 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'targets', key: 'targets',
@ -8,11 +7,11 @@ export default (api) => {
firefox: 64, firefox: 64,
safari: 10, safari: 10,
edge: 13, edge: 13,
ios: 10 ios: 10,
}, },
schema(joi) { schema(joi) {
return joi.object(); return joi.object();
} },
} },
}); });
}; };

View File

@ -1,11 +1,10 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'terserOptions', key: 'terserOptions',
config: { config: {
schema(joi) { schema(joi) {
return joi.object(); return joi.object();
} },
} },
}); });
}; };

View File

@ -0,0 +1,10 @@
export default (api) => {
api.describe({
key: 'vueLoader',
config: {
schema(joi) {
return joi.object({}).description('more vue-loader options see https://vue-loader.vuejs.org/');
},
},
});
};

View File

@ -0,0 +1,6 @@
import { winPath } from '@fesjs/utils';
import { dirname } from 'path';
export const runtimePath = winPath(
dirname(require.resolve('@fesjs/runtime/package.json'))
);

View File

@ -0,0 +1,60 @@
import { lodash, winPath } from '@fesjs/utils';
import assert from 'assert';
import path from 'path';
const reserveLibrarys = ['fes']; // reserve library
// todo 插件导出内容冲突问题待解决
const reserveExportsNames = [
'Link',
'NavLink',
'Redirect',
'dynamic',
'withRouter',
'Route'
];
export default function generateExports(basePath, { item, fesExportsHook }) {
assert(item.source, 'source should be supplied.');
const source = path.relative(path.basename(basePath), item.source);
assert(
item.exportAll || item.specifiers,
'exportAll or specifiers should be supplied.'
);
assert(
!reserveLibrarys.includes(source),
`${source} is reserve library, Please don't use it.`
);
if (item.exportAll) {
return `export * from '${winPath(source)}';`;
}
assert(
Array.isArray(item.specifiers),
`specifiers should be Array, but got ${item.specifiers.toString()}.`
);
const specifiersStrArr = item.specifiers.map((specifier) => {
if (typeof specifier === 'string') {
assert(
!reserveExportsNames.includes(specifier),
`${specifier} is reserve name, you can use 'exported' to set alias.`
);
assert(
!fesExportsHook[specifier],
`${specifier} is Defined, you can use 'exported' to set alias.`
);
fesExportsHook[specifier] = true;
return specifier;
}
assert(
lodash.isPlainObject(specifier),
`Configure item context should be Plain Object, but got ${specifier}.`
);
assert(
specifier.local && specifier.exported,
'local and exported should be supplied.'
);
return `${specifier.local} as ${specifier.exported}`;
});
return `export { ${specifiersStrArr.join(', ')} } from '${winPath(
source
)}';`;
}

View File

@ -8,7 +8,7 @@ export default async ({ api, watch }) => {
api.logger.debug('generate files'); api.logger.debug('generate files');
await api.applyPlugins({ await api.applyPlugins({
key: 'onGenerateFiles', key: 'onGenerateFiles',
type: api.ApplyPluginsType.event type: api.ApplyPluginsType.event,
}); });
} }
@ -27,13 +27,13 @@ export default async ({ api, watch }) => {
const watcher = chokidar.watch(path, { const watcher = chokidar.watch(path, {
// ignore .dotfiles and _mock.js // ignore .dotfiles and _mock.js
ignored: /(^|[/\\])(_mock.js$|\..)/, ignored: /(^|[/\\])(_mock.js$|\..)/,
ignoreInitial: true ignoreInitial: true,
}); });
watcher.on( watcher.on(
'all', 'all',
lodash.throttle(async () => { lodash.throttle(async () => {
await generate(); await generate();
}, 100) }, 100),
); );
watchers.push(watcher); watchers.push(watcher);
} }
@ -42,16 +42,11 @@ export default async ({ api, watch }) => {
const watcherPaths = await api.applyPlugins({ const watcherPaths = await api.applyPlugins({
key: 'addTmpGenerateWatcherPaths', key: 'addTmpGenerateWatcherPaths',
type: api.ApplyPluginsType.add, type: api.ApplyPluginsType.add,
initialValue: [ initialValue: [paths.absPagesPath, getAppPath(paths.absSrcPath)],
paths.absPagesPath, });
getAppPath(paths.absSrcPath) lodash.uniq(watcherPaths.map((p) => winPath(p))).forEach((p) => {
] createWatcher(p);
}); });
lodash
.uniq(watcherPaths.map(p => winPath(p)))
.forEach((p) => {
createWatcher(p);
});
} }
return unwatch; return unwatch;

View File

@ -1,19 +1,11 @@
import { import { dirname, join, basename, relative, extname } from 'path';
dirname, join, basename, relative, extname import { compatESModuleRequire, resolve, winPath, pkgUp, lodash } from '@fesjs/utils';
} from 'path';
import {
compatESModuleRequire,
resolve,
winPath,
pkgUp,
lodash
} from '@fesjs/utils';
import { PluginType } from '../enums'; import { PluginType } from '../enums';
const RE = { const RE = {
[PluginType.plugin]: /^(@fesjs\/|@webank\/fes-|fes-)plugin-/, [PluginType.plugin]: /^(@fesjs\/|@webank\/fes-|fes-)plugin-/,
[PluginType.preset]: /^(@fesjs\/|@webank\/fes-|fes-)preset-/ [PluginType.preset]: /^(@fesjs\/|@webank\/fes-|fes-)preset-/,
}; };
export function isPluginOrPreset(type, name) { export function isPluginOrPreset(type, name) {
@ -25,25 +17,36 @@ export function isPluginOrPreset(type, name) {
return re.test(name); return re.test(name);
} }
function filterPluginAndPreset(type, pkg) {
return Object.keys(pkg.devDependencies || {})
.concat(Object.keys(pkg.dependencies || {}))
.filter(isPluginOrPreset.bind(null, type));
}
function filterBuilder(pkg) {
return Object.keys(pkg.devDependencies || {})
.concat(Object.keys(pkg.dependencies || {}))
.filter((name) => /^@fesjs\/build-/.test(name));
}
export function getPluginsOrPresets(type, opts) { export function getPluginsOrPresets(type, opts) {
const upperCaseType = type.toUpperCase(); const upperCaseType = type.toUpperCase();
return [ return [
// dependencies
// opts // opts
...((opts[type === PluginType.preset ? 'presets' : 'plugins']) || []), ...(opts[type === PluginType.preset ? 'presets' : 'plugins'] || []),
// env // env
...(process.env[`FES_${upperCaseType}S`] || '').split(',').filter(Boolean), ...(process.env[`FES_${upperCaseType}S`] || '').split(',').filter(Boolean),
...Object.keys(opts.pkg.devDependencies || {}) ...filterPluginAndPreset(type, opts.pkg),
.concat(Object.keys(opts.pkg.dependencies || {})) // 构建只允许是 presets
.filter(isPluginOrPreset.bind(null, type)), ...(type === PluginType.preset ? filterBuilder(opts.pkg) : []),
// user config // user config
...((opts[ ...(opts[type === PluginType.preset ? 'userConfigPresets' : 'userConfigPlugins'] || []),
type === PluginType.preset ? 'userConfigPresets' : 'userConfigPlugins' ].map((path) =>
]) || []) resolve.sync(path, {
].map(path => resolve.sync(path, { basedir: opts.cwd,
basedir: opts.cwd, extensions: ['.js', '.ts'],
extensions: ['.js', '.ts'] }),
})); );
} }
// e.g. // e.g.
@ -52,7 +55,7 @@ export function getPluginsOrPresets(type, opts) {
function nameToKey(name) { function nameToKey(name) {
return name return name
.split('.') .split('.')
.map(part => lodash.camelCase(part)) .map((part) => lodash.camelCase(part))
.join('.'); .join('.');
} }
@ -70,8 +73,7 @@ export function pathToObj({ path, type, cwd }) {
if (pkgJSONPath) { if (pkgJSONPath) {
// eslint-disable-next-line // eslint-disable-next-line
pkg = require(pkgJSONPath); pkg = require(pkgJSONPath);
isPkgPlugin = winPath(join(dirname(pkgJSONPath), pkg.main || 'index.js')) isPkgPlugin = winPath(join(dirname(pkgJSONPath), pkg.main || 'index.js')) === winPath(path);
=== winPath(path);
} }
let id; let id;
@ -87,9 +89,7 @@ export function pathToObj({ path, type, cwd }) {
id = id.replace('@fesjs/preset-built-in/lib/plugins', '@@'); id = id.replace('@fesjs/preset-built-in/lib/plugins', '@@');
id = id.replace(/\.js$/, ''); id = id.replace(/\.js$/, '');
const key = isPkgPlugin const key = isPkgPlugin ? pkgNameToKey(pkg.name, type) : nameToKey(basename(path, extname(path)));
? pkgNameToKey(pkg.name, type)
: nameToKey(basename(path, extname(path)));
return { return {
id, id,
@ -106,28 +106,32 @@ export function pathToObj({ path, type, cwd }) {
throw new Error(`Register ${path} failed, since ${e.message}`); throw new Error(`Register ${path} failed, since ${e.message}`);
} }
}, },
defaultConfig: null defaultConfig: null,
}; };
} }
export function resolvePresets(opts) { export function resolvePresets(opts) {
const type = PluginType.preset; const type = PluginType.preset;
const presets = [...getPluginsOrPresets(type, opts)]; const presets = [...getPluginsOrPresets(type, opts)];
return presets.map(path => pathToObj({ return presets.map((path) =>
type, pathToObj({
path, type,
cwd: opts.cwd path,
})); cwd: opts.cwd,
}),
);
} }
export function resolvePlugins(opts) { export function resolvePlugins(opts) {
const type = PluginType.plugin; const type = PluginType.plugin;
const plugins = getPluginsOrPresets(type, opts); const plugins = getPluginsOrPresets(type, opts);
return plugins.map(path => pathToObj({ return plugins.map((path) =>
path, pathToObj({
type, path,
cwd: opts.cwd type,
})); cwd: opts.cwd,
}),
);
} }
export function isValidPlugin(plugin) { export function isValidPlugin(plugin) {

View File

@ -25,59 +25,14 @@
"access": "public" "access": "public"
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.12.13",
"@babel/plugin-proposal-do-expressions": "^7.12.13",
"@babel/plugin-proposal-export-default-from": "^7.12.13",
"@babel/plugin-proposal-function-bind": "^7.12.13",
"@babel/plugin-proposal-pipeline-operator": "^7.12.13",
"@babel/plugin-transform-runtime": "^7.12.13",
"@babel/preset-env": "^7.12.13",
"@babel/preset-typescript": "^7.15.0",
"@fesjs/compiler": "^2.0.5", "@fesjs/compiler": "^2.0.5",
"@fesjs/utils": "^2.0.4", "@fesjs/utils": "^2.0.4",
"@soda/friendly-errors-webpack-plugin": "^1.8.0",
"@vitejs/plugin-vue": "^2.2.4",
"@vitejs/plugin-vue-jsx": "^1.3.8",
"@vue/babel-plugin-jsx": "^1.0.2",
"autoprefixer": "^10.2.4",
"babel-loader": "^8.2.2",
"babel-plugin-import": "1.13.3",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"cli-highlight": "^2.1.4",
"cliui": "7.0.4",
"connect-history-api-fallback": "^1.6.0",
"cookie-parser": "^1.4.5", "cookie-parser": "^1.4.5",
"copy-webpack-plugin": "^7.0.0",
"core-js": "^3.8.3",
"css-loader": "^5.0.1",
"css-minimizer-webpack-plugin": "^3.0.0",
"envinfo": "^7.7.3", "envinfo": "^7.7.3",
"file-loader": "^6.2.0", "mockjs": "^1.1.0"
"html-webpack-plugin": "^5.0.0",
"html-webpack-tags-plugin": "^3.0.0",
"less": "3.9.0",
"less-loader": "^8.0.0",
"mini-css-extract-plugin": "^1.3.5",
"mockjs": "^1.1.0",
"postcss": "8.3.0",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^4.2.0",
"postcss-safe-parser": "^5.0.2",
"qs": "^6.10.2",
"raw-loader": "^4.0.2",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"vite": "^2.8.6",
"vue-loader": "^16.1.2",
"webpack": "^5.24.2",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-chain": "^6.5.1",
"webpack-dev-server": "^3.11.2",
"webpackbar": "^5.0.0-3"
}, },
"peerDependencies": { "peerDependencies": {
"@vue/compiler-sfc": "^3.0.5", "@vue/compiler-sfc": "^3.0.5"
"core-js": "^3.8.3" }
}, }
"devDependencies": {}
}

View File

@ -11,50 +11,22 @@ export default function () {
require.resolve('./plugins/generateFiles/fes'), require.resolve('./plugins/generateFiles/fes'),
// bundle configs // bundle configs
require.resolve('./plugins/features/alias'),
require.resolve('./plugins/features/analyze'),
require.resolve('./plugins/features/autoprefixer'),
require.resolve('./plugins/features/base'), require.resolve('./plugins/features/base'),
require.resolve('./plugins/features/chainWebpack'),
require.resolve('./plugins/features/cssLoader'),
require.resolve('./plugins/features/copy'),
require.resolve('./plugins/features/checkVuePackage'), require.resolve('./plugins/features/checkVuePackage'),
require.resolve('./plugins/features/define'),
require.resolve('./plugins/features/devServer'),
require.resolve('./plugins/features/devtool'),
require.resolve('./plugins/features/dynamicImport'), require.resolve('./plugins/features/dynamicImport'),
require.resolve('./plugins/features/externals'),
require.resolve('./plugins/features/exportStatic'),
require.resolve('./plugins/features/extraBabelPlugins'),
require.resolve('./plugins/features/extraBabelPresets'),
require.resolve('./plugins/features/extraPostCSSPlugins'),
require.resolve('./plugins/features/html'),
require.resolve('./plugins/features/globalCSS'), require.resolve('./plugins/features/globalCSS'),
require.resolve('./plugins/features/inlineLimit'),
require.resolve('./plugins/features/lessLoader'),
require.resolve('./plugins/features/mountElementId'), require.resolve('./plugins/features/mountElementId'),
require.resolve('./plugins/features/mock'), require.resolve('./plugins/features/mock'),
require.resolve('./plugins/features/outputPath'),
require.resolve('./plugins/features/plugins'), require.resolve('./plugins/features/plugins'),
require.resolve('./plugins/features/postcssLoader'),
require.resolve('./plugins/features/proxy'), require.resolve('./plugins/features/proxy'),
require.resolve('./plugins/features/publicPath'),
require.resolve('./plugins/features/runtimePublicPath'),
require.resolve('./plugins/features/singular'), require.resolve('./plugins/features/singular'),
require.resolve('./plugins/features/targets'),
require.resolve('./plugins/features/terserOptions'),
require.resolve('./plugins/features/nodeModulesTransform'),
require.resolve('./plugins/features/vueLoader'),
// misc // route
require.resolve('./plugins/misc/route'), require.resolve('./plugins/route'),
// commands // commands
require.resolve('./plugins/commands/build'),
require.resolve('./plugins/commands/dev'),
require.resolve('./plugins/commands/help'), require.resolve('./plugins/commands/help'),
require.resolve('./plugins/commands/info'), require.resolve('./plugins/commands/info'),
require.resolve('./plugins/commands/webpack') ],
]
}; };
} }

View File

@ -1,227 +0,0 @@
/**
* @copy 该文件代码大部分出自 umi有需要请参考
* https://github.com/umijs/umi/blob/master/packages/preset-built-in/src/plugins/commands/dev/dev.ts
*/
import { createServer } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import SFCConfigBlockPlugin from './SFCConfigBlockPlugin';
const assert = require('assert');
export default (api) => {
const {
env,
paths,
utils: { chalk, portfinder },
} = api;
const unwatchs = [];
let port;
let hostname;
let server;
function destroy() {
for (const unwatch of unwatchs) {
unwatch();
}
server?.close();
}
api.registerCommand({
command: 'dev',
description: 'start a local http service for development',
options: [
{
name: '--port',
description: 'http service port, like 8080',
},
{
name: '--https',
description: 'whether to turn on the https service',
},
],
async fn({ args = {} }) {
const { cleanTmpPathExceptCache } = require('../buildDevUtils');
const generateFiles = require('../../../utils/generateFiles').default;
const { watchPkg } = require('./watchPkg');
const defaultPort = process.env.PORT || args.port || api.config.devServer?.port;
port = await portfinder.getPortPromise({
port: defaultPort ? parseInt(String(defaultPort), 10) : 8000,
});
hostname = process.env.HOST || api.config.devServer?.host || 'localhost';
process.send({
type: 'UPDATE_PORT',
port,
});
// enable https
// const isHTTPS = process.env.HTTPS || args.https;
cleanTmpPathExceptCache({
absTmpPath: paths.absTmpPath,
});
const watch = process.env.WATCH !== 'none';
// generate files
const unwatchGenerateFiles = await generateFiles({
api,
watch,
});
if (unwatchGenerateFiles) unwatchs.push(unwatchGenerateFiles);
if (watch) {
// watch pkg changes
const unwatchPkg = watchPkg({
cwd: api.cwd,
onChange() {
console.log();
api.logger.info('Plugins in package.json changed.');
api.restartServer();
},
});
unwatchs.push(unwatchPkg);
// watch config change
const unwatchConfig = api.service.configInstance.watch({
userConfig: api.service.userConfig,
onChange: async ({ pluginChanged, valueChanged }) => {
if (pluginChanged.length) {
console.log();
api.logger.info(`Plugins of ${pluginChanged.map((p) => p.key).join(', ')} changed.`);
api.restartServer();
}
if (valueChanged.length) {
let reload = false;
let regenerateTmpFiles = false;
const fns = [];
const reloadConfigs = [];
valueChanged.forEach(({ key, pluginId }) => {
const { onChange } = api.service.plugins[pluginId].config || {};
if (onChange === api.ConfigChangeType.regenerateTmpFiles) {
regenerateTmpFiles = true;
}
if (!onChange || onChange === api.ConfigChangeType.reload) {
reload = true;
reloadConfigs.push(key);
}
if (typeof onChange === 'function') {
fns.push(onChange);
}
});
if (reload) {
console.log();
api.logger.info(`Config ${reloadConfigs.join(', ')} changed.`);
api.restartServer();
} else {
api.service.userConfig = api.service.configInstance.getUserConfig();
await api.setConfig();
if (regenerateTmpFiles) {
await generateFiles({
api,
});
} else {
fns.forEach((fn) => fn());
}
}
}
},
});
unwatchs.push(unwatchConfig);
}
server = await createServer({
mode: 'development',
plugins: [vue(), SFCConfigBlockPlugin, vueJsx()],
configFile: false,
resolve: {
alias: {
'@': paths.absSrcPath,
'@@': paths.absTmpPath,
'@fesInner': '/',
},
},
optimizeDeps: {
// exclude: ['@fesjs/fes'],
},
server: {
port: 8000,
},
});
await server.listen();
server.printUrls();
// dev
// const { bundleConfig } = await getBundleAndConfigs({ api });
// const beforeMiddlewares = await api.applyPlugins({
// key: 'addBeforeMiddlewares',
// type: api.ApplyPluginsType.add,
// initialValue: [],
// args: {}
// });
// const middlewares = await api.applyPlugins({
// key: 'addMiddlewares',
// type: api.ApplyPluginsType.add,
// initialValue: [],
// args: {}
// });
// const { startDevServer } = require('./devServer');
// server = startDevServer({
// webpackConfig: bundleConfig,
// host: hostname,
// port,
// proxy: api.config.proxy,
// https: isHTTPS,
// beforeMiddlewares: [...beforeMiddlewares, createRouteMiddleware(api)],
// afterMiddlewares: [...middlewares],
// customerDevServerConfig: api.config.devServer
// });
return {
destroy,
};
},
});
api.registerMethod({
name: 'getPort',
fn() {
assert(env === 'development', 'api.getPort() is only valid in development.');
return port;
},
});
api.registerMethod({
name: 'getHostname',
fn() {
assert(env === 'development', 'api.getHostname() is only valid in development.');
return hostname;
},
});
api.registerMethod({
name: 'getServer',
fn() {
assert(env === 'development', 'api.getServer() is only valid in development.');
return server;
},
});
api.registerMethod({
name: 'restartServer',
fn() {
console.log(chalk.gray('Try to restart dev server...'));
destroy();
process.send({
type: 'RESTART',
});
},
});
};

View File

@ -1,11 +1,9 @@
export default function (api) { export default function (api) {
api.registerCommand({ api.registerCommand({
command: 'help', command: 'help',
description: 'show command helps', description: 'show command helps',
async fn({ program }) { async fn({ program }) {
program.outputHelp(); program.outputHelp();
} },
}); });
} }

View File

@ -1,24 +1,24 @@
export default function (api) { export default function (api) {
api.registerCommand({ api.registerCommand({
command: 'info', command: 'info',
description: 'print debugging information about your environment', description: 'print debugging information about your environment',
async fn() { async fn() {
return require('envinfo').run( return require('envinfo')
{ .run(
System: ['OS', 'CPU'], {
Binaries: ['Node', 'Yarn', 'npm'], System: ['OS', 'CPU'],
Browsers: ['Chrome', 'Edge', 'Firefox', 'Safari'], Binaries: ['Node', 'Yarn', 'npm'],
npmPackages: ['@fesjs/fes', 'vue', 'vue-router'], Browsers: ['Chrome', 'Edge', 'Firefox', 'Safari'],
npmGlobalPackages: ['@fesjs/fes'] npmPackages: ['@fesjs/fes', 'vue', 'vue-router'],
}, npmGlobalPackages: ['@fesjs/fes'],
{ },
showNotFound: true, {
duplicates: true, showNotFound: true,
fullTree: true duplicates: true,
} fullTree: true,
) },
)
.then(console.log); .then(console.log);
} },
}); });
} }

View File

@ -1,50 +0,0 @@
export default function (api) {
api.registerCommand({
command: 'webpack',
description: 'inspect webpack configurations',
options: [{
name: '--rule <ruleName>',
description: 'inspect a specific module rule'
}, {
name: '--plugin <pluginName>',
description: 'inspect a specific plugin'
}, {
name: '--rules',
description: 'list all module rule names'
}, {
name: '--plugins',
description: 'list all plugin names'
}, {
name: '--verbose',
description: 'show full function definitions in output'
}],
async fn({ options }) {
const assert = require('assert');
const { getBundleAndConfigs } = require('../buildDevUtils');
const { toString } = require('webpack-chain');
const { highlight } = require('cli-highlight');
const { bundleConfig } = await getBundleAndConfigs({ api });
let config = bundleConfig;
assert(config, 'No valid config found with fes entry.');
if (options.rule) {
config = config.module.rules.find(
r => r.__ruleNames[0] === options.rule
);
} else if (options.plugin) {
config = config.plugins.find(
p => p.__pluginName === options.plugin
);
} else if (options.rules) {
config = config.module.rules.map(r => r.__ruleNames[0]);
} else if (options.plugins) {
config = config.plugins.map(
p => p.__pluginName || p.constructor.name
);
}
console.log(highlight(toString(config, { verbose: options.verbose }), { language: 'js' }));
}
});
}

View File

@ -1,133 +0,0 @@
// css less post-css mini-css css 压缩
// extraPostCSSPlugins
// postcssLoader
// lessLoader
// css-loader
// 支持 热加载
// 性能优化
// css 压缩 https://github.com/webpack-contrib/css-minimizer-webpack-plugin
// 根据 entry 进行代码块拆分
// 根据 entry 将文件输出到不同的文件夹
import { deepmerge } from '@fesjs/utils';
function createRules({
isDev,
webpackConfig,
config,
lang,
test,
loader,
options,
browserslist,
styleLoaderOption
}) {
function applyLoaders(rule, isCSSModules) {
if (isDev) {
rule.use('extra-css-loader')
.loader(require.resolve('style-loader'))
.options(Object.assign({}, styleLoaderOption));
} else {
rule.use('extra-css-loader')
.loader(require('mini-css-extract-plugin').loader)
.options({
});
}
rule.use('css-loader')
.loader(require.resolve('css-loader'))
.options(
deepmerge(
{
importLoaders: 1,
// https://webpack.js.org/loaders/css-loader/#onlylocals
...(isCSSModules
? {
modules: {
localIdentName: '[local]___[hash:base64:5]'
}
}
: {})
},
config.cssLoader || {}
)
);
rule.use('postcss-loader')
.loader(require.resolve('postcss-loader'))
.options(deepmerge({
postcssOptions: () => ({
plugins: [
// https://github.com/luisrudge/postcss-flexbugs-fixes
require('postcss-flexbugs-fixes'),
require('postcss-safe-parser'),
[require('autoprefixer'), { ...config.autoprefixer, overrideBrowserslist: browserslist }],
...(config.extraPostCSSPlugins ? config.extraPostCSSPlugins : [])
]
})
}, config.postcssLoader || {}));
if (loader) {
rule.use(loader)
.loader(require.resolve(loader))
.options(options);
}
}
const rule = webpackConfig.module.rule(lang).test(test);
applyLoaders(rule.oneOf('css-modules').resourceQuery(/module/), true);
applyLoaders(rule.oneOf('css'), false);
}
export default function createCssWebpackConfig({
isDev,
config,
webpackConfig,
browserslist
}) {
createRules({
isDev,
webpackConfig,
config,
lang: 'css',
test: /\.css$/,
browserslist
});
createRules({
isDev,
webpackConfig,
config,
lang: 'less',
test: /\.less$/,
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true,
...config.lessLoader
}
},
browserslist
});
if (!isDev) {
webpackConfig.plugin('extra-css')
.use(require.resolve('mini-css-extract-plugin'), [{
filename: '[name].[contenthash:8].css',
chunkFilename: '[id].[contenthash:8].css'
}]);
webpackConfig.optimization
.minimizer('css')
.use(require.resolve('css-minimizer-webpack-plugin'), [{}]);
}
return (options) => {
createRules({
isDev,
config,
webpackConfig,
browserslist,
...options
});
};
}

View File

@ -1,12 +0,0 @@
import webpack from 'webpack';
import resolveDefine from './resolveDefine';
export default function createDefineWebpackConfig({
config,
webpackConfig
}) {
webpackConfig.plugin('define')
.use(webpack.DefinePlugin, [
resolveDefine(config)
]);
}

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="<%= htmlWebpackPlugin.options.mountElementId %>"></div>
</body>
</html>

View File

@ -1,13 +0,0 @@
import qs from 'qs';
const pitcher = code => code;
export const pitch = function () {
const context = this;
const query = qs.parse(context.resourceQuery.slice(1));
if (query.type === 'custom' && query.blockType === 'config') {
return '';
}
};
export default pitcher;

View File

@ -1,16 +0,0 @@
export default (api) => {
api.describe({
key: 'autoprefixer',
config: {
default: {
flexbox: 'no-2009'
},
schema(joi) {
return joi
.object()
.description('postcss autoprefixer, default flexbox: no-2009');
}
}
});
};

View File

@ -1,4 +1,3 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'base', key: 'base',
@ -6,7 +5,7 @@ export default (api) => {
default: '', default: '',
schema(joi) { schema(joi) {
return joi.string().allow(''); return joi.string().allow('');
} },
} },
}); });
}; };

View File

@ -7,22 +7,19 @@ export default (api) => {
schema(joi) { schema(joi) {
return joi.object(); return joi.object();
}, },
default: { default: {},
} },
}
}); });
api.onStart(() => { api.onStart(() => {
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
const vuePkg = require('vue/package.json'); const vuePkg = require('vue/package.json');
const vueCompilerPkg = require('@vue/compiler-sfc/package.json'); const vueCompilerPkg = require('@vue/compiler-sfc/package.json');
if ( if (!semver.satisfies(vuePkg.version, `~${vueCompilerPkg.version.replace(/\d+$/, '0')}`, { includePrerelease: true })) {
!semver.satisfies(vuePkg.version, `~${vueCompilerPkg.version.replace(/\d+$/, '0')}`, { includePrerelease: true })
) {
console.log( console.log(
chalk.red( chalk.red(
`You are using vue@${vuePkg.version}, requires @vue/compiler-sfc@${vuePkg.version}.\nPlease upgrade your @vue/compiler-sfc@${vueCompilerPkg.version} version.` `You are using vue@${vuePkg.version}, requires @vue/compiler-sfc@${vuePkg.version}.\nPlease upgrade your @vue/compiler-sfc@${vueCompilerPkg.version} version.`,
) ),
); );
process.exit(1); process.exit(1);
} }

View File

@ -1,37 +0,0 @@
export default (api) => {
api.describe({
key: 'cssLoader',
config: {
default: {},
schema(joi) {
return joi
.object({
url: joi.alternatives(joi.boolean(), joi.function()),
import: joi.alternatives(joi.boolean(), joi.function()),
modules: joi.alternatives(
joi.boolean(),
joi.string(),
joi.object()
),
sourceMap: joi.boolean(),
importLoaders: joi.number(),
onlyLocals: joi.boolean(),
esModule: joi.boolean(),
localsConvention: joi
.string()
.valid(
'asIs',
'camelCase',
'camelCaseOnly',
'dashes',
'dashesOnly'
)
})
.description(
'more css-loader options see https://webpack.js.org/loaders/css-loader/#options'
);
}
}
});
};

View File

@ -1,12 +1,11 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'dynamicImport', key: 'dynamicImport',
config: { config: {
schema(joi) { schema(joi) {
return joi.boolean(); return joi.boolean();
} },
}, },
default: false default: false,
}); });
}; };

View File

@ -4,25 +4,14 @@ import { existsSync } from 'fs';
export default (api) => { export default (api) => {
const { const {
paths, paths,
utils: { winPath } utils: { winPath },
} = api; } = api;
const { absSrcPath = '', absTmpPath = '' } = paths; const { absSrcPath = '', absTmpPath = '' } = paths;
const files = [ const files = ['global.css', 'global.less', 'global.scss', 'global.sass', 'global.styl', 'global.stylus'];
'global.css',
'global.less',
'global.scss',
'global.sass',
'global.styl',
'global.stylus'
];
const globalCSSFile = files const globalCSSFile = files
.map(file => join(absSrcPath || '', file)) .map((file) => join(absSrcPath || '', file))
.filter(file => existsSync(file)) .filter((file) => existsSync(file))
.slice(0, 1); .slice(0, 1);
api.addEntryCodeAhead( api.addEntryCodeAhead(() => `${globalCSSFile.map((file) => `require('${winPath(relative(absTmpPath, file))}');`).join('')}`);
() => `${globalCSSFile
.map(file => `require('${winPath(relative(absTmpPath, file))}');`)
.join('')}`
);
}; };

View File

@ -1,16 +0,0 @@
export default (api) => {
api.describe({
key: 'html',
config: {
schema(joi) {
return joi
.object()
.description(
'more html-webpack-plugin options see https://github.com/jantimon/html-webpack-plugin#configuration'
);
},
default: {
}
}
});
};

View File

@ -25,7 +25,7 @@ export default (api) => {
}); });
api.babelRegister.setOnlyMap({ api.babelRegister.setOnlyMap({
key: 'mock', key: 'mock',
value: [...paths, ...requireDeps] value: [...paths, ...requireDeps],
}); });
}; };
@ -34,9 +34,9 @@ export default (api) => {
config: { config: {
schema(joi) { schema(joi) {
return joi.alternatives(joi.boolean(), joi.object()); return joi.alternatives(joi.boolean(), joi.object());
} },
}, },
enableBy: () => process.env.NODE_ENV === 'development' enableBy: () => process.env.NODE_ENV === 'development',
}); });
// 对 array、object 遍历处理 // 对 array、object 遍历处理
@ -57,11 +57,11 @@ export default (api) => {
// 默认配置 // 默认配置
const option = { const option = {
headers: { headers: {
'Cache-Control': 'no-cache' 'Cache-Control': 'no-cache',
}, },
statusCode: 200, statusCode: 200,
cookies: [], cookies: [],
timeout: 0 timeout: 0,
}; };
if (len === 0) return option; if (len === 0) return option;
if (len === 1) { if (len === 1) {
@ -69,12 +69,9 @@ export default (api) => {
if (lodash.isPlainObject(newOption)) { if (lodash.isPlainObject(newOption)) {
traversalHandler(newOption, (value, key) => { traversalHandler(newOption, (value, key) => {
if (key === 'headers') { if (key === 'headers') {
traversalHandler( traversalHandler(newOption.headers, (headervalue, headerkey) => {
newOption.headers, option.headers[headerkey] = newOption.headers[headerkey];
(headervalue, headerkey) => { });
option.headers[headerkey] = newOption.headers[headerkey];
}
);
} else { } else {
option[key] = newOption[key]; option[key] = newOption[key];
} }
@ -107,9 +104,7 @@ export default (api) => {
// mock打开情况下配置的过滤前缀 // mock打开情况下配置的过滤前缀
const mockPrefixTemp = api.config.mock.prefix || mockPrefix; const mockPrefixTemp = api.config.mock.prefix || mockPrefix;
mockPrefix = mockPrefixTemp === mockPrefix mockPrefix = mockPrefixTemp === mockPrefix ? mockPrefixTemp : `${mockPrefixTemp}/`;
? mockPrefixTemp
: `${mockPrefixTemp}/`;
// mock文件处理 // mock文件处理
mockFile = parsePath('./mock.js'); mockFile = parsePath('./mock.js');
if (!existsSync(mockFile)) { if (!existsSync(mockFile)) {
@ -141,9 +136,7 @@ export default (api) => {
return next(); return next();
} }
// 请求以 cgiMock.prefix 开头,匹配处理 // 请求以 cgiMock.prefix 开头,匹配处理
const matchRequet = requestList.find( const matchRequet = requestList.find((item) => req.path.search(item.url) !== -1);
item => req.path.search(item.url) !== -1
);
if (!matchRequet) { if (!matchRequet) {
return next(); return next();
} }
@ -167,10 +160,7 @@ export default (api) => {
// do result // do result
if (lodash.isFunction(matchRequet.result)) { if (lodash.isFunction(matchRequet.result)) {
matchRequet.result(req, res); matchRequet.result(req, res);
} else if ( } else if (lodash.isArray(matchRequet.result) || lodash.isPlainObject(matchRequet.result)) {
lodash.isArray(matchRequet.result)
|| lodash.isPlainObject(matchRequet.result)
) {
!matchRequet.type && res.type('json'); !matchRequet.type && res.type('json');
res.json(matchRequet.result); res.json(matchRequet.result);
} else { } else {
@ -191,15 +181,13 @@ export default (api) => {
api.onStart(() => { api.onStart(() => {
// 获取mock配置: 是否打开 // 获取mock配置: 是否打开
mockFlag = lodash.isPlainObject(api.config.mock) mockFlag = lodash.isPlainObject(api.config.mock) ? true : api.config.mock;
? true
: api.config.mock;
if (!mockFlag) return; if (!mockFlag) return;
loadMock = createMock(); loadMock = createMock();
return chokidar return chokidar
.watch(mockFile, { .watch(mockFile, {
ignoreInitial: true ignoreInitial: true,
}) })
.on('change', () => { .on('change', () => {
api.logger.info('mock.js changedreload'); api.logger.info('mock.js changedreload');

View File

@ -1,4 +1,3 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'mountElementId', key: 'mountElementId',
@ -6,7 +5,7 @@ export default (api) => {
default: 'app', default: 'app',
schema(joi) { schema(joi) {
return joi.string().allow(''); return joi.string().allow('');
} },
} },
}); });
}; };

View File

@ -1,14 +0,0 @@
export default (api) => {
api.describe({
key: 'outputPath',
config: {
default: 'dist',
schema(joi) {
return joi
.string()
.not('src', 'public', 'pages', 'mock', 'config')
.allow('');
}
}
});
};

View File

@ -1,11 +1,10 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'plugins', key: 'plugins',
config: { config: {
schema(joi) { schema(joi) {
return joi.array().items(joi.string()); return joi.array().items(joi.string());
} },
} },
}); });
}; };

View File

@ -1,4 +1,3 @@
export default (api) => { export default (api) => {
api.describe({ api.describe({
key: 'proxy', key: 'proxy',
@ -8,7 +7,7 @@ export default (api) => {
}, },
schema(joi) { schema(joi) {
return joi.object(); return joi.object();
} },
} },
}); });
}; };

View File

@ -1,14 +0,0 @@
export default (api) => {
api.describe({
key: 'publicPath',
config: {
default: '/',
schema(joi) {
return joi
.string()
.regex(/\/$/)
.error(new Error('config.publicPath must end with /.'));
}
}
});
};

View File

@ -4,9 +4,8 @@ export default (api) => {
config: { config: {
default: false, default: false,
schema(joi) { schema(joi) {
return joi return joi.boolean();
.boolean(); },
} },
}
}); });
}; };

View File

@ -1,16 +0,0 @@
export default (api) => {
api.describe({
key: 'vueLoader',
config: {
schema(joi) {
return joi
.object({})
.description(
'more vue-loader options see https://vue-loader.vuejs.org/'
);
}
}
});
};

View File

@ -8,21 +8,23 @@ export default function (api) {
const coreExports = await api.applyPlugins({ const coreExports = await api.applyPlugins({
key: 'addCoreExports', key: 'addCoreExports',
type: api.ApplyPluginsType.add, type: api.ApplyPluginsType.add,
initialValue: [] initialValue: [],
}); });
const fesExportsHook = {}; // repeated definition const fesExportsHook = {}; // repeated definition
const absoluteFilePath = 'core/coreExports.js'; const absoluteFilePath = 'core/coreExports.js';
const content = `${coreExports const content = `${coreExports
.map(item => generateExports(absoluteFilePath, { .map((item) =>
item, generateExports(absoluteFilePath, {
fesExportsHook item,
})) fesExportsHook,
}),
)
.join('\n')}\n`; .join('\n')}\n`;
const tpl = readFileSync(join(__dirname, './coreExports.tpl'), 'utf-8'); const tpl = readFileSync(join(__dirname, './coreExports.tpl'), 'utf-8');
api.writeTmpFile({ api.writeTmpFile({
path: absoluteFilePath, path: absoluteFilePath,
content: tpl.replace('CORE_EXPORTS', content).replace('RUNTIME_PATH', runtimePath) content: tpl.replace('CORE_EXPORTS', content).replace('RUNTIME_PATH', runtimePath),
}); });
}); });
} }

View File

@ -15,8 +15,3 @@ export {
} from 'RUNTIME_PATH'; } from 'RUNTIME_PATH';
CORE_EXPORTS CORE_EXPORTS
// TODO 优化,放到合适的位置,不能放在 routes会造成循环依赖
export const defineRouteMeta = (param)=>{
return param
}

View File

@ -2,13 +2,11 @@ import { readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { winPath } from '@fesjs/utils'; import { winPath } from '@fesjs/utils';
import { runtimePath } from '../../../../utils/constants'; import { runtimePath } from '../../../../utils/constants';
import { getAppPath } from '../../../../utils/getAppEntryPath';
export default function (api) { export default function (api) {
const { const {
paths, paths,
utils: { Mustache } utils: { Mustache, getAppEntryPath },
} = api; } = api;
const absoluteFilePath = 'core/plugin.js'; const absoluteFilePath = 'core/plugin.js';
@ -32,42 +30,34 @@ export default function (api) {
// 修改histror // 修改histror
'modifyCreateHistroy', 'modifyCreateHistroy',
// 生成router时触发 // 生成router时触发
'onRouterCreated' 'onRouterCreated',
] ],
}); });
const plugins = await api.applyPlugins({ const plugins = await api.applyPlugins({
key: 'addRuntimePlugin', key: 'addRuntimePlugin',
type: api.ApplyPluginsType.add, type: api.ApplyPluginsType.add,
initialValue: [ initialValue: [getAppEntryPath(paths.absSrcPath)].filter(Boolean),
getAppPath(paths.absSrcPath)
].filter(Boolean)
}); });
api.writeTmpFile({ api.writeTmpFile({
path: absoluteFilePath, path: absoluteFilePath,
content: Mustache.render( content: Mustache.render(readFileSync(join(__dirname, 'plugin.tpl'), 'utf-8'), {
readFileSync(join(__dirname, 'plugin.tpl'), 'utf-8'), validKeys,
{ runtimePath,
validKeys, }),
runtimePath
}
)
}); });
api.writeTmpFile({ api.writeTmpFile({
path: 'core/pluginRegister.js', path: 'core/pluginRegister.js',
content: Mustache.render( content: Mustache.render(readFileSync(join(__dirname, 'pluginRegister.tpl'), 'utf-8'), {
readFileSync(join(__dirname, 'pluginRegister.tpl'), 'utf-8'), plugins: plugins.map((plugin, index) => ({
{ index,
plugins: plugins.map((plugin, index) => ({ path: winPath(plugin),
index, })),
path: winPath(plugin) }),
}))
}
)
}); });
}); });
api.addCoreExports(() => ({ api.addCoreExports(() => ({
specifiers: ['plugin'], specifiers: ['plugin'],
source: absoluteFilePath source: absoluteFilePath,
})); }));
} }

View File

@ -1,8 +1,6 @@
import assert from 'assert'; import assert from 'assert';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { import { existsSync, statSync, readFileSync, writeFileSync, copyFileSync } from 'fs';
existsSync, statSync, readFileSync, writeFileSync, copyFileSync
} from 'fs';
export default function (api) { export default function (api) {
[ [
@ -26,48 +24,31 @@ export default function (api) {
'modifyBabelPresetOpts', 'modifyBabelPresetOpts',
'chainWebpack', 'chainWebpack',
'addTmpGenerateWatcherPaths', 'addTmpGenerateWatcherPaths',
'modifyPublicPathStr' 'modifyPublicPathStr',
].forEach((name) => { ].forEach((name) => {
api.registerMethod({ name }); api.registerMethod({ name });
}); });
api.registerMethod({ api.registerMethod({
name: 'writeTmpFile', name: 'writeTmpFile',
fn({ fn({ path, content }) {
path, assert(api.stage >= api.ServiceStage.pluginReady, 'api.writeTmpFile() should not execute in register stage.');
content
}) {
assert(
api.stage >= api.ServiceStage.pluginReady,
'api.writeTmpFile() should not execute in register stage.'
);
const absPath = join(api.paths.absTmpPath, path); const absPath = join(api.paths.absTmpPath, path);
api.utils.mkdirp.sync(dirname(absPath)); api.utils.mkdirp.sync(dirname(absPath));
if (!existsSync(absPath) || readFileSync(absPath, 'utf-8') !== content) { if (!existsSync(absPath) || readFileSync(absPath, 'utf-8') !== content) {
writeFileSync(absPath, content, 'utf-8'); writeFileSync(absPath, content, 'utf-8');
} }
} },
}); });
api.registerMethod({ api.registerMethod({
name: 'copyTmpFiles', name: 'copyTmpFiles',
fn({ fn({ namespace, path, ignore }) {
namespace, path, ignore assert(api.stage >= api.ServiceStage.pluginReady, 'api.copyTmpFiles() should not execute in register stage.');
}) { assert(path, 'api.copyTmpFiles() should has param path');
assert( assert(namespace, 'api.copyTmpFiles() should has param namespace');
api.stage >= api.ServiceStage.pluginReady,
'api.copyTmpFiles() should not execute in register stage.'
);
assert(
path,
'api.copyTmpFiles() should has param path'
);
assert(
namespace,
'api.copyTmpFiles() should has param namespace'
);
const files = api.utils.glob.sync('**/*', { const files = api.utils.glob.sync('**/*', {
cwd: path cwd: path,
}); });
const base = join(api.paths.absTmpPath, namespace); const base = join(api.paths.absTmpPath, namespace);
files.forEach((file) => { files.forEach((file) => {
@ -79,13 +60,13 @@ export default function (api) {
if (statSync(source).isDirectory()) { if (statSync(source).isDirectory()) {
api.utils.mkdirp.sync(target); api.utils.mkdirp.sync(target);
} else if (Array.isArray(ignore)) { } else if (Array.isArray(ignore)) {
if (!ignore.some(pattern => new RegExp(pattern).test(file))) { if (!ignore.some((pattern) => new RegExp(pattern).test(file))) {
copyFileSync(source, target); copyFileSync(source, target);
} }
} else { } else {
copyFileSync(source, target); copyFileSync(source, target);
} }
}); });
} },
}); });
} }

View File

@ -3,7 +3,7 @@ import { join, extname, posix, basename } from 'path';
import { lodash, parser, generator } from '@fesjs/utils'; import { lodash, parser, generator } from '@fesjs/utils';
import { parse } from '@vue/compiler-sfc'; import { parse } from '@vue/compiler-sfc';
import { Logger } from '@fesjs/compiler'; import { Logger } from '@fesjs/compiler';
import { runtimePath } from '../../../utils/constants'; import { runtimePath } from '../../utils/constants';
const logger = new Logger('fes:router'); const logger = new Logger('fes:router');
@ -314,6 +314,7 @@ export default function (api) {
const namespace = 'core/routes'; const namespace = 'core/routes';
const absCoreFilePath = join(namespace, 'routes.js'); const absCoreFilePath = join(namespace, 'routes.js');
const absExportsFilePath = join(namespace, 'routeExports.js');
const absRuntimeFilePath = join(namespace, 'runtime.js'); const absRuntimeFilePath = join(namespace, 'runtime.js');
@ -326,13 +327,19 @@ export default function (api) {
api.onGenerateFiles(async () => { api.onGenerateFiles(async () => {
const routesTpl = readFileSync(join(__dirname, 'template/routes.tpl'), 'utf-8'); const routesTpl = readFileSync(join(__dirname, 'template/routes.tpl'), 'utf-8');
const routes = await api.getRoutes(); const routes = await api.getRoutes();
api.writeTmpFile({ api.writeTmpFile({
path: absCoreFilePath, path: absCoreFilePath,
content: Mustache.render(routesTpl, { content: Mustache.render(routesTpl, {
runtimePath,
COMPONENTS_IMPORT: genComponentImportExpression(routes, api.config).join('\n'), COMPONENTS_IMPORT: genComponentImportExpression(routes, api.config).join('\n'),
routes: await api.getRoutesJSON(), routes: await api.getRoutesJSON(),
}),
});
const routeExportsTpl = readFileSync(join(__dirname, 'template/routeExports.tpl'), 'utf-8');
api.writeTmpFile({
path: absExportsFilePath,
content: Mustache.render(routeExportsTpl, {
runtimePath,
config: api.config, config: api.config,
routerBase: api.config.base, routerBase: api.config.base,
CREATE_HISTORY: historyType[api.config.router.mode] || 'createWebHashHistory', CREATE_HISTORY: historyType[api.config.router.mode] || 'createWebHashHistory',
@ -347,8 +354,8 @@ export default function (api) {
api.addCoreExports(() => [ api.addCoreExports(() => [
{ {
specifiers: ['getRoutes', 'getRouter', 'getHistory', 'destroyRouter'], specifiers: ['getRouter', 'getHistory', 'destroyRouter', 'defineRouteMeta'],
source: absCoreFilePath, source: absExportsFilePath,
}, },
]); ]);

View File

@ -1,13 +1,6 @@
import { createRouter as createVueRouter, {{{ CREATE_HISTORY }}}, ApplyPluginsType } from '{{{ runtimePath }}}'; import { createRouter as createVueRouter, {{{ CREATE_HISTORY }}}, ApplyPluginsType } from '{{{ runtimePath }}}';
import { plugin } from '../plugin'; import { plugin } from '../plugin';
{{{ COMPONENTS_IMPORT }}}
export function getRoutes() {
const routes = {{{ routes }}};
return routes;
}
const ROUTER_BASE = '{{{ routerBase }}}'; const ROUTER_BASE = '{{{ routerBase }}}';
let router = null; let router = null;
let history = null; let history = null;
@ -62,3 +55,7 @@ export const destroyRouter = ()=>{
router = null; router = null;
history = null; history = null;
} }
export const defineRouteMeta = (param)=>{
return param
}

View File

@ -0,0 +1,9 @@
{{{ COMPONENTS_IMPORT }}}
export function getRoutes() {
const routes = {{{ routes }}};
return routes;
}

View File

@ -1,4 +1,4 @@
import { createRouter } from "./routes"; import { createRouter } from "./routeExports";
export function onAppCreated({ app, routes }) { export function onAppCreated({ app, routes }) {
const router = createRouter(routes); const router = createRouter(routes);

View File

@ -1,6 +1,4 @@
import { winPath } from '@fesjs/utils'; import { winPath } from '@fesjs/utils';
import { dirname } from 'path'; import { dirname } from 'path';
export const runtimePath = winPath( export const runtimePath = winPath(dirname(require.resolve('@fesjs/runtime/package.json')));
dirname(require.resolve('@fesjs/runtime/package.json'))
);

View File

@ -3,58 +3,28 @@ import assert from 'assert';
import path from 'path'; import path from 'path';
const reserveLibrarys = ['fes']; // reserve library const reserveLibrarys = ['fes']; // reserve library
// todo 插件导出内容冲突问题待解决 // todo 插件导出内容冲突问题待解决
const reserveExportsNames = [ const reserveExportsNames = ['Link', 'NavLink', 'Redirect', 'dynamic', 'withRouter', 'Route'];
'Link',
'NavLink',
'Redirect',
'dynamic',
'withRouter',
'Route'
];
export default function generateExports(basePath, { item, fesExportsHook }) { export default function generateExports(basePath, { item, fesExportsHook }) {
assert(item.source, 'source should be supplied.'); assert(item.source, 'source should be supplied.');
const source = path.relative(path.basename(basePath), item.source); const source = path.relative(path.basename(basePath), item.source);
assert( assert(item.exportAll || item.specifiers, 'exportAll or specifiers should be supplied.');
item.exportAll || item.specifiers, assert(!reserveLibrarys.includes(source), `${source} is reserve library, Please don't use it.`);
'exportAll or specifiers should be supplied.'
);
assert(
!reserveLibrarys.includes(source),
`${source} is reserve library, Please don't use it.`
);
if (item.exportAll) { if (item.exportAll) {
return `export * from '${winPath(source)}';`; return `export * from '${winPath(source)}';`;
} }
assert( assert(Array.isArray(item.specifiers), `specifiers should be Array, but got ${item.specifiers.toString()}.`);
Array.isArray(item.specifiers),
`specifiers should be Array, but got ${item.specifiers.toString()}.`
);
const specifiersStrArr = item.specifiers.map((specifier) => { const specifiersStrArr = item.specifiers.map((specifier) => {
if (typeof specifier === 'string') { if (typeof specifier === 'string') {
assert( assert(!reserveExportsNames.includes(specifier), `${specifier} is reserve name, you can use 'exported' to set alias.`);
!reserveExportsNames.includes(specifier), assert(!fesExportsHook[specifier], `${specifier} is Defined, you can use 'exported' to set alias.`);
`${specifier} is reserve name, you can use 'exported' to set alias.`
);
assert(
!fesExportsHook[specifier],
`${specifier} is Defined, you can use 'exported' to set alias.`
);
fesExportsHook[specifier] = true; fesExportsHook[specifier] = true;
return specifier; return specifier;
} }
assert( assert(lodash.isPlainObject(specifier), `Configure item context should be Plain Object, but got ${specifier}.`);
lodash.isPlainObject(specifier), assert(specifier.local && specifier.exported, 'local and exported should be supplied.');
`Configure item context should be Plain Object, but got ${specifier}.`
);
assert(
specifier.local && specifier.exported,
'local and exported should be supplied.'
);
return `${specifier.local} as ${specifier.exported}`; return `${specifier.local} as ${specifier.exported}`;
}); });
return `export { ${specifiersStrArr.join(', ')} } from '${winPath( return `export { ${specifiersStrArr.join(', ')} } from '${winPath(source)}';`;
source
)}';`;
} }

View File

@ -34,15 +34,15 @@ export default {
devServer: { devServer: {
port: 8000 port: 8000
}, },
// windicss: { windicss: {
// config: { config: {
// theme: { theme: {
// extend: { extend: {
// colors: { colors: {
// green: '#7cb305' green: '#7cb305'
// } }
// } }
// } }
// } }
// } }
}; };

View File

@ -47,6 +47,8 @@
"@fesjs/fes": "^2.0.0", "@fesjs/fes": "^2.0.0",
"@fesjs/plugin-icon": "^2.0.0", "@fesjs/plugin-icon": "^2.0.0",
"@fesjs/plugin-request": "^2.0.0", "@fesjs/plugin-request": "^2.0.0",
"@fesjs/plugin-windicss": "^2.0.8",
"@fesjs/build-webpack": "^1.0.0",
"vue": "^3.2.2" "vue": "^3.2.2"
}, },
"private": true "private": true

View File

@ -1,12 +1,10 @@
import { defineRouteMeta, useRoute } from '@fesjs/fes'; import { defineRouteMeta, useRoute } from '@fesjs/fes';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
// console.log(defineRouteMeta); defineRouteMeta({
title: 'test',
// defineRouteMeta({ name: 'test',
// title: 'test', });
// name: 'test',
// });
export default defineComponent({ export default defineComponent({
setup() { setup() {

View File

@ -0,0 +1,55 @@
import * as chokidar from 'chokidar';
import lodash from 'lodash';
import winPath from './winPath';
import getAppPath from './getAppEntryPath';
export default async ({ api, watch }) => {
const { paths } = api;
async function generate() {
api.logger.debug('generate files');
await api.applyPlugins({
key: 'onGenerateFiles',
type: api.ApplyPluginsType.event,
});
}
let watchers = [];
await generate();
function unwatch() {
watchers.forEach((watcher) => {
watcher.close();
});
watchers = [];
}
function createWatcher(path) {
const watcher = chokidar.watch(path, {
// ignore .dotfiles and _mock.js
ignored: /(^|[/\\])(_mock.js$|\..)/,
ignoreInitial: true,
});
watcher.on(
'all',
lodash.throttle(async () => {
await generate();
}, 100),
);
watchers.push(watcher);
}
if (watch) {
const watcherPaths = await api.applyPlugins({
key: 'addTmpGenerateWatcherPaths',
type: api.ApplyPluginsType.add,
initialValue: [paths.absPagesPath, getAppPath(paths.absSrcPath)],
});
lodash.uniq(watcherPaths.map((p) => winPath(p))).forEach((p) => {
createWatcher(p);
});
}
return unwatch;
};

Some files were not shown because too many files have changed in this diff Show More