makestatic-pack-webpack
Version:
Bundle assets using webpack
258 lines (215 loc) • 7.79 kB
JavaScript
const SingleEntryDependency =
require('webpack/lib/dependencies/SingleEntryDependency')
const MultiEntryDependency =
require('webpack/lib/dependencies/MultiEntryDependency')
const path = require('path')
const when = require('when')
const node = require('when/node')
/**
* System webpack plugin.
*
* @class PackPlugin
*/
class PackPlugin {
/**
* Create a PackPlugin.
*
* @param {Object} context the processing context.
* @param {Boolean} watching whether the source files are being watched.
*/
constructor (context = {}, watching = false) {
this.name = 'PackPlugin'
this.context = context
this.watching = watching
}
/**
* Adds compilation entries.
*
* @function addFiles
* @member PackPlugin
* @param {Object} compilation the webpack compilation.
* @param {Array} files list of files to add to the compilation.
*
* @returns a promise that resolves when all the files have been added.
*/
addFiles (compilation, files) {
return when.all(Array.prototype.concat(files).map((file) => {
// do not add javascript files other than the entry points
// allows treating some javascript files as static assets
// eg: when you just want to include a library
// and load in the browser, place the files in a `vendor` directory
if (!/vendor\//.test(file.name) && /\.jsx?$/.test(file.name)) {
return when.resolve()
}
// manifest files are dynamic, don't add them to the compilation
/* istanbul ignore next: only applies to repeat builds in watch mode */
/*
if (this.context.options.manifest &&
file.path === this.context.options.manifest) {
return when.resolve()
}
*/
// source map files are also dynamic
/* istanbul ignore next: only applies to repeat builds in watch mode */
if (/\.map$/.test(file.name)) {
return when.resolve()
}
let rel = file.name
if (path.isAbsolute(rel)) {
rel = file.relative(this.context.config.context)
} else {
let ptn = new RegExp('^' + this.context.config.context)
// the webpack context is set so if the input file path
// is relative to the current working directory we need
// to strip the context path otherwise webpack is very unhappy
/* istanbul ignore else:
*
* not going to mock relative path outside context
*
*/
if (ptn.test(rel)) {
rel = rel.replace(ptn, '').replace(/^\/+/, '')
}
}
let dep = new MultiEntryDependency(
[new SingleEntryDependency(`./${rel}`)], rel)
const addEntryFn = compilation.addEntry.bind(compilation)
return node.call(addEntryFn, this.context.config.context, dep, rel)
}))
}
/**
* Webpack plugin entry point that configures the webpack plugin functions.
*
* @function apply
* @member PackPlugin
* @param {Object} compiler the webpack compiler.
*/
apply (compiler) {
const log = this.context.log
let transient = []
let files = []
// all assets including transient ones
let all = {}
compiler.plugin('invalid', (fileName, changeTime) => {
/* istanbul ignore next: not going to mock log message */
log.info('[invalidated] %s %s', fileName, changeTime)
})
// inject files into webpack's pipeline
compiler.plugin('make', (compilation, done) => {
return this.addFiles(compilation, this.context.files)
.done(() => done())
})
// get the assets and mutate file list
compiler.plugin('after-compile', (compilation, done) => {
// clear file list otherwise we duplicate in watch mode
files = []
const assets = Object.keys(compilation.assets)
assets.forEach((name) => {
let relativeSource = path.join(this.context.options.input, name)
let absoluteSource = path.join(this.context.options.root, name)
let relativeOutput = name
let file
const dep = compilation.modules.find((el) => {
if (el.userRequest === absoluteSource) {
return el
}
})
if (dep && dep.error) {
return done(dep.error)
// transformed entry points do not find as dependencies
// as they have been renamed so the compilation modules find fails
// so we create a file for the transformed output
} else if (!dep) {
const asset = compilation.assets[name]
file = this.context.getFile(
{output: name, content: asset.source(), name: name})
} else {
// get the compiled source content for the file
// either from the raw source loader buffer or from
// webpack's compiled value
/* istanbul ignore next:
*/
const content = dep._src
? dep._src
: dep._source._value
// find the file definition for the asset
file = this.context.files.find((file) => {
let relative = file.relative(this.context.config.context)
if (name === relative) {
return file
}
})
// remove existing asset, might well be renamed (file extension)
delete compilation.assets[name]
// update output path, will set the extension if necessary
file.output = relativeOutput
// TODO: only invalidate files that have changed
// invalidate file
if (this.watching) {
file.emitted = false
file.existsAt = null
}
// update content for the file
file.content = content
}
// NOTE: must use `file.output` so the file is renamed before output
all[file.output] = file
if (!file.transient) {
// add to the list of files being processed
files.push(file)
}
})
done()
})
compiler.plugin('emit', (compilation, done) => {
// don't optimize if the compilation has errors
if (compilation.errors.length) {
console.dir(compilation.errors)
return done()
}
// assign asset list so that future plugins can access assets by key
// including transient assets so that plugins that require transient
// assets will be able to access them with watch mode enabled
this.context.assets = all
// console.dir(this.context.list.get('csp.txt'))
// run the optimization lifecycle
const lifecycle = this.context.lifecycle
const phases = lifecycle.main
//console.log('starting compile')
lifecycle.run(phases)
.then(() => {
// remove transient files from compilation assets - this allows
// plugins to mark files as transient during the main lifecycle
for (let k in this.context.assets) {
if (this.context.assets[k].transient) {
transient.push(this.context.assets[k])
this.context.list.remove(this.context.assets[k])
}
}
compilation.assets = this.context.assets
done()
})
.catch(done)
})
compiler.plugin('after-emit', (compilation, done) => {
// need to add transient files back when watching
if (this.watching) {
transient.forEach((file) => {
this.context.list.add(file)
})
}
// not performing a deployment
if (!this.context.options.provider) {
return done()
}
const lifecycle = this.context.lifecycle
const phases = lifecycle.deploy
lifecycle.run(phases)
.then(() => {
done()
})
.catch(done)
})
}
}
module.exports = PackPlugin