From 07b95ae08015c0bcf8da914b2b949cfa545835c4 Mon Sep 17 00:00:00 2001 From: nkzawa Date: Mon, 24 Oct 2016 01:42:13 +0900 Subject: [PATCH] dynamically add/remove pages --- server/build/plugins/dynamic-entry-plugin.js | 62 +++++++++++++++++++ server/build/plugins/unlink-file-plugin.js | 28 +++++++++ server/build/plugins/watch-pages-plugin.js | 48 ++++++++++++++ .../plugins/watch-remove-event-plugin.js | 40 ++++++++++++ server/build/webpack.js | 27 +++++--- server/hot-reloader.js | 7 +++ 6 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 server/build/plugins/dynamic-entry-plugin.js create mode 100644 server/build/plugins/unlink-file-plugin.js create mode 100644 server/build/plugins/watch-pages-plugin.js create mode 100644 server/build/plugins/watch-remove-event-plugin.js diff --git a/server/build/plugins/dynamic-entry-plugin.js b/server/build/plugins/dynamic-entry-plugin.js new file mode 100644 index 00000000..4a8ee3c2 --- /dev/null +++ b/server/build/plugins/dynamic-entry-plugin.js @@ -0,0 +1,62 @@ +import SingleEntryPlugin from 'webpack/lib/SingleEntryPlugin' +import MultiEntryPlugin from 'webpack/lib/MultiEntryPlugin' + +export default class DynamicEntryPlugin { + apply (compiler) { + compiler.entryNames = getInitialEntryNames(compiler) + compiler.addEntry = addEntry + compiler.removeEntry = removeEntry + compiler.hasEntry = hasEntry + compiler.createCompilation = createCompilation(compiler.createCompilation) + } +} + +function getInitialEntryNames (compiler) { + const entryNames = new Set() + const { entry } = compiler.options + + if (typeof entry === 'string' || Array.isArray(entry)) { + entryNames.add('main') + } else if (typeof entry === 'object') { + Object.keys(entry).forEach((name) => { + entryNames.add(name) + }) + } + + return entryNames +} + +function addEntry (entry, name = 'main') { + const { context } = this.options + const Plugin = Array.isArray(entry) ? MultiEntryPlugin : SingleEntryPlugin + this.apply(new Plugin(context, entry, name)) + this.entryNames.add(name) +} + +function removeEntry (name = 'main') { + this.entryNames.delete(name) +} + +function hasEntry (name = 'main') { + this.entryNames.has(name) +} + +function createCompilation (original) { + return function (...args) { + const compilation = original.apply(this, args) + compilation.addEntry = compilationAddEntry(compilation.addEntry) + return compilation + } +} + +function compilationAddEntry (original) { + return function (context, entry, name, callback) { + if (!this.compiler.entryNames.has(name)) { + // skip removed entry + callback() + return + } + + return original.call(this, context, entry, name, callback) + } +} diff --git a/server/build/plugins/unlink-file-plugin.js b/server/build/plugins/unlink-file-plugin.js new file mode 100644 index 00000000..bc419f78 --- /dev/null +++ b/server/build/plugins/unlink-file-plugin.js @@ -0,0 +1,28 @@ +import { join } from 'path' +import { unlink } from 'mz/fs' + +export default class UnlinkFilePlugin { + constructor () { + this.prevAssets = {} + } + + apply (compiler) { + compiler.plugin('after-emit', (compilation, callback) => { + const removed = Object.keys(this.prevAssets) + .filter((a) => !compilation.assets[a]) + + this.prevAssets = compilation.assets + + Promise.all(removed.map(async (f) => { + const path = join(compiler.outputPath, f) + try { + await unlink(path) + } catch (err) { + if (err.code === 'ENOENT') return + throw err + } + })) + .then(() => callback(), callback) + }) + } +} diff --git a/server/build/plugins/watch-pages-plugin.js b/server/build/plugins/watch-pages-plugin.js new file mode 100644 index 00000000..e948142f --- /dev/null +++ b/server/build/plugins/watch-pages-plugin.js @@ -0,0 +1,48 @@ +import { resolve, relative, join, extname } from 'path' + +export default class WatchPagesPlugin { + constructor (dir) { + this.dir = resolve(dir, 'pages') + } + + apply (compiler) { + compiler.plugin('emit', (compilation, callback) => { + // watch the pages directory + compilation.contextDependencies = + compilation.contextDependencies.concat([this.dir]) + + callback() + }) + + const isPageFile = this.isPageFile.bind(this) + const getEntryName = (f) => { + return join('bundles', relative(compiler.options.context, f)) + } + + compiler.plugin('watch-run', (watching, callback) => { + Object.keys(compiler.fileTimestamps) + .filter(isPageFile) + .forEach((f) => { + const name = getEntryName(f) + if (compiler.hasEntry(name)) return + + const entries = ['webpack/hot/only-dev-server', f] + compiler.addEntry(entries, name) + }) + + compiler.removedFiles + .filter(isPageFile) + .forEach((f) => { + const name = getEntryName(f) + compiler.removeEntry(name) + }) + + callback() + }) + } + + isPageFile (f) { + return f.indexOf(this.dir) === 0 && extname(f) === '.js' + } +} + diff --git a/server/build/plugins/watch-remove-event-plugin.js b/server/build/plugins/watch-remove-event-plugin.js new file mode 100644 index 00000000..c4a237dc --- /dev/null +++ b/server/build/plugins/watch-remove-event-plugin.js @@ -0,0 +1,40 @@ + +// watch and trigger file remove event +// see: https://github.com/webpack/webpack/issues/1533 + +export default class WatchRemoveEventPlugin { + constructor () { + this.removedFiles = [] + } + + apply (compiler) { + compiler.removedFiles = [] + + compiler.plugin('environment', () => { + if (!compiler.watchFileSystem) return + + const { watchFileSystem } = compiler + const { watch } = watchFileSystem + + watchFileSystem.watch = (files, dirs, missing, startTime, options, callback, callbackUndelayed) => { + const result = watch.call(watchFileSystem, files, dirs, missing, startTime, options, (...args) => { + compiler.removedFiles = this.removedFiles + this.removedFiles = [] + callback(...args) + }, callbackUndelayed) + + const watchpack = watchFileSystem.watcher + watchpack.fileWatchers.forEach((w) => { + w.on('remove', this.onRemove.bind(this, watchpack, w.path)) + }) + return result + } + }) + } + + onRemove (watchpack, file) { + this.removedFiles.push(file) + watchpack.emit('remove', file) + watchpack._onChange(file) + } +} diff --git a/server/build/webpack.js b/server/build/webpack.js index acdf381c..11f6cd42 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -2,6 +2,10 @@ import { resolve, join } from 'path' import webpack from 'webpack' import glob from 'glob-promise' import WriteFilePlugin from 'write-file-webpack-plugin' +import UnlinkFilePlugin from './plugins/unlink-file-plugin' +import WatchPagesPlugin from './plugins/watch-pages-plugin' +import WatchRemoveEventPlugin from './plugins/watch-remove-event-plugin' +import DynamicEntryPlugin from './plugins/dynamic-entry-plugin' export default async function createCompiler (dir, { hotReload = false } = {}) { dir = resolve(dir) @@ -27,17 +31,24 @@ export default async function createCompiler (dir, { hotReload = false } = {}) { const nodeModulesDir = join(__dirname, '..', '..', '..', 'node_modules') const plugins = [ - hotReload - ? new webpack.HotModuleReplacementPlugin() - : new webpack.optimize.UglifyJsPlugin({ - compress: { warnings: false }, - sourceMap: false - }), new WriteFilePlugin({ exitOnErrors: false, - log: false + log: false, + // required not to cache removed files + useHashIndex: false }) - ] + ].concat(hotReload ? [ + new webpack.HotModuleReplacementPlugin(), + new DynamicEntryPlugin(), + new UnlinkFilePlugin(), + new WatchRemoveEventPlugin(), + new WatchPagesPlugin(dir) + ] : [ + new webpack.optimize.UglifyJsPlugin({ + compress: { warnings: false }, + sourceMap: false + }) + ]) const babelRuntimePath = require.resolve('babel-runtime/package') .replace(/[\\\/]package\.json$/, '') diff --git a/server/hot-reloader.js b/server/hot-reloader.js index ade295e4..c8343bba 100644 --- a/server/hot-reloader.js +++ b/server/hot-reloader.js @@ -9,6 +9,7 @@ export default class HotReloader { this.server = null this.stats = null this.compilationErrors = null + this.prevAssets = {} } async start () { @@ -25,6 +26,12 @@ export default class HotReloader { for (const f of Object.keys(assets)) { deleteCache(assets[f].existsAt) } + for (const f of Object.keys(this.prevAssets)) { + if (!assets[f]) { + deleteCache(this.prevAssets[f].existsAt) + } + } + this.prevAssets = assets callback() })