Skip to content

Instantly share code, notes, and snippets.

@ptb
Created June 28, 2018 18:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ptb/a774ca170ae71c18adacd7c42ff4906f to your computer and use it in GitHub Desktop.
Save ptb/a774ca170ae71c18adacd7c42ff4906f to your computer and use it in GitHub Desktop.
/* eslint compat/compat: off, max-statements: off */
const RawSource = require ("webpack-sources/lib/RawSource")
const evaluate = require ("eval")
const path = require ("path")
const cheerio = require ("cheerio")
const url = require ("url")
const Promise = require ("bluebird")
const findAsset = (src, compilation, webpackStatsJson) => {
if (!src) {
const chunkNames = Object.keys (webpackStatsJson.assetsByChunkName)
src = chunkNames[0]
}
const asset = compilation.assets[src]
if (asset) {
return asset
}
let chunkValue = webpackStatsJson.assetsByChunkName[src]
if (!chunkValue) {
return null
}
// Webpack outputs an array for each chunk when using sourcemaps
if (chunkValue instanceof Array) {
// Is the main bundle always the first element?
chunkValue = chunkValue[0]
}
return compilation.assets[chunkValue]
}
const getAssetsFromCompilation = (compilation, webpackStatsJson) => {
const assets = {}
for (const chunk in webpackStatsJson.assetsByChunkName) {
let chunkValue = webpackStatsJson.assetsByChunkName[chunk]
// Webpack outputs an array for each chunk when using sourcemaps
if (chunkValue instanceof Array) {
// Is the main bundle always the first element?
chunkValue = chunkValue[0]
}
if (compilation.options.output.publicPath) {
chunkValue = compilation.options.output.publicPath + chunkValue
}
assets[chunk] = chunkValue
}
return assets
}
const pathToAssetName = (outputPath) => {
let outputFileName = outputPath.replace (/^(\/|\\)/, "")
if (!(/\.(html?)$/i).test (outputFileName)) {
outputFileName = path.join (outputFileName, "index.html")
}
return outputFileName
}
const makeObject = (key, value) => {
const obj = {}
obj[key] = value
return obj
}
const relativePathsFromHtml = (options) => {
const html = options.source
const currentPath = options.path
const $ = cheerio.load (html)
const linkHrefs = $ ("a[href]")
.map (function (_, el) {
return $ (el).attr ("href")
})
.get ()
const iframeSrcs = $ ("iframe[src]")
.map (function (_, el) {
return $ (el).attr ("src")
})
.get ()
return []
.concat (linkHrefs)
.concat (iframeSrcs)
.map (function (href) {
if (href.indexOf ("//") === 0) {
return null
}
const parsed = url.parse (href)
if (parsed.protocol || typeof parsed.path !== "string") {
return null
}
return parsed.path.indexOf ("/") === 0
? parsed.path
: url.resolve (currentPath, parsed.path)
})
.filter (function (href) {
return href !== null
})
}
const renderPaths = (
crawl,
userLocals,
paths,
render,
assets,
webpackStats,
compilation
) => {
const renderPromises = paths.map (function (outputPath) {
const locals = {
"assets": assets,
"path": outputPath,
"webpackStats": webpackStats
}
for (const prop in userLocals) {
if (userLocals.hasOwnProperty (prop)) {
locals[prop] = userLocals[prop]
}
}
const renderPromise =
render.length < 2
? Promise.resolve (render (locals))
: Promise.fromCallback (render.bind (null, locals))
return renderPromise
.then (function (output) {
const outputByPath =
typeof output === "object" ? output : makeObject (outputPath, output)
const assetGenerationPromises = Object.keys (outputByPath).map (
function (key) {
const rawSource = outputByPath[key]
const assetName = pathToAssetName (key)
if (compilation.assets[assetName]) {
return
}
compilation.assets[assetName] = new RawSource (rawSource)
if (crawl) {
const relativePaths = relativePathsFromHtml ({
"path": key,
"source": rawSource
})
return renderPaths (
crawl,
userLocals,
relativePaths,
render,
assets,
webpackStats,
compilation
)
}
}
)
return Promise.all (assetGenerationPromises)
})
.catch (function (err) {
compilation.errors.push (err.stack)
})
})
return Promise.all (renderPromises)
}
module.exports = class {
constructor (options = {}) {
this.entry = options.entry
this.paths = Array.isArray (options.paths)
? options.paths
: [options.paths || "/"]
this.locals = options.locals
this.globals = options.globals
this.crawl = Boolean (options.crawl)
}
apply (compiler) {
const self = this
compiler.hooks.thisCompilation.tap (
"static-site-generator-webpack-plugin",
function (compilation) {
compilation.hooks.optimizeAssets.tapAsync (
"static-site-generator-webpack-plugin",
function (_, done) {
const webpackStats = compilation.getStats ()
const webpackStatsJson = webpackStats.toJson ()
try {
const asset = findAsset (
self.entry,
compilation,
webpackStatsJson
)
if (asset === null) {
throw new Error (`Source file not found: "${self.entry}"`)
}
const assets = getAssetsFromCompilation (
compilation,
webpackStatsJson
)
const source = asset.source ()
let render = evaluate (
source,
self.entry,
self.globals,
true
)
if (render.hasOwnProperty ("default")) {
render = render.default
}
if (typeof render !== "function") {
throw new Error (
`Export from "${
self.entry
}" must be a function that returns an HTML string. Is output.libraryTarget in the configuration set to "umd"?`
)
}
renderPaths (
self.crawl,
self.locals,
self.paths,
render,
assets,
webpackStats,
compilation
).nodeify (done)
} catch (err) {
compilation.errors.push (err.stack)
done ()
}
}
)
}
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment