This commit is contained in:
chrosey
2017-09-13 07:52:34 +02:00
parent a1f16c37f4
commit 2340b0226b
24621 changed files with 2912161 additions and 149 deletions
+397
View File
@@ -0,0 +1,397 @@
let Verify = require('./Verify');
class Api {
/**
* Create a new API instance.
*
* @param {Mix} Mix
*/
constructor(Mix) {
this.Mix = Mix;
}
/**
* Register the Webpack entry/output paths.
*
* @param {string|Array} entry
* @param {string} output
*/
js(entry, output) {
global.entry.addScript(entry, output);
return this;
};
/**
* Declare support for the React framework.
*/
react(entry, output) {
this.Mix.react = true;
Verify.dependency(
'babel-preset-react',
'npm install babel-preset-react --save-dev'
);
this.js(entry, output);
return this;
};
/**
* Declare support for the TypeScript.
*/
ts(entry, output) {
this.Mix.ts = true;
Verify.dependency(
'ts-loader',
'npm install ts-loader typescript --save-dev'
);
this.js(entry, output);
return this;
};
/**
* Register vendor libs that should be extracted.
* This helps drastically with long-term caching.
*
* @param {Array} libs
* @param {string} output
*/
extract(libs, output) {
global.entry.addVendor(libs, output);
return this;
};
/**
* Register libraries to automatically "autoload" when
* the appropriate variable is references in js
*
* @param {object} libs
*/
autoload(libs) {
let aliases = {};
Object.keys(libs).forEach(library => {
[].concat(libs[library]).forEach(alias => {
aliases[alias] = library;
});
});
this.Mix.autoload = aliases;
return this;
};
/**
* Enable Browsersync support for the project.
*
* @param {object} config
*/
browserSync(config = {}) {
if (typeof config === 'string') {
config = { proxy: config };
}
this.Mix.browserSync = config;
return this;
};
/**
* Register Sass compilation.
*
* @param {string} src
* @param {string} output
* @param {object} pluginOptions
*/
sass(src, output, pluginOptions = {}) {
return this.preprocess(
'Sass', src, output, pluginOptions
);
};
/**
* Register standalone-Sass compilation that will not run through Webpack.
*
* @param {string} src
* @param {string} output
* @param {object} pluginOptions
*/
standaloneSass(src, output, pluginOptions = {}) {
let Preprocessor = require('./Preprocessors/StandaloneSass');
this.Mix.standaloneSass = (this.Mix.standaloneSass || []).concat(
new Preprocessor(src, output, pluginOptions)
);
return this;
};
/**
* Register Less compilation.
*
* @param {string} src
* @param {string} output
* @param {object} pluginOptions
*/
less(src, output, pluginOptions = {}) {
return this.preprocess(
'Less', src, output, pluginOptions
);
};
/**
* Register Stylus compilation.
*
* @param {string} src
* @param {string} output
* @param {object} pluginOptions
*/
stylus(src, output, pluginOptions = {}) {
Verify.dependency(
'stylus-loader',
'npm install stylus-loader stylus --save-dev'
);
return this.preprocess(
'Stylus', src, output, pluginOptions
);
};
/**
* Register a generic CSS preprocessor.
*
* @param {string} type
* @param {string} src
* @param {string} output
* @param {object} pluginOptions
*/
preprocess(type, src, output, pluginOptions) {
Verify.preprocessor(type, src, output);
global.entry.addStylesheet(src, output);
let Preprocessor = require('./Preprocessors/' + type);
this.Mix.preprocessors = (this.Mix.preprocessors || []).concat(
new Preprocessor(src, output, pluginOptions)
);
return this;
};
/**
* Combine a collection of files.
*
* @param {string|Array} src
* @param {string} output
*/
combine(src, output) {
this.Mix.concat.add({ src, output });
return this;
};
/**
* Alias for this.Mix.combine().
*
* @param {string|Array} src
* @param {string} output
*/
scripts(src, output) {
return this.combine(src, output);
};
/**
* Alias for this.Mix.combine().
*
* @param {string|Array} src
* @param {string} output
*/
styles(src, output) {
return this.combine(src, output);
};
/**
* Identical to this.Mix.combine(), but includes Babel compilation.
*
* @param {string|Array} src
* @param {string} output
*/
babel(src, output) {
this.Mix.concat.add({ src, output, babel: true });
return this;
};
/**
* Copy one or more files to a new location.
*
* @param {string} from
* @param {string} to
*/
copy(from, to) {
this.Mix.copy.push({ from, to: global.Paths.root(to) });
return this;
};
/**
* Copy a directory to a new location. This is identical
* to mix.copy().
*
* @param {string} from
* @param {string} to
*/
copyDirectory(from, to) {
return this.copy(from, to);
};
/**
* Minify the provided file.
*
* @param {string|Array} src
*/
minify(src) {
let output = src.replace(/\.([a-z]{2,})$/i, '.min.$1');
this.Mix.concat.add({ src, output });
return this;
};
/**
* Enable sourcemap support.
*
* @param {Boolean} productionToo
*/
sourceMaps(productionToo = true) {
let type = 'cheap-module-eval-source-map';
if (this.Mix.inProduction) {
type = productionToo ? 'cheap-source-map' : false;
}
global.options.sourcemaps = type;
return this;
};
/**
* Enable compiled file versioning.
*
* @param {string|Array} files
*/
version(files = []) {
global.options.versioning = true;
this.Mix.version = [].concat(files);
return this;
};
/**
* Disable all OS notifications.
*/
disableNotifications() {
global.options.notifications = false;
return this;
};
/**
* Set the path to your public folder.
*
* @param {string} path
*/
setPublicPath(path) {
global.options.publicPath = this.Mix.publicPath = new File(path)
.parsePath()
.pathWithoutExt;
return this;
};
/**
* Set prefix for generated asset paths
*
* @param {string} path
*/
setResourceRoot(path) {
global.options.resourceRoot = path;
return this;
};
/**
* Merge custom config with the provided webpack.config file.
*
* @param {object} config
*/
webpackConfig(config) {
this.Mix.webpackConfig = config;
return this;
}
/**
* Set Mix-specific options.
*
* @param {object} options
*/
options(options) {
if (options.purifyCss) {
options.purifyCss = require('./PurifyPaths').build(options.purifyCss);
Verify.dependency(
'purifycss-webpack',
'npm install purifycss-webpack --save-dev',
true // abortOnComplete
);
}
global.options.merge(options);
return this;
};
/**
* Register a Webpack build event handler.
*
* @param {Function} callback
*/
then(callback) {
global.events.listen('build', callback);
return this;
}
}
module.exports = Api;
+55
View File
@@ -0,0 +1,55 @@
class Collection {
/**
* Create a new Collection instance.
*
* @param {object} items
*/
constructor(items = {}) {
this.items = items;
}
/**
* Add a new key-value pair to the collection.
*
* @param {string} name
* @param {string|Array} files
*/
add(name, files) {
if (! this.items[name]) {
this.items[name] = [];
}
this.items[name] = this.items[name].concat(files);
}
/**
* Get the underlying items for the collection.
*
* @return {Array}
*/
get() {
return this.items;
}
/**
* Determine if there are any items in the collection.
*/
any() {
return Object.keys(this.get()).length > 0;
}
/**
* Empty the collection.
*/
empty() {
this.items = {};
return this;
}
}
module.exports = Collection;
+129
View File
@@ -0,0 +1,129 @@
let md5 = require('md5');
let chokidar = require('chokidar');
let concatenate = require('concatenate');
let babel;
class Concat {
/**
* Create a new Concat instance.
*/
constructor() {
this.combinations = [];
global.events.listen('build', this.run.bind(this))
.listen('init', () => this.watch());
}
/**
* Add a set of files to be combined.
*
* @param {object} files
*/
add(files) {
this.combinations.push({
src: files.src,
output: files.output,
outputOriginal: files.output,
babel: !! files.babel
});
return this;
}
/**
* Watch all relevant files for changes.
*
* @param {object|null} watcher
*/
watch(watcher) {
watcher = watcher || chokidar;
if (! this.shouldWatch() || ! this.any()) return;
this.combinations.forEach(combination => {
watcher.watch(combination.src, { persistent: true })
.on('change', this.combine.bind(this, combination));
});
}
/**
* Determine if file watching should be enabled.
*
* @return {boolean}
*/
shouldWatch() {
return this.any() && process.argv.includes('--watch');
}
/**
* Process combination.
*
* @param {object} files
*/
combine(files) {
let output = File.find(files.output).makeDirectories();
let mergedFileContents = concatenate.sync(files.src, files.output);
if (files.babel && output.fileType === '.js') {
output.write(this.babelify(mergedFileContents));
}
// If file versioning is enabled, then we'll
// rename the output file to apply a hash.
if (global.options.versioning) {
let versionedPath = File.find(files.outputOriginal)
.versionedPath(md5(mergedFileContents));
files.output = output.rename(versionedPath).file;
}
if (process.env.NODE_ENV === 'production' || process.argv.includes('-p')) {
new File(files.output).minify();
}
// We'll now fire an event, so that the Manifest class
// can be refreshed to reflect these new files.
global.events.fire('combined', files);
}
/**
* Apply Babel to the given contents.
*
* @param {string} contents
*/
babelify(contents) {
if (! babel) babel = require('babel-core');
return babel.transform(
contents, { presets: ['env'] }
).code;
}
/**
* Perform all relevant combinations.
*/
run() {
this.combinations.forEach(files => this.combine(files));
return this;
}
/**
* Determine if there are any files to concatenate.
*
* @return {boolean}
*/
any() {
return this.combinations.length > 0;
}
}
module.exports = Concat;
+48
View File
@@ -0,0 +1,48 @@
class Dispatcher {
/**
* Create a new Dispatcher instance.
*/
constructor() {
this.events = {};
}
/**
* Listen for the given event.
*
* @param {string|Array} events
* @param {Function} handler
*/
listen(events, handler) {
events = [].concat(events);
events.forEach(event => {
this.events[event] = (this.events[event] || []).concat(handler);
});
return this;
}
/**
* Trigger all handlers for the given event.
*
* @param {string} event
* @param {*} data
*/
fire(event, data) {
if (! this.events[event]) return false;
this.events[event].forEach(handler => handler(data));
}
/**
* Fetch all registered event listeners.
*/
all() {
return this.events;
}
}
module.exports = Dispatcher;
+176
View File
@@ -0,0 +1,176 @@
let Collection = new require('./Collection');
let Verify = require('./Verify');
class Entry {
/**
* Create a new Entry instance.
*/
constructor() {
this.entry = new Collection;
this.scripts = [];
this.extractions = [];
}
/**
* Add a script to the entry.
*
* @param {string|array} entry
* @param {string} output
*/
addScript(entry, output) {
Verify.js(entry, output);
entry = [].concat(entry).map(file => {
return new File(path.resolve(file)).parsePath();
});
output = this.normalizeOutput(output, entry);
this.entry.add(
this.entryName(output),
entry.map(src => src.path)
);
this.scripts = this.scripts.concat(entry);
this.base = output.base.replace(global.options.publicPath, '');
return this;
}
/**
* Add a stylesheet to the entry.
*
* @param {string} src
* @param {string} output
*/
addStylesheet(src, output) {
let name = Object.keys(this.get())[0];
this.entry.add(name, path.resolve(src));
return this;
}
/**
* Add a set of vendor extractions to the entry.
*
* @param {array} libs
* @param {string|null} output
*/
addVendor(libs, output) {
if (! this.hasScripts() && ! output) {
throw new Error(
'Please provide an output path as the second argument to ' +
'mix.extract(), or call mix.js() first.'
);
}
let vendorPath = output
? output.replace(/\.js$/, '').replace(global.options.publicPath, '')
: path.join(this.base, 'vendor').replace(/\\/g, '/');
this.extractions.push(vendorPath);
this.extractionBase = new File(vendorPath).parsePath().base;
this.entry.add(vendorPath, libs);
return this;
}
/**
* Calculate the entry named from the output path.
*
* @param {object} output
*/
entryName(output) {
if (typeof output === 'string') {
output = new File(path.resolve(output)).parsePath();
}
return output.pathWithoutExt
.replace(/\\/g, '/')
.replace(/\.js$/, '')
.replace(global.options.publicPath + '/', '/');
}
/**
* Normalize the full output path.
*
* @param {string} output
* @param {string|array} entry
*/
normalizeOutput(output, entry) {
output = new File(output).parsePath();
if (output.isDir) {
output = new File(
path.join(output.path, entry[0].file)
).parsePath();
}
return output;
}
/**
* Fetch the Webpack-ready entry object.
*/
get() {
if (! this.entry.any()) {
let file = new File(path.resolve(__dirname, 'mock-entry.js'));
this.entry.add('mix', file.path());
}
return this.entry.get();
}
/**
* Determine if there are any registered vendor extractions.
*/
hasExtractions() {
return this.extractions.length > 0;
}
/**
* Fetch the vendor extractions list.
*/
getExtractions() {
// We also need to extract webpack's manifest file,
// so that it doesn't bust the cache.
return this.extractions.concat(
path.join(this.extractionBase, 'manifest').replace(/\\/g, '/')
);
}
/**
* Determine if the requested entry includes script compilation.
*/
hasScripts() {
return this.scripts.length > 0;
}
/**
* Fetch the user requested script compilations.
*/
scripts() {
return this.scripts;
}
/**
* Reset the entry object.
*/
reset() {
return new Entry;
}
}
module.exports = new Entry;
+184
View File
@@ -0,0 +1,184 @@
let fs = require('fs');
let md5 = require('md5');
let chokidar = require('chokidar');
let mkdirp = require('mkdirp');
let options = require('./Options');
let uglify = require('uglify-js');
let UglifyCss = require('clean-css');
class File {
/**
* Create a new File instance.
*
* @param {string} file
*/
constructor(file) {
this.file = file;
this.fileType = path.extname(file);
}
/**
* Static constructor.
*
* @param {string} file
*/
static find(file) {
return new File(file);
}
/**
* Make all nested directories in the current file path.
*/
makeDirectories() {
mkdirp.sync(this.parsePath().base);
return this;
}
/**
* Minify the file, if it is CSS or JS.
*/
minify() {
if (this.fileType === '.js') {
this.write(uglify.minify(this.file, options.uglify).code);
}
if (this.fileType === '.css') {
this.write(
new UglifyCss(options.cleanCss).minify(this.read()).styles
);
}
}
/**
* Determine if the given file exists.
*
* @param {string} file
*/
static exists(file) {
return fs.existsSync(file);
}
/**
* Read the file.
*/
read() {
return fs.readFileSync(this.file, {
encoding: 'utf-8'
});
}
/**
* Write the given contents to the file.
*
* @param {string} body
*/
write(body) {
if (typeof body === 'object') {
body = JSON.stringify(body, null, 2);
}
fs.writeFileSync(this.file, body);
return this;
}
/**
* Delete/Unlink the current file.
*/
delete() {
if (fs.existsSync(this.file)) {
fs.unlinkSync(this.file);
}
}
/**
* Watch the current file for changes.
*
* @param {Function} callback
*/
watch(callback) {
return chokidar.watch(
this.path(), { persistent: true }
).on('change', () => callback(this));
}
/**
* Fetch the full path to the file.
*
* @return {string}
*/
path() {
return path.resolve(this.file);
}
/**
* Version the current file.
*/
version() {
let contents = this.read();
let versionedPath = this.versionedPath(md5(contents));
return new File(versionedPath).write(contents);
}
/**
* Fetch a full, versioned path to the file.
*
* @param {string} hash
*/
versionedPath(hash) {
if (! hash) hash = md5(this.read());
return this.parsePath().hashedPath.replace('[hash]', hash);
}
/**
* Parse the file path into segments.
*/
parsePath() {
let outputSegments = path.parse(this.file);
return {
path: this.file,
pathWithoutExt: path.join(outputSegments.dir, `${outputSegments.name}`),
hashedPath: path.join(outputSegments.dir, `${outputSegments.name}.[hash]${outputSegments.ext}`),
base: outputSegments.dir,
file: outputSegments.base,
hashedFile: `${outputSegments.name}.[hash]${outputSegments.ext}`,
name: outputSegments.name,
isFile: !! outputSegments.ext,
isDir: ! outputSegments.ext,
ext: outputSegments.ext
};
}
/**
* Rename the file.
*
* @param {string} to
*/
rename(to) {
fs.renameSync(this.file, to);
this.file = to;
return this;
}
}
module.exports = File;
+84
View File
@@ -0,0 +1,84 @@
let fs = require('fs-extra');
let chokidar = require('chokidar');
let glob = require('glob');
class FileCollection {
/**
* Create a new FileCollection instance.
*
* @param {string|array} files
*/
constructor(files) {
this.files = files;
}
/**
* Copy the src files to the given destination.
*
* @param {string} destination
* @param {string|array|null} src
* @return {this}
*/
copyTo(destination, src) {
src = src || this.files;
this.destination = destination;
if (Array.isArray(src)) {
src.forEach(file => this.copyTo(this.destination, file));
return this;
}
if (src.includes('*')) {
return this.copyTo(this.destination, glob.sync(src));
}
src = new File(src).parsePath();
let output = this.outputPath(src);
console.log('Copying ' + src.path + ' to ' + output);
fs.copySync(src.path, output);
return this;
}
/**
* Construct the appropriate output path for the copy.
*
* @param {Object} src
* @return {string}
*/
outputPath(src) {
let output = new File(this.destination).parsePath();
// If the src path is a file, but the output is a directory,
// we have to append the src filename to the output path.
if (src.isFile && output.isDir) {
output = path.join(
output.path,
Array.isArray(this.files) ? src.file : src.path.replace(this.files, '')
);
if (new File(output).parsePath().isDir) {
output = path.join(output, src.file);
}
return output;
}
return output.path;
}
/**
* Watch all files in the collection for changes.
*/
watch() {
chokidar.watch(this.files, { persistent: true })
.on('change', updatedFile => this.copyTo(this.destination, updatedFile));
}
}
module.exports = FileCollection;
+174
View File
@@ -0,0 +1,174 @@
let objectValues = require('lodash').values;
let object = require('lodash/fp/object');
class Manifest {
/**
* Create a new Manifest instance.
*/
constructor() {
this.cache = this.exists() ? this.read() : {};
this.manifest = this.cache;
this.registerEvents();
}
/**
* Register any applicable event listeners.
*/
registerEvents() {
global.events.listen('combined', this.appendCombinedFiles.bind(this))
.listen('standalone-sass-compiled', compiledFile => {
this.add(compiledFile);
this.refresh();
});
return this;
}
/**
* Add a key-value pair to the manifest file.
*
* @param {File} file
*/
add(file) {
let original = this.preparePath(file.file);
this.manifest[original] = global.options.versioning ? this.preparePath(file.versionedPath()) : original;
return this;
}
/**
* Get the modified version of the given path.
*
* @param {string} original
*/
get(original) {
if (original) {
if (original instanceof File) original = original.file;
return this.manifest[this.preparePath(original)];
}
return this.manifest;
}
/**
* Transform the Webpack stats into the shape we need.
*
* @param {object} stats
* @param {object} options
*/
transform(stats, options) {
let flattenedPaths = [].concat.apply(
[], objectValues(stats.assetsByChunkName)
);
flattenedPaths.forEach(path => {
path = this.preparePath(path);
if (! path.startsWith('/')) path = ('/'+path);
let original = path.replace(/\.(\w{20}|\w{32})(\..+)/, '$2');
if (Object.keys(this.cache).length) {
let old = this.cache[original];
if(old && File.exists(old.replace(/^\//, ''))) {
File.find(old.replace(/^\//, '')).delete();
}
}
this.manifest[original] = path;
});
return JSON.stringify(this.manifest, null, 2);
}
/**
* Append any mix.combine()'d output paths to the manifest.
*
* @param {Array} toCombine
*/
appendCombinedFiles(toCombine) {
let output = this.preparePath(toCombine.output);
this.manifest[
output.replace(/\.(\w{32})(\..+)/, '$2')
] = output;
this.refresh();
}
/**
* Refresh the mix-manifest.js file.
*/
refresh() {
let manifest = {};
for (let key in this.manifest) {
let val = this.preparePath(this.manifest[key]);
key = this.preparePath(key);
manifest[key] = val;
}
manifest = object.merge(manifest, this.cache);
File.find(this.path()).write(manifest);
}
/**
* Get the path to the manifest file.
*/
path() {
return path.join(global.options.publicPath, 'mix-manifest.json');
}
/**
* Determine if the manifest file exists.
*/
exists() {
return File.exists(this.path());
}
/**
* Retrieve the JSON output from the manifest file.
*/
read() {
return JSON.parse(File.find(this.path()).read());
}
/**
* Prepare the provided path for processing.
*
* @param {string} path
*/
preparePath(path) {
return path.replace(new RegExp('^' + global.options.publicPath), '')
.replace(/\\/g, '/');
}
/**
* Delete the given file from the manifest.
*
* @param {string} file
*/
remove(file) {
File.find(file).delete();
}
}
module.exports = Manifest;
+155
View File
@@ -0,0 +1,155 @@
let Concat = require('./Concat');
let Manifest = require('./Manifest');
let Versioning = require('./Versioning');
class Mix {
/**
* Create a new Laravel Mix instance.
*/
constructor() {
this.concat = new Concat();
this.copy = [];
this.inProduction = options.production;
this.publicPath = options.publicPath;
this.options = global.options; // deprecated
this.Paths = global.Paths;
}
/**
* Initialize the user's webpack.mix.js configuration file.
*/
initialize() {
if (this.isUsingLaravel()) {
this.publicPath = options.publicPath = 'public';
}
this.manifest = new Manifest();
try { require(Paths.mix()); }
catch (e) {}
if (options.versioning) {
this.versioning = new Versioning(this.version, this.manifest).watch();
}
if (this.standaloneSass) {
this.standaloneSass.forEach(sass => sass.run());
}
this.detectHotReloading();
global.events.fire('init', this);
}
/**
* Prepare the Webpack entry object.
*/
entry() {
return global.entry;
}
/**
* Determine the Webpack output path.
*/
output() {
let filename = options.versioning ? '[name].[chunkhash].js' : '[name].js';
let chunkFilename = path.join(
global.entry.base || '', (options.versioning ? '[name].[chunkhash].js' : '[name].js')
);
let http = process.argv.includes('--https') ? 'https' : 'http';
return {
path: path.resolve(options.hmr ? '/' : options.publicPath),
filename: filename,
chunkFilename: chunkFilename.replace(/^\//, ''),
publicPath: options.hmr ? (http + '://localhost:8080/') : ''
};
}
/**
* Detect if the user desires hot reloading.
*
* @param {boolean} force
*/
detectHotReloading(force = false) {
let file = new File(options.publicPath + '/hot');
file.delete();
// If the user wants hot module replacement, we'll create
// a temporary file, so that Laravel can detect it, and
// reference the proper base URL for any assets.
if (options.hmr || force) {
options.hmr = true;
file.write('hot reloading');
}
}
/**
* Fetch the appropriate Babel config for babel-loader.
*/
babelConfig() {
if (File.exists(Paths.root('.babelrc'))) return '?cacheDirectory';
// If the user doesn't have a .babelrc, we'll use our config.
if (this.react) {
options.babel.presets.push('react');
}
return '?' + JSON.stringify(options.babel);
}
/**
* Fetch definitions for DefinePlugin
*
* @param {object} merge
*/
definitions(merge = {}) {
let regex = /^MIX_/i
// Filter out environment variables that doesn't pass regex
let env = Object.keys(process.env)
.filter(key => regex.test(key))
.reduce((value, key) => {
value[key] = process.env[key]
return value
}, {});
let values = Object.assign(env, merge);
return {
'process.env': Object.keys(values)
// Stringify all values so we can feed into Webpack DefinePlugin
.reduce((value, key) => {
value[key] = JSON.stringify(values[key])
return value
}, {})
};
}
/**
* Determine if we are working with a Laravel project.
*/
isUsingLaravel() {
return File.exists('./artisan');
}
/**
* Fetch the Vue-specific ExtractTextPlugin.
*/
vueExtractTextPlugin() {
let VueExtractTextPluginFactory = require('./Vue/ExtractTextPluginFactory');
return new VueExtractTextPluginFactory(this, options.extractVueStyles).build();
}
};
module.exports = Mix;
+191
View File
@@ -0,0 +1,191 @@
module.exports = {
/**
* Determine if webpack should be triggered in a production environment.
*
* @type {Booolean}
*/
production: (process.env.NODE_ENV === 'production' || process.argv.includes('-p')),
/**
* Determine if we should enable hot reloading.
*
* @type {Boolean}
*/
hmr: process.argv.includes('--hot'),
/**
* Determine if sourcemaps should be created for the build.
*
* @type {Boolean}
*/
sourcemaps: false,
/**
* Determine if notifications should be displayed for each build.
*
* @type {Boolean}
*/
notifications: true,
/**
* The public path for the build.
*
* @type {String}
*/
publicPath: '',
/**
* The resource root for the build.
*
* @type {String}
*/
resourceRoot: '/',
/**
* The default Babel configuration.
*
* @type {Object}
*/
babel: {
cacheDirectory: true,
presets: [
['env', {
'modules': false,
'targets': {
'browsers': ['> 2%'],
uglify: true
}
}]
]
},
/**
* Determine if the bundled assets should be versioned.
*
* @type {Boolean}
*/
versioning: false,
/**
* Whether to extract .vue component styles into a dedicated file.
* You may provide a boolean, or a dedicated path to extract to.
*
* @type {Boolean|string}
*/
extractVueStyles: false,
/**
* Determine if CSS url()s should be processed by Webpack.
*
* @type {Boolean}
*/
processCssUrls: true,
/**
* Determine if Mix should remove unused selectors from your CSS bundle.
* You may provide a boolean, or object for the Purify plugin.
*
* https://github.com/webpack-contrib/purifycss-webpack#options
*
* @type {Boolean|object}
*/
purifyCss: false,
/**
* Uglify-specific settings for Webpack.
*
* See: https://github.com/mishoo/UglifyJS2#compressor-options
*
* @type {Object}
*/
uglify: {
sourceMap: true,
compress: {
warnings: false,
drop_console: true
}
},
/**
* img-loader settings for Webpack.
* See: https://github.com/thetalecrafter/img-loader#options
* @type {Object}
*/
imgLoaderOptions: {
enabled: true,
gifsicle: {},
mozjpeg: {},
optipng: {},
svgo: {},
},
/**
* PostCSS plugins to be applied to compiled CSS.
*
* See: https://github.com/postcss/postcss/blob/master/docs/plugins.md
*
* @type {Array}
*/
postCss: [
require('autoprefixer')
],
/**
* vue-loader specific options.
*
* @type {Object}
*/
vue: {
preLoaders: {},
postLoaders: {}
},
/**
* Determine if Mix should ask the friendly errors plugin to
* clear the console before outputting the results or not.
*
* https://github.com/geowarin/friendly-errors-webpack-plugin#options
*
* @type {Boolean}
*/
clearConsole: true,
/**
* CleanCss-specific settings for Webpack.
*
* See: https://github.com/jakubpawlowicz/clean-css#constructor-options
*
* @type {Object}
*/
cleanCss: {
},
/**
* Merge the given options with the current defaults.
*
* @param {object} options
*/
merge(options) {
let mergeWith = require('lodash').mergeWith;
mergeWith(this, options, (objValue, srcValue) => {
if (Array.isArray(objValue)) {
return objValue.concat(srcValue);
}
});
}
};
+42
View File
@@ -0,0 +1,42 @@
let argv = require('yargs').argv;
class Paths {
/**
* Create a new Paths instance.
*/
constructor() {
this.rootPath = path.resolve(__dirname, '../../../');
}
/**
* Set the root path to resolve webpack.mix.js.
*
* @param {string} path
*/
setRootPath(path) {
this.rootPath = path;
return this;
}
/**
* Determine the path to the user's webpack.mix.js file.
*/
mix() {
return argv.env && argv.env.mixfile !== undefined ? this.root(argv.env.mixfile) : this.root('webpack.mix');
}
/**
* Determine the project root.
*
* @param {string|null} append
*/
root(append = '') {
return path.resolve(this.rootPath, append);
}
}
module.exports = Paths;
+15
View File
@@ -0,0 +1,15 @@
let Preprocessor = require('./Preprocessor');
class Less extends Preprocessor {
/**
* Fetch the Webpack loaders for Less.
*/
loaders(sourceMaps) {
return [{
loader: 'less-loader' + (sourceMaps ? '?sourceMap' : ''),
options: this.pluginOptions
}];
}
}
module.exports = Less;
+99
View File
@@ -0,0 +1,99 @@
let ExtractTextPlugin = require('extract-text-webpack-plugin');
class Preprocessor {
/**
* Create a new Preprocessor instance.
*
* @param {string} src
* @param {string} output
* @param {object} pluginOptions
* @param {object} mixOptions
*/
constructor(src, output, pluginOptions) {
src = new File(path.resolve(src)).parsePath();
output = new File(output).parsePath();
if (output.isDir) {
output = new File(
path.join(output.path, src.name + '.css')
).parsePath();
}
this.src = src;
this.output = output;
this.pluginOptions = pluginOptions;
}
/**
* Get the Webpack extract text plugin instance.
*/
getExtractPlugin() {
if (! this.extractPlugin) {
this.extractPlugin = new ExtractTextPlugin(this.outputPath());
}
return this.extractPlugin;
}
/**
* Prepare the Webpack rules for the preprocessor.
*/
rules() {
return {
test: this.test(),
use: this.getExtractPlugin().extract({
fallback: 'style-loader',
use: this.defaultLoaders().concat(this.loaders(global.options.sourcemaps))
})
};
}
/**
* Get the regular expression test for the Extract plugin.
*/
test() {
return new RegExp(this.src.path.replace(/\\/g, '\\\\') + '$');
}
/**
* Fetch the default Webpack loaders.
*/
defaultLoaders() {
let sourceMap = !!global.options.sourcemaps;
return [
{
loader: 'css-loader',
options: {
url: global.options.processCssUrls,
sourceMap: sourceMap
}
},
{
loader: 'postcss-loader',
options: {
sourceMap: sourceMap
}
}
];
}
/**
* Determine the appropriate CSS output path.
*
* @param {object} output
*/
outputPath() {
let regex = new RegExp('^(\.\/)?' + global.options.publicPath);
let pathVariant = global.options.versioning ? 'hashedPath' : 'path';
return this.output[pathVariant].replace(regex, '').replace(/\\/g, '/').replace('[hash]','[contenthash]');
}
}
module.exports = Preprocessor;
+33
View File
@@ -0,0 +1,33 @@
let Preprocessor = require('./Preprocessor');
class Sass extends Preprocessor {
/**
* Fetch the Webpack loaders for Sass.
*/
loaders(sourceMaps) {
let loaders = [
{ loader: 'sass-loader', options: this.sassPluginOptions() }
];
if (global.options.processCssUrls) {
loaders.unshift(
{ loader: 'resolve-url-loader' + (sourceMaps ? '?sourceMap' : '') }
);
}
return loaders;
}
/**
* Fetch the Node-Sass-specififc plugin options.
*/
sassPluginOptions() {
return Object.assign({
precision: 8,
outputStyle: 'expanded'
}, this.pluginOptions, { sourceMap: true })
}
}
module.exports = Sass;
+166
View File
@@ -0,0 +1,166 @@
let File = require('../File');
let path = require('path');
let spawn = require('child_process').spawn;
let notifier = require('node-notifier');
class StandaloneSass {
/**
* Create a new StandaloneSass instance.
*
* @param {string} src
* @param {string} output
* @param {object} pluginOptions
*/
constructor(src, output, pluginOptions) {
src = new File(path.resolve(src)).parsePath();
output = new File(output).parsePath();
if (output.isDir) {
output = new File(
path.join(output.path, src.name + '.css')
).parsePath();
}
this.src = src;
this.output = output;
this.pluginOptions = pluginOptions;
this.shouldWatch = process.argv.includes('--watch');
}
/**
* Run the node-sass compiler.
*/
run() {
this.compile();
if (this.shouldWatch) this.watch();
}
/**
* Compile Sass.
*
* @param {Boolean} watch
*/
compile(watch = false) {
let output = this.output.path;
if (! output.startsWith(options.publicPath)) {
output = path.join(options.publicPath, output);
}
this.command = spawn(
'node-sass', [this.src.path, output].concat(this.options(watch)), { shell: true }
);
this.whenOutputIsAvailable((output, event) => {
if (event === 'error') this.onFail(output);
if (event === 'success') this.onSuccess(output);
});
return this;
}
/**
* Fetch the node-sass options.
*
* @param {Boolean} watch
*/
options(watch) {
let sassOptions = [
'--precision=8',
'--output-style=' + (global.options.production ? 'compressed' : 'expanded'),
];
if (watch) sassOptions.push('--watch');
if (this.pluginOptions.includePaths) {
this.pluginOptions.includePaths.forEach(
path => sassOptions.push('--include-path=' + path)
);
}
if (global.options.sourcemaps && ! global.options.production) {
sassOptions.push('--source-map-embed');
}
return sassOptions;
}
/**
* Compile Sass, while registering a watcher.
*/
watch() {
return this.compile(true);
}
/**
* Register a callback for when output is available.
*
* @param {Function} callback
*/
whenOutputIsAvailable(callback) {
this.command.stderr.on('data', output => {
output = output.toString();
let event = 'change';
if (/Error/.test(output)) event = 'error';
if (/Wrote CSS/.test(output)) event = 'success';
callback(output, event);
});
}
/**
* Handle successful compilation.
*
* @param {string} output
*/
onSuccess(output) {
console.log("\n");
console.log(output);
if (global.options.notifications) {
notifier.notify({
title: 'Laravel Mix',
message: 'Sass Compilation Successful',
contentImage: 'node_modules/laravel-mix/icons/laravel.png'
});
}
global.events.fire(
'standalone-sass-compiled', File.find(this.output.path)
);
}
/**
* Handle failed compilation.
*
* @param {string} output
*/
onFail(output) {
output = output.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
console.log("\n");
console.log('Sass Compilation Failed!');
console.log();
console.log(output);
if (global.options.notifications) {
notifier.notify({
title: 'Laravel Mix',
subtitle: 'Sass Compilation Failed',
message: JSON.parse(output).message,
contentImage: 'node_modules/laravel-mix/icons/laravel.png'
});
}
if (! this.shouldWatch) process.exit();
}
}
module.exports = StandaloneSass;
+15
View File
@@ -0,0 +1,15 @@
let Preprocessor = require('./Preprocessor');
class Stylus extends Preprocessor {
/**
* Fetch the Webpack loaders for Stylus.
*/
loaders(sourceMaps) {
return [{
loader: 'stylus-loader' + (sourceMaps ? '?sourceMap' : ''),
options: this.pluginOptions
}];
}
}
module.exports = Stylus;
+26
View File
@@ -0,0 +1,26 @@
let glob = require('glob');
class Purify {
/**
* Build up the proper Purify file paths.
*
* @param {Boolean|object} options
*/
static build(options) {
if (typeof options === 'object' && options.paths) {
let paths = options.paths;
paths.forEach(path => {
if (! path.includes('*')) return;
options.paths.splice(paths.indexOf(path), 1);
options.paths = paths.concat(glob.sync(path));
});
}
return options;
}
}
module.exports = Purify;
+84
View File
@@ -0,0 +1,84 @@
let assert = require('assert');
let exec = require('child_process').execSync;
class Verify {
/**
* Verify that the call the mix.js() is valid.
*
* @param {*} entry
* @param {*} output
*/
static js(entry, output) {
assert(
typeof entry === 'string' || Array.isArray(entry),
'mix.js() is missing required parameter 1: entry'
);
assert(
typeof output === 'string',
'mix.js() is missing required parameter 2: output'
);
}
/**
* Verify that the calls the mix.sass() and mix.less() are valid.
*
* @param {string} type
* @param {string} src
* @param {string} output
*/
static preprocessor(type, src, output) {
assert(
typeof src === 'string',
`mix.${type}() is missing required parameter 1: src`
);
assert(
typeof output === 'string',
`mix.${type}() is missing required parameter 2: output`
);
}
/**
* Verify that the call the mix.extract() is valid.
*
* @param {Array} libs
*/
static extract(libs) {
assert(
libs && Array.isArray(libs),
'mix.extract() requires an array as its first parameter.'
);
}
/**
* Verify that the necessary dependency is available.
*
* @param {string} dependency
* @param {string} installCommand
* @param {Boolean} abortOnComplete
*/
static dependency(dependency, installCommand, abortOnComplete = false) {
try {
require.resolve(dependency);
} catch (e) {
console.log(
'Additional dependencies must be installed. ' +
'This will only take a moment.'
);
exec(installCommand);
if (abortOnComplete) {
console.log('Finished. Please run Mix again.');
process.exit();
}
}
}
}
module.exports = Verify;
+108
View File
@@ -0,0 +1,108 @@
let objectValues = require('lodash').values;
class Versioning {
/**
* Create a new Versioning instance.
*
* @param {Array} manualFiles
* @param {object} manifest
* @param {string} publicPath
*/
constructor(manualFiles = [], manifest) {
this.manualFiles = manualFiles.map(file => new File(file));
this.manifest = manifest;
this.registerEvents();
}
/**
* Register all relevant event listeners.
*/
registerEvents() {
global.events.listen('standalone-sass-compiled', compiledFile => {
compiledFile.rename(compiledFile.versionedPath());
this.prune();
});
global.events.listen(
['build', 'combined'], () => this.prune()
);
}
/**
* Register a watcher for any files that aren't
* included in Webpack's core bundle process.
*/
watch() {
if (! process.argv.includes('--watch')) return this;
this.manualFiles.forEach(file => {
file.watch(file => {
// Delete the old versioned file.
File.find(
path.join(global.options.publicPath, this.manifest.get(file))
).delete();
// And then whip up a new one.
file.version();
this.prune();
});
});
return this;
}
/**
* Create all hashed files requested by the user,
* when they called mix.version(['file']);
*/
writeHashedFiles() {
this.manualFiles.forEach(file => file.version());
return this;
}
/**
* The user may optionally add extra files to be
* versioned. Here, we'll manually add those to
* Mix's manifest file.
*/
addManualFilesToManifest() {
this.manualFiles.forEach(file => this.manifest.add(file));
}
/**
* Replace all old hashed files with the new versions.
*/
prune() {
this.writeHashedFiles().addManualFilesToManifest();
let cachedFiles = objectValues(this.manifest.cache);
let currentFiles = objectValues(this.manifest.get());
cachedFiles
.filter(file => ! currentFiles.includes(file))
.map(file => {
return file.startsWith(global.options.publicPath)
? file
: path.join(global.options.publicPath, file);
})
.forEach(file => {
this.manifest.remove(file)
});
this.manifest.refresh();
this.manifest.cache = this.manifest.get();
return currentFiles;
}
}
module.exports = Versioning;
+72
View File
@@ -0,0 +1,72 @@
let WebpackExtractPlugin = require('extract-text-webpack-plugin')
class ExtractTextPluginFactory {
/**
* Create a new class instance.
*
* @param {string|boolean} cssPath
*/
constructor(mix, cssPath) {
if (typeof cssPath === 'boolean') {
cssPath = path.join(global.entry.base || '', 'vue-styles.css');
this.useDefault = true;
}
this.mix = mix;
this.path = cssPath;
}
/**
* Build up the necessary ExtractTextPlugin instance.
*/
build() {
if (this.mix.preprocessors) {
// If no output path is provided, we can use the default plugin.
if (this.useDefault) return this.mix.preprocessors[0].getExtractPlugin();
// If what the user passed matches the output to mix.preprocessor(),
// then we can use that plugin instead and append to it.
if (this.pluginIsAlreadyBuilt()) return this.getPlugin();
}
// Otherwise, we'll setup a new plugin to toss the styles into it.
return new WebpackExtractPlugin(this.outputPath());
}
/**
* Check if the the provided path is already registered as an extract instance.
*/
pluginIsAlreadyBuilt() {
return this.mix.preprocessors.find(
preprocessor => preprocessor.output.path === this.path
);
}
/**
* Fetch the Extract plugin instance that matches the current output path.
*/
getPlugin() {
return this.mix.preprocessors.find(
preprocessor => preprocessor.getExtractPlugin().filename === this.outputPath()
).getExtractPlugin();
}
/**
* Prepare the appropriate output path.
*/
outputPath() {
let segments = new File(this.path).parsePath();
let regex = new RegExp('^(\.\/)?' + global.options.publicPath);
let pathVariant = global.options.versioning ? 'hashedPath' : 'path';
return segments[pathVariant].replace(regex, '').replace(/\\/g, '/');
}
}
module.exports = ExtractTextPluginFactory;
+16
View File
@@ -0,0 +1,16 @@
let FileCollection = require('../FileCollection');
/**
* Create a new CopyWebpackPlugin instance.
*
* @param {array} copy
*/
module.exports = function CopyWebpackPlugin(copy) {
copy.forEach(copy => {
let filesToCopy = new FileCollection(copy.from).copyTo(copy.to);
if (process.argv.includes('--watch') || process.argv.includes('--hot')) {
filesToCopy.watch();
}
});
}
+24
View File
@@ -0,0 +1,24 @@
function MockEntryPlugin(outputPath) {
this.outputPath = outputPath;
}
MockEntryPlugin.prototype.apply = function (compiler) {
compiler.plugin('done', stats => {
// If no mix.js() call was requested, we'll also need
// to delete the output script for the user. Since we
// won't know the exact name, we'll hunt it down.
let temporaryOutputFile = stats.toJson()
.assets
.find(asset => asset.chunkNames.includes('mix'));
if (temporaryOutputFile) {
File.find(
path.resolve(this.outputPath, temporaryOutputFile.name)
).delete();
}
delete stats.compilation.assets['mix.js'];
});
};
module.exports = MockEntryPlugin;
+29
View File
@@ -0,0 +1,29 @@
global.options = require('./Options');
global.entry = require('./Entry');
global.path = require('path');
global.Paths = new (require('./Paths'));
global.events = new (require('./Dispatcher'));
global.File = require('./File');
let mix = new (require('./Mix'));
// The default export for this module will in fact
// be the fluent API for your webpack.mix.js file.
module.exports = api = new (require('./Api'))(mix);
module.exports.mix = api; // Deprecated.
// However, you can access the Mix instance like this:
module.exports.config = mix;
// We'll export a handful of common plugins for a cleaner config file.
module.exports.plugins = {
WebpackNotifierPlugin: require('webpack-notifier'),
WebpackOnBuildPlugin: require('on-build-webpack'),
ExtractTextPlugin: require('extract-text-webpack-plugin'),
FriendlyErrorsWebpackPlugin: require('friendly-errors-webpack-plugin'),
StatsWriterPlugin: require('webpack-stats-plugin').StatsWriterPlugin,
WebpackChunkHashPlugin: require('webpack-chunk-hash'),
BrowserSyncPlugin: require('browser-sync-webpack-plugin'),
CopyWebpackPlugin: require('./WebpackPlugins/CopyWebpackPlugin'),
MockEntryPlugin: require('./WebpackPlugins/MockEntryPlugin')
};
View File