UNPKG

generate-examples-index-webpack-plugin

Version:
373 lines (331 loc) 12.6 kB
const path = require('path'); const fs = require('fs'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const validate = require('@webpack-contrib/schema-utils'); const schema = require('../schema/plugin.json'); const getPackageJson = require('./utils/getPackageJson'); const getAssociatedFile = require('./utils/getAssociatedFile'); const getExcludeAssets = require('./utils/getExcludeAssets'); const getFolders = require('./utils/getFolders'); const getHtmlTitle = require('./utils/getHtmlTitle'); const stripExtension = require('./utils/stripExtension'); const isWebpack4Compiler = require('./utils/isWebpack4Compiler'); const getIndexListHtml = require('./html/getIndexListHtml'); const getExamplesFilterHtml = require('./html/getExamplesFilterHtml'); const getBreadCrumbsHtml = require('./html/breadcrumb'); const pluginName = require('../package.json').name; class ExamplesGenerator { constructor(options = {}) { validate({ name: 'generate-examples-index-webpack-plugin', schema, target: options, }); this.options = { addJsExtension: undefined, analyzer: 'assets/analyzer.html', app: 'assets/app', assets: 'assets', breadcrumbsHtmlGenerator: getBreadCrumbsHtml, buildTimeLocale: 'ja', examples: 'examples', examplesFilterHtmlGenerator: getExamplesFilterHtml, examplesIndexHtmlGenerator: getIndexListHtml, excludeAssets: false, extensions: ["js", "jsx", "ts", "tsx"], noEntries: false, outputPath: 'examplesBuild', packageJson: undefined, static: 'assets', templateIndex: path.join(__dirname, 'html', 'index.html'), vendorContent: [], vendorFolder: '', vendorName: 'vendor', ...options, }; this.options.buildTimeOptions = { timeZone: 'Asia/Tokyo', hour12: false, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', ...options.buildTimeOptions, }; this.options.examples = path.resolve(this.options.examples); this.options.outputPath = path.resolve(this.options.outputPath); if (this.options.vendorFolder) { this.options.vendorFolder = path.resolve(this.options.vendorFolder); } } apply(compiler) { // only works with webpack-4 (T_T) if (!isWebpack4Compiler(compiler)) { // eslint-disable-next-line no-console console.warn(`${pluginName} requires webpack 4. No examples index will be generated`); return; } // if `addJsExtension` is not specified, try to detect the correct value if (this.options.addJsExtension === undefined) { this.options.addJsExtension = compiler.options.output.filename.indexOf('.js') === -1; } this.entry = compiler.options.entry; this.plugins = compiler.options.plugins; this.outputPath = compiler.options.output.path; const relativeOutput = path.relative(compiler.options.output.path, this.options.outputPath); const info = this.getExamplesInfo(this.options.examples); const entries = this.getExamplesEntries(info, relativeOutput); const plugins = this.getExamplesPlugins(info, relativeOutput); if (this.options.noEntries) { compiler.options.entry = {}; } // transform the `entry` list to an object so we can add the examples entries if (!compiler.options.entry) { compiler.options.entry = {}; } else if (typeof compiler.options.entry !== 'object' || Array.isArray(compiler.options.entry)) { compiler.options.entry = { main: compiler.options.entry }; } // duplicate the original files to the app folder // (would be better to just generate the files once and copy them, but can't with copy-webpack-plugin) Object.keys(compiler.options.entry).forEach((key) => { const value = compiler.options.entry[key]; compiler.options.entry[path.join(relativeOutput, this.options.app, key)] = value; }); // webpack-merge doesn't work here (?), so copy entries and plugins manually const exampleEntries = Object.keys(entries); exampleEntries.forEach((chunk) => { const chunkName = this.options.addJsExtension ? `${chunk}.js` : chunk; compiler.options.entry[chunkName] = entries[chunk]; }); plugins.forEach((plugin) => { plugin.apply(compiler); }); if (this.options.excludeAssets) { if (!compiler.options.stats) { compiler.options.stats = {}; } if (!compiler.options.devServer) { compiler.options.devServer = { stats: {} }; } if (!compiler.options.devServer.stats) { compiler.options.devServer.stats = {}; } compiler.options.stats.excludeAssets = getExcludeAssets(compiler.options.stats.excludeAssets, exampleEntries); compiler.options.devServer.stats.excludeAssets = getExcludeAssets(compiler.options.devServer.stats.excludeAssets, exampleEntries); } } /** * Analyze the examples folder and generate information to use for generating * html files, indexes and webpack entries objects * * It returns an object like: { * entries: { 'chunkName': ['tjsFiles'] ], * examples: [{ * folderName: 'folderName': * cases: [{ * name: 'case name', * title: 'case title', * htmlPath: 'htmlAbsolutePath', * route: 'exampleRelativePath', * chunks: ['required chunk names'], * }], * }], * } * * If an example folder is named like `this.options.assets`, will be ignored because * it's the one used for placing the app static files */ getExamplesInfo(examplesPath) { const folders = getFolders(examplesPath); const res = { entries: {}, examples: [], app: this.getAppFilesInfo(), }; // get the needed files per each folder folders.forEach((folder) => { const folderName = path.basename(folder); if (folderName === this.options.assets || folderName === this.options.app) { // eslint-disable-next-line no-console console.warn(`Examples in ${folderName} folder will be ignored.`); return; } const folderObject = { folderName, cases: [], }; const files = fs.readdirSync(folder); files.forEach((htmlFile) => { if (path.extname(htmlFile) === '.html') { const fullPath = path.join(folder, htmlFile); const tjsFile = getAssociatedFile(fullPath, this.options.extensions); const caseObject = { name: htmlFile, title: getHtmlTitle(fullPath), route: path.relative(examplesPath, fullPath), htmlPath: fullPath, }; if (tjsFile) { const chunkFile = path.relative(examplesPath, tjsFile); caseObject.chunks = [res.app.vendor, chunkFile]; res.entries[chunkFile] = tjsFile; } else { caseObject.chunks = []; } folderObject.cases.push(caseObject); } }); if (folderObject.cases.length > 0) { res.examples.push(folderObject); } }); return res; } /** * Get the list of chunks and associated files for webpack to build * * @param info Object from getExamplesInfo */ getExamplesEntries(info, relativeOutput) { const res = { ...info.entries, ...info.app.entries }; Object.keys(res).forEach((key) => { const fixedPath = path.normalize(path.join(relativeOutput, key)); const value = res[key]; delete(res[key]); res[fixedPath] = value; }); return res; } /** * Get a list of plugins to add to webpack (HtmlWebpackPlugin) * Basically, for each example it has an html with this injected junks: * - vendor + associated js/ts file if any * - vendor + app files if there's no associated js/ts file * Plus, a HtmlWebpackPlugin entry for the index page, * which has no JS but a list of available examples * * @param info Object from getExamplesInfo */ getExamplesPlugins(info, relativeOutput) { const plugins = []; const packageJson = getPackageJson(this.options.packageJson); const projectName = packageJson && packageJson.name || ''; const projectVersion = packageJson && packageJson.version || ''; const examplesFolder = path.basename(this.options.examples); const buildDate = new Date().toLocaleString(this.options.buildTimeLocale, this.options.buildTimeOptions); const reportFilename = this.options.analyzer && path.join(relativeOutput, this.options.analyzer); // one plugin for the index page plugins.push(new HtmlWebpackPlugin({ filename: path.normalize(path.join(relativeOutput, 'index.html')), template: this.options.templateIndex, chunks: [], minify: false, // available values in the template buildDate, projectName, projectVersion, examplesFolder, examplesIndex: this.options.examplesIndexHtmlGenerator(info), examplesFilter: this.options.examplesFilterHtmlGenerator(info), assets: this.options.assets, reportLink: reportFilename || '', })); // copy the internal assets to the examples assets folder plugins.push(new CopyWebpackPlugin([{ from: path.join(__dirname, 'assets'), to: path.join(this.options.outputPath, this.options.assets), ignore: '.DS_Store', }])); // copy the static folder to the examples assets folder if (fs.existsSync(this.options.static)) { plugins.push(new CopyWebpackPlugin([{ from: this.options.static, to: path.join(this.options.outputPath, this.options.assets), }])); } if (reportFilename) { plugins.push(new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false, reportFilename, })); } // one plugin per example case info.examples.forEach((example) => { example.cases.forEach((entry) => { const relPath = path.relative(entry.htmlPath, this.options.examples).replace('../', ''); const backLink = path.join(relPath, 'index.html'); const assets = path.join(relPath, this.options.assets); const app = path.join(relPath, this.options.app); const projectCss = projectName && path.join(relPath, `${assets}/${projectName}.css`) || ''; const breadcrumbs = this.options.breadcrumbsHtmlGenerator(entry.route); plugins.push(new HtmlWebpackPlugin({ filename: path.normalize(path.join(relativeOutput, entry.route)), template: entry.htmlPath, chunks: entry.chunks.map((chunk) => { const chunkName = path.normalize(path.join(relativeOutput, chunk)); return this.options.addJsExtension ? `${chunkName}.js` : chunkName; }), inject: true, minify: false, // available values in the template breadcrumbs, buildDate, projectName, projectVersion, examplesFolder, app, projectCss, assets, backLink, })); }); }); return plugins; } /** * Return an object with information about the app files to use in the example. * The object is like this: { * vendor: `vendorChunkName`, * entries: { chunkName: [files] }, * chunks: [chunkNames], * } */ getAppFilesInfo() { const res = { entries: {} }; res.vendor = path.join(this.options.app, stripExtension(this.options.vendorName)); Object.keys(this.entry, (key) => { const chunk = path.join(this.options.app, key); res.entries[chunk] = this.entry[key]; }); res.chunks = Object.keys(res.entries); return res; } /** * * @param {*} list */ getVendor(list) { list = list || {}; if (this.options.vendorName) { const vendorName = stripExtension(this.options.vendor); const vendorFolder = this.options.vendorFolder; list[vendorName] = this.options.vendorContent; if (fs.existsSync(vendorFolder)) { fs.readdirSync(vendorFolder).forEach((file) => { if (/\.[tj]sx?$/.test(file)) { list[vendorName].push(path.join(vendorFolder, file)); } }); } } return list; } } module.exports = ExamplesGenerator;