forked from abraham-musa/developer-portal
-
Notifications
You must be signed in to change notification settings - Fork 0
/
SitemapBuilderWebpackPlugin.js
151 lines (137 loc) · 5.4 KB
/
SitemapBuilderWebpackPlugin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/* This webpack plugin uses the existing React routes and JSON API definitions
* included in the app to create a sitemap at build using react-router-sitemap
* (as opposed to having to maintain a separate map of the app that uses
* Node.js-friendly code or building the sitemap by hand).
* This is complicated by the fact that the app's routes and API definitions
* include dependencies on the components that they render, leading to dependencies
* on not just tsx files but also scss and mdx files.
* The processing necessary to transform these dependencies into Node.js-friendly code
* taps into the existing webpack configuration to avoid a reimplementation
* of this workflow that also is run at build.
*
* The basic flow of this plugin is:
* 1. Copy the webpack.config.prod.js and change the entry point to Routes.tsx
* 2. Compile Routes.tsx and its dependencies
* 3. Save the compiled bundle into memory and execute the script
* using a Node.js virtual machine with implementations of web standards (notably DOM and Window)
* 4. use the resulting exported function `getSitemapData` to build a sitemap with react-router-sitemap
* 5. add `sitemap.xml` to the original webpack compilation's assets so that it is output like any other file in the build bundle
* You can pass this plugin the option verbose: true to receive messages as it moves through this flow.
*/
const webpack = require('webpack');
const MemoryFileSystem = require('memory-fs');
const util = require('util');
const vm = require('vm');
const fs = require('fs');
class SitemapBuilderPlugin {
constructor(options) {
if (typeof options === 'undefined' || !options.routesFile) {
console.error(
'To use SitemapBuilderPlugin you must provide an options object with routesFile as a property',
);
}
this.routesFile = options.routesFile;
this.polyfillsFile = options.polyfillsFile;
this.verbose = options.verbose || false;
this.fileName = options.fileName || 'sitemap.xml';
}
apply(compiler) {
compiler.hooks.emit.tapAsync('SitemapBuilderPlugin', (compilation, callback) => {
if (this.verbose) {
console.log('Sitemap Builder Plugin start');
}
// To avoid error, immediately invoke callback() when this plugin is being called by a
// compilation it triggers
if (compiler.options.entry.includes(this.routesFile)) {
if (this.verbose) {
console.log('Sitemap Builder Plugin end without compiling', compiler.options.entry);
}
callback();
} else {
let config = compiler.options;
config.entry = [this.polyfillsFile, this.routesFile];
config.output.path = '/';
config.output.filename = 'routes.js';
config.optimization = { minimize: false };
this.compileRoutes(config)
.then(mfs => this.executeRoutes(mfs))
.then(sitemapConfig => this.buildSitemap(sitemapConfig, compilation))
.then(() => this.finish(callback))
.catch(err => this.finishWithError(err, compilation, callback));
}
});
}
compileRoutes(config) {
if (this.verbose) {
console.log('compileRoutes start');
}
return new Promise((resolve, reject) => {
let compiler = webpack(config);
let mfs = new MemoryFileSystem();
compiler.outputFileSystem = mfs;
compiler.run((err, stats) => {
if (this.verbose) {
console.log('compileRoutes compiler.run callback');
}
if (err) {
reject(err);
} else {
resolve(mfs);
}
});
});
}
executeRoutes(mfs) {
if (this.verbose) {
console.log('executeRoutes start');
}
return new Promise((resolve, reject) => {
const source = mfs.readFileSync('/routes.js').toString();
const script = new vm.Script(source, { filename: 'routes.vm', displayErrors: true });
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
const dom = new JSDOM(``, { runScripts: 'outside-only' });
const vmContext = dom.getInternalVMContext();
const sitemapConfig = script.runInContext(vmContext).sitemapConfig();
resolve(sitemapConfig);
});
}
buildSitemap(sitemapConfig, compilation) {
if (this.verbose) {
console.log('buildSitemap start');
}
const path = require('path');
const paths = require('./config/paths');
const prodURL = require(paths.appPackageJson).homepage;
const Sitemap = require('react-router-sitemap').default;
const sitemap = new Sitemap(sitemapConfig.topLevelRoutes())
.filterPaths(sitemapConfig.pathFilter)
.applyParams(sitemapConfig.paramsConfig)
.build(prodURL)
.save(path.join(paths.appBuild, 'sitemap.xml'));
const cachedSitemap = sitemap.sitemaps[0].cache;
fs.unlinkSync(path.join(paths.appBuild, 'sitemap.xml'));
compilation.fileDependencies.add(this.fileName);
compilation.assets[this.fileName] = {
size: () => {
return Buffer.byteLength(cachedSitemap, 'utf8');
},
source: () => {
return cachedSitemap;
},
};
}
finish(callback) {
if (this.verbose) {
console.log('Sitemap Builder Plugin end');
}
callback();
}
finishWithError(err, compilation, callback) {
console.log('Sitemap Builder Plugin Error');
console.error(err.stack);
compilation.errors.push(err.stack);
callback();
}
}
module.exports = SitemapBuilderPlugin;