598 lines
21 KiB
JavaScript
598 lines
21 KiB
JavaScript
/**
|
|
* 把目录下所有的.ts和.less文件加入entry
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const fsExtra = require('fs-extra');
|
|
const globby = require('globby');
|
|
const { optimize, sources } = require('webpack');
|
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
// const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
|
|
// const NodeSourcePlugin = require('webpack/lib/node/NodeSourcePlugin');
|
|
// const JsonpTemplatePlugin = require('webpack/lib/web/JsonpTemplatePlugin');
|
|
const JavascriptModulesPlugin = require('webpack/lib/javascript/JavascriptModulesPlugin');
|
|
const EntryPlugin = require('webpack/lib/EntryPlugin');
|
|
const ensurePosix = require('ensure-posix-path');
|
|
const requiredPath = require('required-path');
|
|
|
|
const pluginName = 'OakWeChatMpPlugin';
|
|
const OakPagePrefix = '@oak-general-business';
|
|
const OakPagePath = 'node_modules/oak-general-business/wechatMp/';
|
|
|
|
function getIsOak(str) {
|
|
return str.indexOf(OakPagePrefix) === 0;
|
|
}
|
|
|
|
class OakWeChatMpPlugin {
|
|
constructor(options = {}) {
|
|
this.options = Object.assign(
|
|
{},
|
|
{
|
|
clear: true,
|
|
extensions: ['.js', '.ts'], // script ext
|
|
include: [], // include assets file
|
|
exclude: [], // ignore assets file
|
|
assetsChunkName: '__assets_chunk__',
|
|
commonsChunkName: 'commons',
|
|
vendorChunkName: 'vendor',
|
|
runtimeChunkName: 'runtime',
|
|
},
|
|
options
|
|
);
|
|
}
|
|
|
|
apply(compiler) {
|
|
this.setBasePath(compiler);
|
|
|
|
const catchError = (handler) => async (arg) => {
|
|
try {
|
|
await handler(arg);
|
|
} catch (err) {
|
|
console.warn(err);
|
|
}
|
|
};
|
|
this.firstClean = false;
|
|
|
|
compiler.hooks.run.tapPromise(
|
|
pluginName,
|
|
catchError((compiler) => this.setAppEntries(compiler))
|
|
);
|
|
|
|
compiler.hooks.watchRun.tapPromise(
|
|
pluginName,
|
|
catchError((compiler) => this.setAppEntries(compiler))
|
|
);
|
|
|
|
compiler.hooks.compilation.tap(
|
|
pluginName,
|
|
catchError((compilation) => this.compilationHooks(compilation))
|
|
);
|
|
|
|
compiler.hooks.emit.tapPromise(
|
|
pluginName,
|
|
catchError(async (compilation) => {
|
|
const { clear } = this.options;
|
|
if (clear && !this.firstClean) {
|
|
this.firstClean = true;
|
|
await OakWeChatMpPlugin.clearOutPut(compilation);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
compilationHooks(compilation) {
|
|
const { assetsChunkName } = this.options;
|
|
|
|
JavascriptModulesPlugin.getCompilationHooks(compilation).render.tap(
|
|
pluginName,
|
|
(source, { chunkGraph, chunk }) => {
|
|
// 非动态入口
|
|
const hasEntryModule =
|
|
chunkGraph.getNumberOfEntryModules(chunk) > 0;
|
|
if (!hasEntryModule) return source;
|
|
// 收集动态入口所有的依赖
|
|
let dependences = [];
|
|
[...chunk.groupsIterable].forEach((group) => {
|
|
[...group.chunks].forEach((chunk2) => {
|
|
const filename = ensurePosix(
|
|
path.relative(path.dirname(chunk.name), chunk2.name)
|
|
);
|
|
// console.log(filename)
|
|
if (
|
|
chunk === chunk2 ||
|
|
dependences.includes(filename)
|
|
) {
|
|
return;
|
|
}
|
|
dependences.push(filename);
|
|
});
|
|
});
|
|
// 没有依赖
|
|
if (dependences.length == 0) return;
|
|
// 源文件拼接依赖
|
|
const concatStr = dependences.reduce((prev, file) => {
|
|
prev += `require('${requiredPath(file)}');`;
|
|
return prev;
|
|
}, ';');
|
|
return new sources.ConcatSource(concatStr, source);
|
|
}
|
|
);
|
|
|
|
// splice assets module
|
|
compilation.hooks.beforeChunkAssets.tap(pluginName, () => {
|
|
let assetsChunk;
|
|
compilation.chunks.forEach((chunk) => {
|
|
if (chunk.name === assetsChunkName) {
|
|
assetsChunk = chunk;
|
|
}
|
|
});
|
|
if (assetsChunk) {
|
|
compilation.chunks.delete(assetsChunk);
|
|
}
|
|
});
|
|
|
|
compilation.hooks.processAssets.tapPromise(
|
|
{
|
|
name: pluginName,
|
|
stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
|
|
},
|
|
async (assets) => {
|
|
await this.emitAssetsFile(compilation);
|
|
}
|
|
);
|
|
}
|
|
|
|
async setAppEntries(compiler) {
|
|
this.npmComponents = new Set();
|
|
this.oakPages = new Set();
|
|
this.oakComponents = new Set();
|
|
this.appEntries = await this.resolveAppEntries(compiler);
|
|
await Promise.all([
|
|
this.addScriptEntry(compiler),
|
|
this.addAssetsEntries(compiler),
|
|
]);
|
|
this.applyPlugin(compiler);
|
|
}
|
|
|
|
// resolve tabbar page compoments
|
|
async resolveAppEntries(compiler) {
|
|
const {
|
|
tabBar = {},
|
|
pages = [],
|
|
subpackages = [],
|
|
} = fsExtra.readJSONSync(path.join(this.basePath, 'app.json'));
|
|
|
|
let tabBarAssets = new Set();
|
|
let components = new Set();
|
|
let subPageRoots = [];
|
|
let independentPageRoots = [];
|
|
let realPages = [];
|
|
this.subpackRoot = [];
|
|
|
|
for (const { iconPath, selectedIconPath } of tabBar.list || []) {
|
|
if (iconPath) {
|
|
tabBarAssets.add(iconPath);
|
|
}
|
|
if (selectedIconPath) {
|
|
tabBarAssets.add(selectedIconPath);
|
|
}
|
|
}
|
|
|
|
// parse subpage
|
|
for (const subPage of subpackages) {
|
|
subPageRoots.push(subPage.root);
|
|
if (subPage.independent) {
|
|
independentPageRoots.push(subPage.root);
|
|
}
|
|
for (const page of subPage.pages || []) {
|
|
realPages.push(path.join(subPage.root, page));
|
|
}
|
|
}
|
|
|
|
// add app.[ts/js]
|
|
realPages.push('app');
|
|
// resolve page components
|
|
for (const page of pages) {
|
|
let instance;
|
|
let isOak = getIsOak(page);
|
|
if (isOak) {
|
|
const oakPage = OakPagePath + page.replace(
|
|
new RegExp(OakPagePrefix),
|
|
'pages'
|
|
);
|
|
instance = path.resolve(process.cwd(), oakPage);
|
|
if (!this.oakPages.has(oakPage)) {
|
|
this.oakPages.add(oakPage);
|
|
realPages.push(oakPage);
|
|
}
|
|
} else {
|
|
realPages.push(page);
|
|
instance = path.resolve(this.basePath, page);
|
|
}
|
|
|
|
await this.getComponents(components, instance, isOak);
|
|
}
|
|
|
|
components = Array.from(components) || [];
|
|
tabBarAssets = Array.from(tabBarAssets) || [];
|
|
|
|
const ret = [...realPages, ...components];
|
|
|
|
Object.defineProperties(ret, {
|
|
pages: {
|
|
get: () => realPages,
|
|
},
|
|
components: {
|
|
get: () => components,
|
|
},
|
|
tabBarAssets: {
|
|
get: () => tabBarAssets,
|
|
},
|
|
subPageRoots: {
|
|
get: () => subPageRoots,
|
|
},
|
|
independentPageRoots: {
|
|
get: () => independentPageRoots,
|
|
},
|
|
});
|
|
return ret;
|
|
}
|
|
|
|
// parse components
|
|
async getComponents(components, instance, isOak) {
|
|
try {
|
|
const { usingComponents = {} } = fsExtra.readJSONSync(
|
|
`${instance}.json`
|
|
);
|
|
const instanceDir = path.parse(instance).dir;
|
|
for (const c of Object.values(usingComponents)) {
|
|
if (c.indexOf('plugin://') === 0) {
|
|
break;
|
|
}
|
|
if (c.indexOf('/miniprogram_npm') === 0) {
|
|
const component = c.replace(
|
|
/\/miniprogram_npm/,
|
|
'node_modules'
|
|
);
|
|
if (!this.npmComponents.has(component)) {
|
|
this.npmComponents.add(component);
|
|
components.add(component);
|
|
this.getComponents(
|
|
components,
|
|
path.resolve(process.cwd(), component)
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
if (isOak) {
|
|
const component = path
|
|
.resolve(instanceDir, c)
|
|
.replace(/\\/g, '/');
|
|
const component2 = component.replace(
|
|
process.cwd().replace(/\\/g, '/') + '/',
|
|
''
|
|
);
|
|
if (!this.oakComponents.has(component2)) {
|
|
this.oakComponents.add(component2);
|
|
components.add(component2);
|
|
await this.getComponents(
|
|
components,
|
|
path.resolve(process.cwd(), component2),
|
|
isOak
|
|
);
|
|
}
|
|
} else {
|
|
const component = path
|
|
.resolve(instanceDir, c)
|
|
.replace(/\\/g, '/');
|
|
if (!components.has(component)) {
|
|
// components.add(path.relative(this.basePath, component));
|
|
// await this.getComponents(components, component);
|
|
const component2 = path
|
|
.relative(this.basePath, component)
|
|
.replace(/\\/g, '/');
|
|
if (!components.has(component2)) {
|
|
components.add(component2);
|
|
await this.getComponents(components, component);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {}
|
|
}
|
|
|
|
// add script entry
|
|
async addScriptEntry(compiler) {
|
|
this.appEntries
|
|
.filter((resource) => resource !== 'app')
|
|
.forEach((resource) => {
|
|
if (this.npmComponents.has(resource)) {
|
|
new EntryPlugin(
|
|
this.basePath,
|
|
path.join(process.cwd(), resource),
|
|
resource.replace(/node_modules/, 'miniprogram_npm')
|
|
).apply(compiler);
|
|
} else if (this.oakPages.has(resource)) {
|
|
new EntryPlugin(
|
|
this.basePath,
|
|
path.join(process.cwd(), resource),
|
|
resource.replace(new RegExp(OakPagePath), '')
|
|
).apply(compiler);
|
|
} else if (this.oakComponents.has(resource)) {
|
|
new EntryPlugin(
|
|
this.basePath,
|
|
path.join(process.cwd(), resource),
|
|
resource.replace(new RegExp(OakPagePath), '')
|
|
).apply(compiler);
|
|
} else {
|
|
const fullPath = this.getFullScriptPath(resource);
|
|
if (fullPath) {
|
|
new EntryPlugin(
|
|
this.basePath,
|
|
fullPath,
|
|
resource
|
|
).apply(compiler);
|
|
} else {
|
|
console.warn(`file ${resource} is exists`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// add assets entry
|
|
async addAssetsEntries(compiler) {
|
|
const { include, exclude, extensions, assetsChunkName } = this.options;
|
|
const patterns = this.appEntries
|
|
.map((resource) => `${resource}.*`)
|
|
.concat(include);
|
|
const entries = await globby(patterns, {
|
|
cwd: this.basePath,
|
|
nodir: true,
|
|
realpath: true,
|
|
ignore: [...extensions.map((ext) => `**/*${ext}`), ...exclude],
|
|
dot: false,
|
|
});
|
|
|
|
this.assetsEntry = [...entries, ...this.appEntries.tabBarAssets];
|
|
this.assetsEntry.forEach((resource) => {
|
|
new EntryPlugin(
|
|
this.basePath,
|
|
path.resolve(this.basePath, resource),
|
|
assetsChunkName
|
|
).apply(compiler);
|
|
});
|
|
|
|
const oakPageAssetsEntry = await globby(
|
|
[...this.oakPages]
|
|
.map((resource) => `${path.parse(resource).dir}/**/*.*`)
|
|
.concat(include),
|
|
{
|
|
cwd: process.cwd(),
|
|
nodir: true,
|
|
realpath: true,
|
|
ignore: [...extensions.map((ext) => `**/*${ext}`), ...exclude],
|
|
dot: false,
|
|
}
|
|
);
|
|
const oakComponentAssetsEntry = await globby(
|
|
[...this.oakComponents]
|
|
.map((resource) => `${path.parse(resource).dir}/**/*.*`)
|
|
.concat(include),
|
|
{
|
|
cwd: process.cwd(),
|
|
nodir: true,
|
|
realpath: true,
|
|
ignore: [...extensions.map((ext) => `**/*${ext}`), ...exclude],
|
|
dot: false,
|
|
}
|
|
);
|
|
this.oakAssetsEntry = [
|
|
...oakPageAssetsEntry,
|
|
...oakComponentAssetsEntry,
|
|
];
|
|
this.oakAssetsEntry.forEach((resource) => {
|
|
new EntryPlugin(
|
|
this.basePath,
|
|
path.resolve(process.cwd(), resource),
|
|
assetsChunkName
|
|
).apply(compiler);
|
|
});
|
|
|
|
const npmAssetsEntry = await globby(
|
|
[...this.npmComponents]
|
|
.map((resource) => `${path.parse(resource).dir}/**/*.*`)
|
|
.concat(include),
|
|
{
|
|
cwd: process.cwd(),
|
|
nodir: true,
|
|
realpath: false,
|
|
ignore: [...extensions.map((ext) => `**/*${ext}`), ...exclude],
|
|
dot: false,
|
|
}
|
|
);
|
|
if (npmAssetsEntry.length > 0) {
|
|
new CopyWebpackPlugin({
|
|
patterns: [
|
|
...npmAssetsEntry.map((resource) => {
|
|
return {
|
|
from: path.resolve(
|
|
process.cwd().replace(/\\/g, '/'),
|
|
resource
|
|
),
|
|
to: resource.replace(
|
|
/node_modules/,
|
|
'miniprogram_npm'
|
|
),
|
|
globOptions: {
|
|
ignore: [
|
|
...extensions.map((ext) => `**/*${ext}`),
|
|
...exclude,
|
|
],
|
|
},
|
|
};
|
|
}),
|
|
],
|
|
}).apply(compiler);
|
|
}
|
|
}
|
|
|
|
// code splite
|
|
applyPlugin(compiler) {
|
|
const { runtimeChunkName, commonsChunkName, vendorChunkName } =
|
|
this.options;
|
|
const subpackRoots = this.appEntries.subPageRoots;
|
|
const independentPageRoots = this.appEntries.independentPageRoots;
|
|
|
|
new optimize.RuntimeChunkPlugin({
|
|
name({ name }) {
|
|
const index = independentPageRoots.findIndex((item) =>
|
|
name.includes(item)
|
|
);
|
|
if (index !== -1) {
|
|
return path.join(
|
|
independentPageRoots[index],
|
|
runtimeChunkName
|
|
);
|
|
}
|
|
return runtimeChunkName;
|
|
},
|
|
}).apply(compiler);
|
|
|
|
new optimize.SplitChunksPlugin({
|
|
hidePathInfo: false,
|
|
chunks: 'async',
|
|
minSize: 10000,
|
|
minChunks: 1,
|
|
maxAsyncRequests: Infinity,
|
|
automaticNameDelimiter: '~',
|
|
maxInitialRequests: Infinity,
|
|
name: true,
|
|
cacheGroups: {
|
|
default: false,
|
|
// node_modules
|
|
vendor: {
|
|
chunks: 'all',
|
|
test: /[\\/]node_modules[\\/]/,
|
|
name: vendorChunkName,
|
|
minChunks: 0,
|
|
},
|
|
// 其他公用代码
|
|
common: {
|
|
chunks: 'all',
|
|
test: /[\\/]src[\\/]/,
|
|
minChunks: 2,
|
|
name({ context }) {
|
|
const index = subpackRoots.findIndex((item) =>
|
|
context.includes(item)
|
|
);
|
|
if (index !== -1) {
|
|
return path.join(
|
|
subpackRoots[index],
|
|
commonsChunkName
|
|
);
|
|
}
|
|
return commonsChunkName;
|
|
},
|
|
minSize: 0,
|
|
},
|
|
},
|
|
}).apply(compiler);
|
|
}
|
|
|
|
async emitAssetsFile(compilation) {
|
|
const emitAssets = [];
|
|
for (let entry of this.assetsEntry) {
|
|
const assets = path.resolve(this.basePath, entry);
|
|
if (/\.(sass|scss|css|less|styl|xml)$/.test(assets)) {
|
|
continue;
|
|
}
|
|
const toTmit = async () => {
|
|
const stat = await fsExtra.stat(assets);
|
|
let size = stat.size;
|
|
let source = await fsExtra.readFile(assets);
|
|
if (entry === 'app.json') {
|
|
const appJson = JSON.parse(source.toString());
|
|
|
|
let pages = [];
|
|
for (let page of appJson.pages) {
|
|
let isOak = getIsOak(page);
|
|
|
|
if (isOak) {
|
|
page = page.replace(
|
|
new RegExp(OakPagePrefix),
|
|
'pages'
|
|
);
|
|
}
|
|
pages.push(page);
|
|
}
|
|
appJson.pages = pages;
|
|
source = Buffer.from(JSON.stringify(appJson, null, 2));
|
|
size = source.length;
|
|
}
|
|
compilation.assets[entry] = {
|
|
size: () => size,
|
|
source: () => source,
|
|
};
|
|
};
|
|
emitAssets.push(toTmit());
|
|
}
|
|
for (let entry of this.oakAssetsEntry) {
|
|
const assets = path.resolve(process.cwd(), entry);
|
|
if (/\.(sass|scss|css|less|styl|xml)$/.test(assets)) {
|
|
continue;
|
|
}
|
|
const toTmit = async () => {
|
|
const stat = await fsExtra.stat(assets);
|
|
const source = await fsExtra.readFile(assets);
|
|
compilation.assets[entry.replace(new RegExp(OakPagePath), '')] =
|
|
{
|
|
size: () => stat.size,
|
|
source: () => source,
|
|
};
|
|
};
|
|
emitAssets.push(toTmit());
|
|
}
|
|
await Promise.all(emitAssets);
|
|
}
|
|
|
|
setBasePath(compiler) {
|
|
this.basePath = compiler.options.context;
|
|
}
|
|
|
|
// async enforceTarget(compiler) {
|
|
// const { options } = compiler;
|
|
// // set jsonp obj motuned obj
|
|
// options.output.globalObject = 'global';
|
|
// options.node = {
|
|
// ...(options.node || {}),
|
|
// global: false,
|
|
// };
|
|
|
|
// // set target to web
|
|
// new JsonpTemplatePlugin(options.output).apply(compiler);
|
|
// new NodeSourcePlugin(options.node).apply(compiler);
|
|
// new LoaderTargetPlugin('web').apply(compiler);
|
|
// }
|
|
|
|
// script full path
|
|
getFullScriptPath(script) {
|
|
const {
|
|
basePath,
|
|
options: { extensions },
|
|
} = this;
|
|
for (const ext of extensions) {
|
|
const fullPath = path.resolve(basePath, script + ext);
|
|
if (fsExtra.existsSync(fullPath)) {
|
|
return fullPath;
|
|
}
|
|
}
|
|
}
|
|
|
|
static async clearOutPut(compilation) {
|
|
const { path } = compilation.options.output;
|
|
await fsExtra.remove(path);
|
|
}
|
|
}
|
|
|
|
module.exports = OakWeChatMpPlugin;
|