This commit is contained in:
2022-04-07 18:09:31 +05:30
parent 36ea86d8e6
commit 1c779ef816
14095 changed files with 1769361 additions and 47 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Jeffrey Way <jeffrey@jeffrey-way.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env node
// @ts-check
const { Command } = require('commander');
const { spawn } = require('child_process');
const path = require('path');
const pkg = require('../package.json');
const { assertSupportedNodeVersion } = require('../src/Engine.js');
run().catch(err => {
console.error(err);
process.exitCode = process.exitCode || 1;
process.exit();
});
/**
* Run the program.
*/
async function run() {
const program = new Command();
program.name('mix');
program.version(pkg.version);
program.option(
'--mix-config <path>',
'The path to your Mix configuration file.',
'webpack.mix'
);
program.option('--no-progress', 'Disable progress reporting', false);
program
.command('watch')
.description('Build and watch files for changes.')
.option('--hot', 'Enable hot reloading.', false)
.option('--https', 'Enable https.', false)
.action((opts, cmd) =>
executeScript('watch', { ...program.opts(), ...opts }, cmd.args)
);
program
.command('build', { isDefault: true })
.description('Compile Mix.')
.option('-p, --production', 'Run Mix in production mode.', false)
.action((opts, cmd) =>
executeScript('build', { ...program.opts(), ...opts }, cmd.args)
);
await program.parseAsync(process.argv);
}
/**
* Execute the script.
*
* @param {"build"|"watch"} cmd
* @param {{[key: string]: any}} opts
* @param {string[]} args
*/
async function executeScript(cmd, opts, args = []) {
assertSupportedNodeVersion();
const env = getEffectiveEnv(opts);
// We MUST use a relative path because the files
// created by npm dont correctly handle paths
// containg spaces on Windows (yarn does)
const configPath = path.relative(
process.cwd(),
require.resolve('../setup/webpack.config.js')
);
const script = [
commandScript(cmd, opts),
`--config="${configPath}"`,
...quoteArgs(args)
].join(' ');
const scriptEnv = {
NODE_ENV: env,
MIX_FILE: opts.mixConfig
};
const nodeEnv = requiresLegacyOpenSSLProvider()
? { NODE_OPTIONS: process.env.NODE_OPTIONS || `--openssl-legacy-provider` }
: {};
if (isTesting()) {
process.stdout.write(
JSON.stringify({
script,
env: scriptEnv
})
);
return;
}
function restart() {
let child = spawn(script, {
stdio: 'inherit',
shell: true,
env: {
...process.env,
...nodeEnv,
...scriptEnv
}
});
let shouldOverwriteExitCode = true;
child.on('exit', (code, signal) => {
// Note adapted from cross-env:
// https://github.com/kentcdodds/cross-env/blob/3edefc7b450fe273655664f902fd03d9712177fe/src/index.js#L30-L31
// The process exit code can be null when killed by the OS (like an out of memory error) or sometimes by node
// SIGINT means the _user_ pressed Ctrl-C to interrupt the process execution
// Return the appropriate error code in that case
if (code === null) {
code = signal === 'SIGINT' ? 130 : 1;
}
if (shouldOverwriteExitCode) {
process.exitCode = code;
}
});
process.on('SIGINT', () => {
shouldOverwriteExitCode = false;
child.kill('SIGINT');
});
process.on('SIGTERM', () => {
shouldOverwriteExitCode = false;
child.kill('SIGTERM');
});
}
restart();
}
/**
* Get the command-specific portion of the script.
*
* @param {"build"|"watch"} cmd
* @param {{[key: string]: any}} opts
*/
function commandScript(cmd, opts) {
const showProgress = isTTY() && opts.progress;
const script = ['webpack'];
if (cmd === 'build' && showProgress) {
script.push('--progress');
} else if (cmd === 'watch' && !opts.hot) {
script.push('--watch');
if (showProgress) {
script.push('--progress');
}
} else if (cmd === 'watch' && opts.hot) {
script.push('serve', '--hot');
if (opts.https) {
script.push('--https');
}
}
return script.join(' ');
}
/**
* Get the command arguments with quoted values.
*
* @param {string[]} args
*/
function quoteArgs(args) {
return args.map(arg => {
// Split string at first = only
const pattern = /^([^=]+)=(.*)$/;
const keyValue = arg.includes('=') ? pattern.exec(arg).slice(1) : [];
if (keyValue.length === 2) {
return `${keyValue[0]}="${keyValue[1]}"`;
}
return arg;
});
}
/**
* Get the effective envirnoment to run in
*
** @param {{[key: string]: any}} opts
*/
function getEffectiveEnv(opts) {
// If we've requested a production compile we enforce use of the production env
// If we don't a user's global NODE_ENV may override and prevent minification of assets
if (opts.production) {
return 'production';
}
// We use `development` by default or under certain specific conditions when testing
if (!process.env.NODE_ENV || (isTesting() && process.env.NODE_ENV === 'test')) {
return 'development';
}
// Otherwsise defer to the current value of NODE_ENV
return process.env.NODE_ENV;
}
function isTesting() {
return process.env.TESTING;
}
function isTTY() {
if (isTesting() && process.env.IS_TTY !== undefined) {
return process.env.IS_TTY === 'true';
}
if (isTesting() && process.stdout.isTTY === undefined) {
return true;
}
return process.stdout.isTTY;
}
function requiresLegacyOpenSSLProvider() {
if (!process.version.startsWith('v17.')) {
return false;
}
try {
require('crypto').createHash('md4').update('test').digest('hex');
return false;
} catch (err) {
return true;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+165
View File
@@ -0,0 +1,165 @@
{
"name": "laravel-mix",
"version": "6.0.43",
"description": "The power of webpack, distilled for the rest of us.",
"main": "src/index.js",
"types": "types/index.d.ts",
"scripts": {
"mix": "./bin/cli.js",
"test": "cd test && ava --verbose --timeout=30s --no-worker-threads",
"eslint": "eslint setup/ src/ test/ --max-warnings=0"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
},
"bin": {
"mix": "./bin/cli.js",
"laravel-mix": "./bin/cli.js"
},
"repository": "JeffreyWay/laravel-mix",
"bugs": {
"url": "https://github.com/JeffreyWay/laravel-mix/issues"
},
"homepage": "https://github.com/JeffreyWay/laravel-mix#readme",
"keywords": [
"laravel",
"webpack",
"laravel elixir",
"laravel mix"
],
"author": "Jeffrey Way",
"license": "MIT",
"files": [
"icons",
"setup",
"src",
"types"
],
"dependencies": {
"@babel/core": "^7.15.8",
"@babel/plugin-proposal-object-rest-spread": "^7.15.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.15.8",
"@babel/preset-env": "^7.15.8",
"@babel/runtime": "^7.15.4",
"@types/babel__core": "^7.1.16",
"@types/clean-css": "^4.2.5",
"@types/imagemin-gifsicle": "^7.0.1",
"@types/imagemin-mozjpeg": "^8.0.1",
"@types/imagemin-optipng": "^5.2.1",
"@types/imagemin-svgo": "^8.0.0",
"autoprefixer": "^10.4.0",
"babel-loader": "^8.2.3",
"chalk": "^4.1.2",
"chokidar": "^3.5.2",
"clean-css": "^5.2.4",
"cli-table3": "^0.6.0",
"collect.js": "^4.28.5",
"commander": "^7.2.0",
"concat": "^1.0.3",
"css-loader": "^5.2.6",
"cssnano": "^5.0.8",
"dotenv": "^10.0.0",
"dotenv-expand": "^5.1.0",
"file-loader": "^6.2.0",
"fs-extra": "^10.0.0",
"glob": "^7.2.0",
"html-loader": "^1.3.2",
"imagemin": "^7.0.1",
"img-loader": "^4.0.0",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"mini-css-extract-plugin": "^1.6.2",
"node-libs-browser": "^2.2.1",
"postcss-load-config": "^3.1.0",
"postcss-loader": "^6.2.0",
"semver": "^7.3.5",
"strip-ansi": "^6.0.0",
"style-loader": "^2.0.0",
"terser": "^5.9.0",
"terser-webpack-plugin": "^5.2.4",
"vue-style-loader": "^4.1.3",
"webpack": "^5.60.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.7.3",
"webpack-merge": "^5.8.0",
"webpack-notifier": "^1.14.1",
"webpackbar": "^5.0.0-3",
"yargs": "^17.2.1"
},
"devDependencies": {
"@babel/eslint-parser": "^7.15.8",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.0-beta.0",
"@types/cssnano": "^4.0.1",
"@types/fs-extra": "^9.0.13",
"@types/koa": "^2.13.4",
"@types/koa-static": "^4.0.2",
"@types/lodash": "^4.14.176",
"@types/mock-require": "^2.0.0",
"@types/react": "^17.0.33",
"@types/react-dom": "^17.0.10",
"@types/semver": "^7.3.9",
"@types/sinon": "^10.0.6",
"@types/supertest": "^2.0.11",
"@types/yargs": "^17.0.2",
"@vue/compiler-sfc": "^3.2.20",
"ava": "^4.0.0-rc.1",
"browserslist": "^4.17.5",
"buffer": "^6.0.3",
"coffee-loader": "^3.0.0",
"coffeescript": "^2.6.1",
"core-js": "^3.19.0",
"eol": "^0.9.1",
"eslint": "^7.28.0",
"husky": "^4.3.0",
"koa": "^2.13.4",
"koa-static": "^5.0.0",
"less": "^3.12.2 || ^4.1.2",
"less-loader": "^9.1.0",
"mock-require": "^3.0.3",
"normalize.css": "^8.0.1",
"playwright": "^1.16.2",
"postcss": "^8.3.11",
"postcss-custom-properties": "^11.0.0",
"prettier": "^2.4.1",
"pretty-quick": "^3.1.1",
"process": "^0.11.10",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-refresh": "^0.10.0",
"resolve-url-loader": "^5.0.0",
"sass": "^1.43.4",
"sass-loader": "^12.3.0",
"sass-resources-loader": "^2.2.4",
"sinon": "^11.1.2",
"stylus": "^0.55.0",
"stylus-loader": "^6.2.0",
"supertest": "^6.1.6",
"temp-sandbox": "^4.0.1",
"ts-loader": "^9.2.6",
"tsc": "^2.0.3",
"typescript": "^4.4.4",
"vue-loader15": "npm:vue-loader@^15.9.8",
"vue-loader16": "npm:vue-loader@^16.8.2",
"vue-template-compiler": "^2.6.14",
"vue2": "npm:vue@^2.6.14",
"vue3": "npm:vue@^3.2.20"
},
"peerDependencies": {
"@babel/core": "^7.15.8",
"@babel/plugin-proposal-object-rest-spread": "^7.15.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.15.8",
"@babel/preset-env": "^7.15.8",
"postcss": "^8.3.11",
"webpack": "^5.60.0",
"webpack-cli": "^4.9.1"
},
"engines": {
"node": ">=12.14.0"
}
}
+25
View File
@@ -0,0 +1,25 @@
<p align="center"><img src="https://laravel.com/assets/img/components/logo-mix.svg" alt="Laravel Mix"></p>
<p align="center">
<a href="https://www.npmjs.com/package/laravel-mix"><img src="https://img.shields.io/node/v/laravel-mix.svg" alt="Node"></a>
<a href="https://www.npmjs.com/package/laravel-mix"><img src="https://img.shields.io/npm/v/laravel-mix.svg" alt="NPM"></a>
<a href="https://npmcharts.com/compare/laravel-mix?minimal=true"><img src="https://img.shields.io/npm/dt/laravel-mix.svg" alt="NPM"></a>
<a href="https://www.npmjs.com/package/laravel-mix"><img src="https://img.shields.io/npm/l/laravel-mix.svg" alt="NPM"></a>
</p>
## Introduction
![Laravel Mix thumbnail](https://repository-images.githubusercontent.com/76991633/43a4fe80-025e-11eb-8b88-bf742e4412a7)
Laravel Mix provides a clean, fluent API for defining basic [webpack](http://github.com/webpack/webpack) build steps for your applications. Mix supports several common CSS and JavaScript pre-processors.
If you've ever been confused about how to get started with module bundling and asset compilation, you will love Laravel Mix!
## Documentation
You may review the initial documentation [here](docs/).
## License
Laravel Mix is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT).
+17
View File
@@ -0,0 +1,17 @@
const { assertSupportedNodeVersion } = require('../src/Engine');
module.exports = async () => {
// @ts-ignore
process.noDeprecation = true;
assertSupportedNodeVersion();
const mix = require('../src/Mix').primary;
require(mix.paths.mix());
await mix.installDependencies();
await mix.init();
return mix.build();
};
+74
View File
@@ -0,0 +1,74 @@
let assert = require('assert');
let File = require('./File');
class Assert {
/**
* Assert that the call the mix.js() is valid.
*
* @param {any} entry
* @param {any} 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'
);
}
/**
* Assert that the calls to 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`
);
}
/**
* Assert that calls to mix.combine() are valid.
*
* @param {string|string[]} src
* @param {File} output
*/
static combine(src, output) {
assert(
typeof src === 'string' || Array.isArray(src),
`mix.combine() requires a valid src string or array.`
);
assert(
output.isFile(),
'mix.combine() requires a full output file path as the second argument. Got ' +
output.path()
);
}
/**
* Assert that the given file exists.
*
* @param {string} file
*/
static exists(file) {
assert(
File.exists(file),
`Whoops, you are trying to compile ${file}, but that file does not exist.`
);
}
}
module.exports = Assert;
+162
View File
@@ -0,0 +1,162 @@
const babel = require('@babel/core');
/**
* @typedef {string|string[]} BabelTargetsBrowserslist
* @typedef {Record<string,string> & {browsers: BabelTargetsBrowserslist}} BabelTargetsPerEngine
* @typedef {BabelTargetsBrowserslist | BabelTargetsPerEngine} BabelTargets
*/
/**
* @typedef {object} AdditionalBabelOptions
* @property {boolean} [cacheDirectory]
* @property {BabelTargets} [targets]
*/
/** @typedef {import("@babel/core").TransformOptions & AdditionalBabelOptions} BabelOptions */
class BabelConfig {
/**
*
* @param {import("./Mix")} [mix]
*/
constructor(mix) {
this.mix = mix || global.Mix;
}
/**
* Generate the appropriate Babel configuration for the build.
*
* @deprecated
*/
static generate() {
return new BabelConfig().generate();
}
/**
* Generate the appropriate Babel configuration for the build.
*/
generate() {
return this.mergeAll([
this.default(),
this.getCustomConfig(this.mix.config.babelConfig),
{
root: this.mix.paths.root(),
babelrc: true,
configFile: true,
babelrcRoots: ['.', this.mix.paths.root()]
}
]);
}
/**
* Fetch the user's .babelrc config file, if any.
*
* @param {string} path
* @deprecated
*/
fetchBabelRc(path) {
const File = require('./File');
return File.exists(path) ? JSON.parse(File.find(path).read()) : {};
}
/**
* Get the babel config setup when using mix.babelConfig()
*
* @internal
* @param {import('@babel/core').TransformOptions} customOptions
*/
getCustomConfig(customOptions) {
const config = babel.loadPartialConfig(customOptions);
return config ? config.options : {};
}
/**
* Merge babel configs
*
* @param {BabelOptions[]} configs
*/
mergeAll(configs) {
const options = configs.reduce((prev, current) => {
const presets = [...(prev.presets || []), ...(current.presets || [])].map(
preset => babel.createConfigItem(preset, { type: 'preset' })
);
const plugins = [...(prev.plugins || []), ...(current.plugins || [])].map(
preset => babel.createConfigItem(preset, { type: 'plugin' })
);
return Object.assign(prev, current, { presets, plugins });
});
options.plugins = this.filterConfigItems(options.plugins || []);
options.presets = this.filterConfigItems(options.presets || []);
return options;
}
/**
* Filter merged presets or plugins
*
* @internal
* @param {import("@babel/core").PluginItem[]} items
* @returns {import("@babel/core").PluginItem[]}
*/
filterConfigItems(items) {
/**
*
* @param {import("@babel/core").PluginItem[]} unique
* @param {import("@babel/core").PluginItem} item
* @returns
*/
function dedupe(unique, item) {
if (item.file != null) {
const toDeleteIndex = unique.findIndex(
element =>
element.file && element.file.resolved === item.file.resolved
);
if (toDeleteIndex >= 0) {
unique.splice(toDeleteIndex, 1);
}
}
return [...unique, item];
}
return items.reduce((unique, configItem) => dedupe(unique, configItem), []);
}
/** @deprecated */
static default() {
return new BabelConfig().default();
}
/**
* Fetch the default Babel configuration.
*
* @internal
* @returns {BabelOptions}
*/
default() {
return {
cacheDirectory: true,
presets: [
['@babel/preset-env', { modules: false, forceAllTransforms: true }]
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-object-rest-spread',
[
'@babel/plugin-transform-runtime',
{
helpers: false
}
]
]
};
}
}
module.exports = BabelConfig;
+269
View File
@@ -0,0 +1,269 @@
let path = require('path');
/** @typedef {import("webpack").Module} Module */
/** @typedef {object} OptimizationSplitChunksCacheGroup */
/**
* @typedef {Object} CacheGroupsContext
* @property {import("webpack").ModuleGraph} moduleGraph
* @property {import("webpack").ChunkGraph} chunkGraph
*/
/** @typedef {any} CacheGroup */
/**
* @typedef {(module: import("webpack").Module, context: CacheGroupsContext) => boolean} ChunkTestCallback
* @typedef {undefined|boolean|string|RegExp|ChunkTestCallback} ChunkTest
*/
/**
* @typedef {(chunk: CacheGroup, id: string) => boolean} ChunkFindCallback
*/
class Chunks {
/** @type {Chunks|null} */
static _instance = null;
/**
*
* @param {import("./Mix")} mix
*/
constructor(mix) {
// TODO: Simplify in Mix 7 -- Here for backwards compat if a plugin creates this class directly
this.mix = mix || global.Mix;
/** @type {{[key: string]: CacheGroup}} */
this.chunks = {};
/** @type {import("./builder/Entry.js")|null} */
this.entry = null;
this.runtime = false;
}
/** @deprecated */
makeCurrent() {
Chunks._instance = this;
}
/**
* @deprecated
* @return {Chunks}
*/
static instance() {
return Chunks._instance || this.reset();
}
/**
* @deprecated
* @return {Chunks}
*/
static reset() {
return (Chunks._instance = new Chunks(global.Mix));
}
/**
*
* @param {string} id A unique identifier for this chunk. Multiple chunks with the same ID are merged.
* @param {string} path The output path for this chunk
* @param {ChunkTest|ChunkTest[]} test A test that tells webpack how to determine what to put in this chunk
* @param {Partial<CacheGroup>} attrs
*/
add(id, path, test, attrs = {}) {
this.create(id, path, attrs).addTo(id, test);
}
/**
*
* @param {string} id A unique identifier for this chunk. Multiple chunks with the same ID are merged.
* @param {string} path The output path for this chunk
* @param {Partial<OptimizationSplitChunksCacheGroup>} attrs
*/
create(id, path, attrs = {}) {
this.chunks[id] = this.chunks[id] || {
name: path,
...attrs
};
return this;
}
/**
*
* @param {string} idOrPath
* @param {ChunkTest|ChunkTest[]} test
*/
addTo(idOrPath, test) {
const chunk = this.find(idOrPath);
if (Array.isArray(test)) {
test = this._checkAllTests(test);
}
if (chunk.test) {
test = this._checkAnyTests([chunk.test, test]);
}
chunk.test = test;
return this;
}
/**
*
* @param {string|ChunkFindCallback} idOrPath
* @returns {CacheGroup|null}
*/
find(idOrPath) {
if (typeof idOrPath === 'string') {
if (this.chunks[idOrPath]) {
return this.chunks[idOrPath];
}
return this.find((_, id) => id === idOrPath);
}
const item = Object.entries(this.chunks).find(([id, chunk]) =>
idOrPath(chunk, id)
);
return item ? item[1] : null;
}
/**
* @returns {import('webpack').Configuration}
*/
config() {
return {
optimization: {
...this.runtimeChunk(),
...this.splitChunks()
}
};
}
runtimeChunk() {
if (!this.runtime || !this.entry) {
return {};
}
return {
runtimeChunk: {
name: path
.join(this.mix.config.runtimeChunkPath || this.entry.base, 'manifest')
.replace(/\\/g, '/')
}
};
}
splitChunks() {
return {
splitChunks: {
...this.cacheGroups()
}
};
}
cacheGroups() {
return {
cacheGroups: {
/** @type {false} */
default: false,
/** @type {false} */
defaultVendors: false,
...this.chunks
}
};
}
/**
* Check to see if a chunk should be included based on multiple tests
*
* This is for internal use only and may be changed or removed at any time
*
* @internal
*
* @param {(undefined|boolean|string|RegExp|Function)[]} tests
* @returns {ChunkTestCallback}
*/
_checkAllTests(tests) {
return (module, context) =>
tests.every(test => this._checkTest(test, module, context));
}
/**
* Check to see if a chunk should be included based on multiple tests
*
* This is for internal use only and may be changed or removed at any time
*
* @internal
*
* @param {(undefined|boolean|string|RegExp|Function)[]} tests
* @returns {ChunkTestCallback}
*/
_checkAnyTests(tests) {
return (module, context) =>
tests.some(test => this._checkTest(test, module, context));
}
/**
* Check to see if a chunk should be included
*
* NOTE: This repeats the code from the SplitChunksPlugin checkTest function
* This is for internal use only and may be changed or removed at any time
*
* @internal
*
* @param {undefined|boolean|string|RegExp|Function} test test option
* @param {import("webpack").Module} module the module
* @param {CacheGroupsContext} context context object
* @returns {boolean} true, if the module should be selected
*/
_checkTest(test, module, context) {
if (this._checkModuleTest(test, module, context)) {
return true;
}
const issuer = context.moduleGraph.getIssuer(module);
if (issuer) {
return this._checkTest(test, issuer, context);
}
return false;
}
/**
* Check to see if a chunk should be included
*
* NOTE: This repeats the code from the SplitChunksPlugin checkTest function
* This is for internal use only and may be changed or removed at any time
*
* @internal
*
* @param {undefined|boolean|string|RegExp|Function} test test option
* @param {import("webpack").Module} module the module
* @param {any} context context object
* @returns {boolean} true, if the module should be selected
*/
_checkModuleTest(test, module, context) {
if (test === undefined) return true;
if (typeof test === 'function') {
return test(module, context);
}
if (typeof test === 'boolean') return test;
if (typeof test === 'string') {
const name = module.nameForCondition();
return (name && name.startsWith(test)) || false;
}
if (test instanceof RegExp) {
const name = module.nameForCondition();
return (name && test.test(name)) || false;
}
return false;
}
}
module.exports.Chunks = Chunks;
+103
View File
@@ -0,0 +1,103 @@
let Log = require('./Log');
let PackageManager = require('./PackageManager');
let { PackageDependency } = require('./PackageDependency');
class Dependencies {
/** @type {PackageDependency[]} */
items = [];
requiresReload = false;
/**
* Create a new Dependencies instance.
*
* @param {import('./PackageDependency').Dependency[]} deps
*/
enqueue(deps, requiresReload = false) {
this.items.push(...deps.filter(dep => dep).map(PackageDependency.from));
this.requiresReload = this.requiresReload || requiresReload;
return this;
}
/**
* Install all dependencies that aren't available.
*/
async install() {
const packages = await this.installablePackages();
if (!packages.length) {
return;
}
await this.execute(this.buildInstallCommand(packages));
}
async installablePackages() {
const results = await Promise.all(
this.items.map(async dep => {
if (await dep.needsInstallation()) {
return dep;
}
return null;
})
);
return /** @type {PackageDependency[]} */ (results.filter(Boolean));
}
/**
* Execute the provided console command.
*
* @param {string} command
*/
async execute(command) {
Log.feedback(
'Additional dependencies must be installed. This will only take a moment.'
);
Log.feedback(`Running: ${command}`);
const childProcess = require('child_process');
const { promisify } = require('util');
await promisify(childProcess.exec)(command);
this.respond();
}
/**
* Build the dependency install command.
*
* @param {PackageDependency[]} deps
*/
buildInstallCommand(deps) {
const list = deps.map(dep => dep.package).join(' ');
switch (PackageManager.detect()) {
case 'npm':
return `npm install ${list} --save-dev --legacy-peer-deps`;
case 'yarn':
return `yarn add ${list} --dev`;
}
}
/**
* Complete the install process.
*/
respond() {
if (!this.requiresReload) {
return;
}
Log.feedback('Finished. Please run Mix again.');
if (process.env.NODE_ENV !== 'test') {
process.exit(1);
}
}
}
module.exports = Dependencies;
+50
View File
@@ -0,0 +1,50 @@
/** @typedef {(...args: any) => void|Promise<void>} Handler */
const { concat } = require('lodash');
class Dispatcher {
/**
* Create a new Dispatcher instance.
*/
constructor() {
/** @type {Record<string, Handler[]>} */
this.events = {};
}
/**
* Listen for the given event.
*
* @param {string|string[]} events
* @param {Handler} 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 {any} [data]
*/
async fire(event, data) {
if (!this.events[event]) return false;
return Promise.all(this.events[event].map(handler => handler(data)));
}
/**
* Fetch all registered event listeners.
*/
all() {
return this.events;
}
}
module.exports = Dispatcher;
+10
View File
@@ -0,0 +1,10 @@
const semver = require('semver');
const pkg = require('../package.json');
module.exports.assertSupportedNodeVersion = function assertSupportedNodeVersion() {
if (!semver.satisfies(process.versions.node, pkg.engines.node)) {
throw new Error(
`You are using an unsupported version of Node. Please update to at least Node v12.14`
);
}
};
+370
View File
@@ -0,0 +1,370 @@
let os = require('os');
let md5 = require('md5');
let path = require('path');
let fs = require('fs-extra');
let Terser = require('terser');
let UglifyCss = require('clean-css');
const { escapeRegExp } = require('lodash');
class File {
/**
* Create a new instance.
*
* @param {string} filePath
*/
constructor(filePath) {
this.absolutePath = path.resolve(filePath);
this.filePath = this.relativePath();
this.segments = this.parse();
}
/**
* Static constructor.
*
* @param {string} file
*/
static find(file) {
return new File(file);
}
/**
* Get the size of the file.
*/
size() {
return fs.statSync(this.path()).size;
}
/**
* Determine if the given file exists.
*
* @param {string} file
*/
static exists(file) {
return fs.existsSync(file);
}
/**
* Determine if the given file exists.
*/
exists() {
return fs.existsSync(this.path());
}
normalizedOutputPath() {
let path = this.pathFromPublic(this.mix.config.publicPath);
path = File.stripPublicDir(path);
return path
.replace(/\.(js|css)$/, '')
.replace(/\\/g, '/')
.replace(/^\//, '');
}
/**
* Delete/Unlink the current file.
*/
delete() {
if (fs.existsSync(this.path())) {
fs.unlinkSync(this.path());
}
}
/**
* Get the name of the file.
*/
name() {
return this.segments.file;
}
/**
* Get the name of the file, minus the extension.
*/
nameWithoutExtension() {
return this.segments.name;
}
/**
* Get the extension of the file.
*/
extension() {
return this.segments.ext;
}
/**
* Get the absolute path to the file.
*/
path() {
return this.absolutePath;
}
/**
* Get the relative path to the file, from the project root.
*/
relativePath() {
return path.relative(this.mix.paths.root(), this.path());
}
/**
* Get the relative path to the file, from the project root.
*/
relativePathWithoutExtension() {
return path.relative(this.mix.paths.root(), this.pathWithoutExtension());
}
/**
* Get the absolute path to the file, minus the extension.
*/
pathWithoutExtension() {
return this.segments.pathWithoutExt;
}
/**
* Force the file's relative path to begin from the public path.
*
* @param {string|null} [publicPath]
*/
forceFromPublic(publicPath = null) {
publicPath = publicPath || this.mix.config.publicPath;
if (!this.relativePath().startsWith(publicPath)) {
return new File(path.join(publicPath, this.relativePath()));
}
return this;
}
/**
*
* @param {string} filePath
* @param {string|null} publicPath
*/
static stripPublicDir(filePath, publicPath = null) {
let publicDir = path.basename(publicPath || this.mix.config.publicPath);
publicDir = escapeRegExp(publicDir);
return filePath.replace(new RegExp(`^[/\\\\]?${publicDir}[/\\\\]`), '');
}
/**
* Get the path to the file, starting at the project's public dir.
*
* @param {string|null} [publicPath]
*/
pathFromPublic(publicPath) {
publicPath = publicPath || this.mix.config.publicPath;
let extra = this.filePath.startsWith(publicPath) ? publicPath : '';
// If the path starts with the public folder remove it
if (
this.filePath.startsWith(
path.normalize(`${publicPath}/${path.basename(publicPath)}`)
)
) {
extra += `/${path.basename(publicPath)}`;
}
return path.normalize(
'/' + path.relative(this.mix.paths.root(extra), this.path())
);
}
/**
* Get the path to the file without query string.
*/
pathWithoutQueryString() {
const queryStringIndex = this.path().indexOf('?');
return queryStringIndex < 0
? this.path()
: this.path().substr(0, queryStringIndex);
}
/**
* Get the base directory of the file.
*/
base() {
return this.segments.base;
}
/**
* Determine if the file is a directory.
*/
isDirectory() {
return this.segments.isDir;
}
/**
* Determine if the path is a file, and not a directory.
*/
isFile() {
return this.segments.isFile;
}
/**
* Write the given contents to the file.
*
* @param {string} body
*/
write(body) {
if (typeof body === 'object') {
body = JSON.stringify(body, null, 4);
}
body = body + os.EOL;
fs.writeFileSync(this.absolutePath, body);
return this;
}
/**
* Read the file's contents.
*/
read() {
return fs.readFileSync(this.pathWithoutQueryString(), 'utf8');
}
/**
* Calculate the proper version hash for the file.
*/
version() {
return md5(this.read());
}
/**
* Create all nested directories.
*/
makeDirectories() {
fs.mkdirSync(this.base(), { mode: 0o777, recursive: true });
return this;
}
/**
* Copy the current file to a new location.
*
* @param {string} destination
*/
copyTo(destination) {
fs.copySync(this.path(), destination);
return this;
}
/**
* Minify the file, if it is CSS or JS.
*/
async minify() {
if (this.extension() === '.js') {
const output = await Terser.minify(
this.read(),
this.mix.config.terser.terserOptions
);
if (output.code !== undefined) {
this.write(output.code);
}
}
if (this.extension() === '.css') {
const output = await new UglifyCss(this.mix.config.cleanCss).minify(
this.read()
);
this.write(output.styles);
}
return this;
}
/**
* Rename the file.
*
* @param {string} to
*/
rename(to) {
to = path.join(this.base(), to);
fs.renameSync(this.path(), to);
return new File(to);
}
/**
* It can append to the current path.
*
* @param {string} append
*/
append(append) {
return new File(path.join(this.path(), append));
}
/**
* Determine if the file path contains the given text.
*
* @param {string} text
*/
contains(text) {
return this.path().includes(text);
}
/**
* Parse the file path.
*/
parse() {
let parsed = path.parse(this.absolutePath);
let isDir = this.checkIsDir(parsed);
return {
path: this.filePath,
absolutePath: this.absolutePath,
pathWithoutExt: path.join(parsed.dir, `${parsed.name}`),
isDir,
isFile: !isDir,
name: parsed.name,
ext: parsed.ext,
file: parsed.base,
base: parsed.dir
};
}
/**
* This is a bit more involved check to verify if a file is a directory
*
* @param {path.ParsedPath} parsed
* @private
*/
checkIsDir(parsed) {
// 1. If the file exists and is a directory
try {
return fs.lstatSync(this.absolutePath).isDirectory();
} catch (err) {
//
}
// 2. If the path ends in a slash
if (this.absolutePath.endsWith('/')) {
return true;
}
// 3. Pevious logic: No extension & does not end in a wildcard
return !parsed.ext && !parsed.name.endsWith('*');
}
// TODO: Can we refactor this to remove the need for implicit global? Or does this one make sense to leave as is?
get mix() {
// TODO: Use warning-less version here
return global.Mix;
}
static get mix() {
// TODO: Use warning-less version here
return global.Mix;
}
}
module.exports = File;
+129
View File
@@ -0,0 +1,129 @@
let concat = require('concat');
let path = require('path');
let fs = require('fs');
let babel = require('@babel/core');
let glob = require('glob');
let _ = require('lodash');
let Log = require('./Log');
let File = require('./File');
class FileCollection {
/**
* Create a new FileCollection instance.
*
* @param {string|string[]} files
*/
constructor(files = []) {
/** @type {string[]} */
this.files = _.concat([], files);
}
/**
* Fetch the underlying files.
*/
get() {
return this.files;
}
/**
* Merge all files in the collection into one.
*
* @param {File} output
* @param {boolean} wantsBabel
*/
async merge(output, wantsBabel = false) {
/** @type {string} */
// @ts-ignore
const contents = await concat(this.files, output.makeDirectories().path());
if (this.shouldCompileWithBabel(wantsBabel, output)) {
const code = this.babelify(contents);
if (code) {
output.write(code);
}
}
return new File(output.makeDirectories().path());
}
/**
* Determine if we should add a Babel pass to the concatenated file.
*
* @param {Boolean} wantsBabel
* @param {File} output
*/
shouldCompileWithBabel(wantsBabel, output) {
return wantsBabel && output.extension() === '.js';
}
/**
* Apply Babel to the given contents.
*
* @param {string} contents
*/
babelify(contents) {
let babelConfig = this.mix.config.babel();
delete babelConfig.cacheDirectory;
const result = babel.transform(contents, babelConfig);
return result && result.code;
}
/**
* Copy the src files to the given destination.
*
* @param {File} destination
* @param {string[]|File} [src]
* @return {void|string}
*/
copyTo(destination, src = this.files) {
this.assets = this.assets || [];
this.destination = destination;
if (Array.isArray(src)) {
src.forEach(file => this.copyTo(destination, new File(file)));
return;
}
if (src.isDirectory()) {
src.copyTo(destination.path());
this.assets = fs.readdirSync(src.path()).map(file => {
return new File(path.resolve(destination.path(), file));
});
return;
}
if (src.contains('*')) {
let files = glob.sync(src.path(), { nodir: true });
if (!files.length) {
Log.feedback(`Notice: The ${src.path()} search produced no matches.`);
}
return this.copyTo(destination, files);
}
if (destination.isDirectory()) {
destination = destination.append(src.name());
}
src.copyTo(destination.path());
this.assets = this.assets.concat(destination);
return destination.path();
}
get mix() {
return global.Mix;
}
}
module.exports = FileCollection;
+73
View File
@@ -0,0 +1,73 @@
let path = require('path');
let File = require('./File');
class HotReloading {
/**
*
* @param {import('./Mix')} mix
*/
constructor(mix) {
this.mix = mix;
}
record() {
this.clean();
if (!this.mix.config.hmr) {
return;
}
this.hotFile().write(
`${this.http()}://${this.mix.config.hmrOptions.host}:${this.port()}`
);
process.on('exit', () => this.clean());
process.on('SIGINT', () => this.clean());
process.on('SIGHUP', () => this.clean());
}
hotFile() {
return new File(path.join(this.mix.config.publicPath, 'hot'));
}
http() {
return process.argv.includes('--https') ? 'https' : 'http';
}
port() {
return process.argv.includes('--port')
? process.argv[process.argv.indexOf('--port') + 1]
: this.mix.config.hmrOptions.port;
}
clean() {
this.hotFile().delete();
}
/** @deprecated */
static record() {
return new HotReloading(global.Mix).record();
}
/** @deprecated */
static hotFile() {
return new HotReloading(global.Mix).hotFile();
}
/** @deprecated */
static http() {
return new HotReloading(global.Mix).http();
}
/** @deprecated */
static port() {
return new HotReloading(global.Mix).port();
}
/** @deprecated */
static clean() {
return new HotReloading(global.Mix).clean();
}
}
module.exports = HotReloading;
+154
View File
@@ -0,0 +1,154 @@
const chalk = require('chalk');
/**
* @typedef {object} LogMessage
* @property {string} text
* @property {'info' | 'warn' | 'error'} type
**/
/**
* @typedef {'default' | 'green' | 'red'} LogColor
**/
class Log {
/**
* Determine if we are in test mode.
*/
static testing = false;
/**
* All logged messages.
*
* @type {string[]}
*/
static fakedLogs = [];
/**
* Log basic info to the console.
*
* @param {string} message
* @param {LogColor} color
*/
static info(message, color = 'default') {
if (Log.testing) {
Log.fakedLogs.push(message);
return;
}
console.log(Log.colors()[color], message);
Log.reset();
}
/**
*
* @param {LogMessage} message
*/
static message(message) {
if (Log.testing) {
Log.fakedLogs.push(message.text);
return;
}
/** @type {string} */
let prefix = '';
if (message.type === 'info') {
prefix = ' INFO ';
} else if (message.type === 'warn') {
prefix = ' WARN ';
} else if (message.type === 'error') {
prefix = ' ERR ';
}
const line = message.text.replace(/\n/g, '\n' + ' '.repeat(prefix.length + 1));
if (message.type === 'info') {
console.warn(`${chalk.bgBlue.white(prefix)} ${chalk.white(line)}`);
} else if (message.type === 'warn') {
console.warn(`${chalk.bgYellow.black(prefix)} ${chalk.yellow(line)}`);
} else if (message.type === 'error') {
console.warn(`${chalk.bgRed.white(prefix)} ${chalk.red(line)}`);
}
}
/**
* Log feedback info to the console.
*
* @param {string} message
* @param {LogColor} color
*/
static feedback(message, color = 'green') {
Log.line('\t' + message, color);
}
/**
* Log error info to the console.
*
* @param {string} message
* @param {LogColor} color
*/
static error(message, color = 'red') {
Log.line(message, color);
}
/**
* Log a new line of info to the console.
*
* @param {string} message
* @param {LogColor} color
*/
static line(message, color = 'default') {
Log.info(message, color);
}
/**
* Reset the default color for future console.logs.
*/
static reset() {
console.log(Log.colors()['default'], '');
}
/**
* Enter testing mode.
*/
static fake() {
Log.testing = true;
}
/**
* Exit testing mode.
*/
static restore() {
Log.testing = false;
Log.fakedLogs = [];
}
/**
* Determine if the given message was logged.
*
* @param {string} message
*/
static received(message) {
let result = Log.fakedLogs.some(log => log.includes(message));
Log.restore();
return result;
}
/**
* The colors lookup table.
*/
static colors() {
return {
default: '\x1b[0m',
green: '\x1b[32m',
red: '\x1b[31m'
};
}
}
module.exports = Log;
+172
View File
@@ -0,0 +1,172 @@
let collect = require('collect.js');
let path = require('path');
let File = require('./File');
let hasWarnedAboutManifestChange = false;
class Manifest {
/**
* Create a new Manifest instance.
*
* @param {string} name
* @param {import('./Mix.js')} [mix]
*/
constructor(name = 'mix-manifest.json', mix = undefined) {
this.mix = mix || global.Mix;
/** @type {Record<string, string>} */
this.manifest = {};
this.name = name;
}
/**
* Get the underlying manifest collection.
* @param {string|null} [file]
* @returns {string | Record<string, string>}
*/
get(file = null) {
if (file) {
return path.posix.join(
this.mix.config.publicPath,
this.manifest[this.normalizePath(file)]
);
}
return Object.fromEntries(
Object.entries(this.manifest).sort((a, b) => a[0].localeCompare(b[0]))
);
}
/**
* Add the given path to the manifest file.
*
* @param {string} filePath
*/
add(filePath) {
filePath = this.normalizePath(filePath);
let original = filePath.replace(/\?id=\w+/, '');
this.manifest[original] = filePath;
return this;
}
/**
* Add a new hashed key to the manifest.
*
* @param {string} file
*/
hash(file) {
let hash = new File(path.join(this.mix.config.publicPath, file)).version();
let filePath = this.normalizePath(file);
this.manifest[filePath] = filePath + '?id=' + hash;
return this;
}
/**
* Transform the Webpack stats into the shape we need.
*
* @param {object} stats
*/
transform(stats) {
this.flattenAssets(stats).forEach(this.add.bind(this));
return this;
}
/**
* Refresh the mix-manifest.js file.
*/
refresh() {
if (this.path() === null) {
return;
}
File.find(this.path()).makeDirectories().write(this.manifest);
}
/**
* Retrieve the JSON output from the manifest file.
*/
read() {
if (this.path() === null) {
return {};
}
return JSON.parse(File.find(this.path()).read());
}
/**
* Get the path to the manifest file.
*/
path() {
// Backwards compat for plugis that manually swapped out the manifest plugin with a different name
if (this.name !== 'mix-manifest.json') {
if (!hasWarnedAboutManifestChange) {
hasWarnedAboutManifestChange = true;
console.warn(
`You can now customize the name of the manifest using mix.options(${JSON.stringify(
{ manifest: this.name }
)}) instead of changing the manifest name directly.`
);
}
return path.join(this.mix.config.publicPath, this.name);
}
if (!this.mix.config.manifest) {
return null;
}
return path.join(this.mix.config.publicPath, this.mix.config.manifest);
}
/**
* Flatten the generated stats assets into an array.
*
* @param {Object} stats
*/
flattenAssets(stats) {
let assets = Object.assign({}, stats.assetsByChunkName);
// If there's a temporary mix.js chunk, we can safely remove it.
if (assets.mix) {
assets.mix = collect(assets.mix).except('mix.js').all();
}
return (
collect(assets)
.flatten()
// Don't add hot updates to manifest
.filter(name => name.indexOf('hot-update') === -1)
.all()
);
}
/**
* Prepare the provided path for processing.
*
* @param {string} filePath
*/
normalizePath(filePath) {
if (
this.mix.config.publicPath &&
filePath.startsWith(this.mix.config.publicPath)
) {
filePath = filePath.substring(this.mix.config.publicPath.length);
}
filePath = filePath.replace(/\\/g, '/');
if (!filePath.startsWith('/')) {
filePath = '/' + filePath;
}
return filePath;
}
}
module.exports = Manifest;
+280
View File
@@ -0,0 +1,280 @@
let { Chunks } = require('./Chunks');
let buildConfig = require('./config');
let ComponentRegistrar = require('./components/ComponentRegistrar');
let Components = require('./components/Components');
let Dependencies = require('./Dependencies');
let Dispatcher = require('./Dispatcher');
let Dotenv = require('dotenv');
let File = require('./File');
let HotReloading = require('./HotReloading');
let Manifest = require('./Manifest');
let Paths = require('./Paths');
let WebpackConfig = require('./builder/WebpackConfig');
let { Resolver } = require('./Resolver');
const Log = require('./Log');
/** @typedef {import("./tasks/Task")} Task */
class Mix {
/** @type {Mix|null} */
static _primary = null;
/** @type {Record<string, boolean>} */
static _hasWarned = {};
/**
* Create a new instance.
*/
constructor() {
/** @type {ReturnType<buildConfig>} */
this.config = buildConfig(this);
this.chunks = new Chunks(this);
this.components = new Components();
this.dispatcher = new Dispatcher();
this.manifest = new Manifest('mix-manifest.json', this);
this.paths = new Paths();
this.registrar = new ComponentRegistrar(this);
this.webpackConfig = new WebpackConfig(this);
this.hot = new HotReloading(this);
this.resolver = new Resolver();
this.dependencies = new Dependencies();
this.logger = Log;
/** @type {Task[]} */
this.tasks = [];
this.booted = false;
this.bundlingJavaScript = false;
/**
* @internal
* @type {boolean}
**/
this.initialized = false;
/**
* @internal
* @type {false | null | string | string[] | Record<string, string | string[]>}
*/
this.globalStyles = null;
/**
* @internal
* @type {boolean|string}
**/
this.extractingStyles = false;
}
/**
* @internal
*/
static get primary() {
return Mix._primary || (Mix._primary = new Mix());
}
/**
* @internal
*/
async build() {
if (!this.booted) {
console.warn(
'Mix was not set up correctly. Please ensure you import or require laravel-mix in your mix config.'
);
this.boot();
}
return this.webpackConfig.build();
}
/**
* @internal
*/
boot() {
if (this.booted) {
return this;
}
this.booted = true;
// Load .env
Dotenv.config();
// If we're using Laravel set the public path by default
if (this.sees('laravel')) {
this.config.publicPath = 'public';
}
this.listen('init', () => this.hot.record());
this.makeCurrent();
return this;
}
/**
* @internal
*/
async installDependencies() {
await this.dispatch('internal:gather-dependencies');
await this.dependencies.install();
}
/**
* @internal
*/
init() {
if (this.initialized) {
return;
}
this.initialized = true;
return this.dispatch('init', this);
}
/**
* @return {import("laravel-mix").Api}
*/
get api() {
if (!this._api) {
this._api = this.registrar.installAll();
// @ts-ignore
this._api.inProduction = () => this.config.production;
}
// @ts-ignore
return this._api;
}
/**
* Determine if the given config item is truthy.
*
* @param {string} tool
*/
isUsing(tool) {
// @ts-ignore
return !!this.config[tool];
}
/**
* Determine if Mix is executing in a production environment.
*/
inProduction() {
return this.config.production;
}
/**
* Determine if Mix should use HMR.
*/
isHot() {
return process.argv.includes('--hot');
}
/**
* Determine if Mix should watch files for changes.
*/
isWatching() {
return this.isHot() || process.argv.includes('--watch');
}
/**
* Determine if polling is used for file watching
*/
isPolling() {
const hasPollingOption = process.argv.some(arg =>
arg.includes('--watch-options-poll')
);
return this.isWatching() && hasPollingOption;
}
/**
* Determine if Mix sees a particular tool or framework.
*
* @param {string} tool
* @deprecated
*/
sees(tool) {
if (tool === 'laravel') {
return File.exists('./artisan');
}
return false;
}
/**
* Determine if the given npm package is installed.
*
* @param {string} npmPackage
* @deprecated
*/
seesNpmPackage(npmPackage) {
return this.resolver.has(npmPackage);
}
/**
* Queue up a new task.
*
* @param {Task} task
*/
addTask(task) {
this.tasks.push(task);
}
/**
* Listen for the given event.
*
* @param {string|string} event
* @param {import('./Dispatcher').Handler} callback
*/
listen(event, callback) {
this.dispatcher.listen(event, callback);
}
/**
* Dispatch the given event.
*
* @param {string} event
* @param {any | (() => any)} [data]
*/
async dispatch(event, data) {
if (typeof data === 'function') {
data = data();
}
return this.dispatcher.fire(event, data);
}
/**
* @param {string} name
* @internal
*/
resolve(name) {
return this.resolver.get(name);
}
/**
* @internal
*/
makeCurrent() {
// Set up some globals
// @ts-ignore
global.Config = this.config;
// @ts-ignore
global.Mix = this;
// @ts-ignore
global.webpackConfig = this.webpackConfig;
this.chunks.makeCurrent();
return this;
}
}
module.exports = Mix;
+59
View File
@@ -0,0 +1,59 @@
/**
* @typedef {object} DependencyObject
* @property {string} package
* @property {(name: string) => boolean} [check]
*/
/**
* @typedef {string | DependencyObject} Dependency
*/
exports.PackageDependency = class PackageDependency {
/** @type {string} */
package;
/** @type {string} */
name;
/** @type {(name: string) => boolean | Promise<boolean>} */
checkFn;
/**
*
* @param {Dependency} dep
*/
static from(dep) {
const depObj = typeof dep === 'string' ? { package: dep } : dep;
return new PackageDependency(depObj.package, depObj.check);
}
/**
*
* @param {string} pkg
* @param {(name: string) => boolean} [checkFn]
*/
constructor(pkg, checkFn) {
this.package = pkg;
this.name = pkg.replace(/(?!^@)@.+$/, '');
this.checkFn = checkFn || (() => true);
}
async needsInstallation() {
return !(await this.isInstalled()) || !(await this.isValid());
}
async isInstalled() {
try {
require.resolve(this.name);
return true;
} catch (e) {
return false;
}
}
async isValid() {
return this.checkFn(this.name);
}
};
+26
View File
@@ -0,0 +1,26 @@
const File = require('./File');
const Paths = require('./Paths');
class PackageManager {
static detect() {
const execPath = process.env.npm_execpath || '';
if (
execPath.endsWith('yarn.js') ||
execPath.endsWith('yarn') ||
PackageManager.hasYarnLockFile()
) {
return 'yarn';
}
return 'npm';
}
static hasYarnLockFile() {
const paths = new Paths();
return File.exists(paths.root('yarn.lock'));
}
}
module.exports = PackageManager;
+56
View File
@@ -0,0 +1,56 @@
let path = require('path');
class Paths {
/**
* Create a new Paths instance.
*/
constructor() {
// TODO: Refactor setup to allow removing this check
if (process.env.NODE_ENV === 'test') {
this.rootPath = path.resolve(__dirname, '../');
} else {
this.rootPath = process.cwd();
}
}
/**
* 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() {
const path = this.root(
process.env && process.env.MIX_FILE ? process.env.MIX_FILE : 'webpack.mix'
);
try {
require.resolve(`${path}.cjs`);
return `${path}.cjs`;
} catch (err) {
//
}
return path;
}
/**
* Determine the project root.
*
* @param {string} [append]
*/
root(append = '') {
return path.resolve(this.rootPath, append);
}
}
module.exports = Paths;
+83
View File
@@ -0,0 +1,83 @@
module.exports = class PostCssPluginsFactory {
/**
* Create a new instance.
* @param {import('./Mix.js')} context
*/
constructor(context) {
this.context = context;
/** @type {import('postcss').AcceptedPlugin[]} */
this.plugins = [];
}
/**
* Load all relevant PostCSS plugins.
*
* @param {import('postcss').AcceptedPlugin[] | undefined} plugins
*/
load(plugins = []) {
this.loadGlobalPlugins();
this.loadLocalPlugins(plugins);
this.loadAutoprefixer();
this.loadCssNano();
return this.plugins;
}
/**
* Load the user's postcss.config.js file, if any.
*
* @deprecated postcss-loader already does this on its own
*/
loadConfigFile() {
let postcssrc = require('postcss-load-config');
try {
this.plugins = [...this.plugins, ...postcssrc.sync().plugins];
} catch (e) {
// No postcss.config.js file exists.
}
}
/**
* Load any global postcss plugins that have been passed to Mix.
*/
loadGlobalPlugins() {
if (this.context.config.postCss && this.context.config.postCss.length) {
this.plugins = [...this.plugins, ...this.context.config.postCss];
}
}
/**
* Load any postcss plugins that were passed to the Mix command.
*
* @param {import('postcss').AcceptedPlugin[] | undefined} plugins
*/
loadLocalPlugins(plugins) {
if (plugins && plugins.length) {
this.plugins = [...this.plugins, ...plugins];
}
}
/**
* Add autoprefixer to the plugins list.
*/
loadAutoprefixer() {
if (this.context.config.autoprefixer) {
this.plugins.push(require('autoprefixer')(this.context.config.autoprefixer));
}
}
/**
* Add CSSNano to the plugins list.
*/
loadCssNano() {
if (this.context.config.production && this.context.config.cssNano !== false) {
this.plugins.push(
require('cssnano')({
preset: ['default', this.context.config.cssNano]
})
);
}
}
};
+57
View File
@@ -0,0 +1,57 @@
class Resolver {
constructor() {
/** @type {Record<string, string>} */
this.aliases = {};
}
/**
*
* @param {string} name
*/
get(name) {
if (this.aliases[name] !== undefined) {
return this.aliases[name];
}
try {
return require.resolve(name);
} catch (err) {
return name;
}
}
/**
*
* @param {string} name
* @returns {boolean}
*/
has(name) {
if (this.aliases[name] !== undefined) {
return true;
}
try {
require.resolve(name);
return true;
} catch (e) {
return false;
}
}
/**
*
* @param {string} name
* @param {string} newName
* @internal
*/
alias(name, newName) {
this.aliases[name] = require.resolve(newName);
}
clear() {
this.aliases = {};
}
}
module.exports.Resolver = Resolver;
+57
View File
@@ -0,0 +1,57 @@
class VueVersion {
/**
*
* @param {import("./Mix")} mix
*/
constructor(mix) {
this.mix = mix;
}
/**
* Vue versions that are supported by Mix.
*
* @returns {number[]}
*/
supported() {
return [2, 3];
}
/**
* Detect and validate the current version of Vue for the build.
*
* @param {string|number|null|undefined} [version]
* @returns {number}
*/
detect(version) {
version = parseInt(`${version}`);
if (!version || isNaN(version)) {
try {
return this.detect(require(this.mix.resolve('vue')).version);
} catch (e) {
this.fail();
}
}
if (this.supported().includes(version)) {
return version;
}
this.fail();
}
/**
* Abort and log that a supported version of Vue wasn't found.
* @returns {never}
*/
fail() {
this.mix.logger.error(
`We couldn't find a supported version of Vue in your project. ` +
`Please ensure that it's installed (npm install vue).`
);
throw new Error('Unable to detect vue version');
}
}
module.exports = VueVersion;
+113
View File
@@ -0,0 +1,113 @@
let path = require('path');
let File = require('../File');
class Entry {
/**
* Create a new Entry instance.
* @param {import("../Mix")} mix
*/
constructor(mix) {
// TODO: Simplify in Mix 7 -- Here for backwards compat if a plugin creates this class directly
this.mix = mix || global.Mix;
/** @type {Record<string, string[]>} */
this.structure = {};
this.base = '';
}
/**
* Fetch the underlying entry structure.
*/
get() {
return this.structure;
}
/**
* Get the object keys for the structure.
*/
keys() {
return Object.keys(this.structure);
}
/**
* Add a key key-val pair to the structure.
*
* @param {string} key
* @param {any} val
*/
add(key, val) {
this.structure[key] = (this.structure[key] || []).concat(val);
return this;
}
/**
* Add a new key-val pair, based on a given output path.
*
* @param {any} val
* @param {File} output
* @param {File} fallback
*/
addFromOutput(val, output, fallback) {
output = this.normalizePath(output, fallback);
return this.add(this.createName(output), val);
}
/**
* Add a default entry script to the structure.
*/
addDefault() {
this.add('mix', new File(path.resolve(__dirname, 'mock-entry.js')).path());
}
hasDefault() {
return (this.structure.mix || []).some(path => path.includes('mock-entry.js'));
}
/**
* Build the proper entry name, based on a given output.
*
* @param {File} output
*/
createName(output) {
let name = output
.pathFromPublic(this.mix.config.publicPath)
.replace(/\.js$/, '')
.replace(/\\/g, '/');
this.base = path.parse(name).dir;
return name;
}
/**
* Normalize the given output path.
*
* @param {File} output
* @param {File} fallback
*/
normalizePath(output, fallback) {
// All output paths need to start at the project's public dir.
let pathFromPublicDir = output.pathFromPublic();
if (
!pathFromPublicDir.startsWith('/' + this.mix.config.publicPath) &&
!pathFromPublicDir.startsWith('\\' + this.mix.config.publicPath)
) {
output = new File(
path.join(this.mix.config.publicPath, output.pathFromPublic())
);
}
// If the output points to a directory, we'll grab a file name from the fallback src.
if (output.isDirectory()) {
output = new File(
path.join(output.filePath, fallback.nameWithoutExtension() + '.js')
);
}
return output;
}
}
module.exports = Entry;
+291
View File
@@ -0,0 +1,291 @@
// @ts-nocheck
// This code was taken from v4.2.2 of webpack-merge for backwards compatability.
// With minor modifications since Mix is not transpiled before running in Node.
// Sources:
// 1. https://github.com/survivejs/webpack-merge/blob/v4.2.2/src/index.js
// 2. https://github.com/survivejs/webpack-merge/blob/v4.2.2/src/join-arrays-smart.js
const { isEqual, mergeWith, differenceWith, unionWith } = require('lodash');
function isRule(key) {
return ['preLoaders', 'loaders', 'postLoaders', 'rules'].indexOf(key) >= 0;
}
const isArray = Array.isArray;
function uniteRules(rules, key, newRule, rule) {
if (
String(rule.test) !== String(newRule.test) ||
((newRule.enforce || rule.enforce) && rule.enforce !== newRule.enforce) ||
(newRule.include && !isSameValue(rule.include, newRule.include)) ||
(newRule.exclude && !isSameValue(rule.exclude, newRule.exclude))
) {
return false;
} else if (
!rule.test &&
!rule.include &&
!rule.exclude &&
(rule.loader && rule.loader.split('?')[0]) !==
(newRule.loader && newRule.loader.split('?')[0])
) {
// Don't merge the rule if there isn't any identifying fields and the loaders don't match
return false;
} else if ((rule.include || rule.exclude) && !newRule.include && !newRule.exclude) {
// Don't merge child without include/exclude to parent that has either
return false;
}
// apply the same logic for oneOf
if (rule.oneOf && newRule.oneOf) {
rule.oneOf = unionWith(
rule.oneOf,
newRule.oneOf,
uniteRules.bind(null, {}, 'oneOf')
);
return true;
}
// newRule.loader should always override use, loaders and oneOf
if (newRule.loader) {
const optionsKey = newRule.options ? 'options' : newRule.query && 'query';
delete rule.use;
delete rule.loaders;
delete rule.oneOf;
rule.loader = newRule.loader;
if (optionsKey) {
rule[optionsKey] = newRule[optionsKey];
}
} else if (newRule.oneOf) {
delete rule.use;
delete rule.loaders;
delete rule.loader;
rule.oneOf = newRule.oneOf;
} else if (
(rule.use || rule.loaders || rule.loader) &&
(newRule.use || newRule.loaders)
) {
const expandEntry = loader => (typeof loader === 'string' ? { loader } : loader);
// this is only here to avoid breaking existing tests
const unwrapEntry = entry =>
!entry.options && !entry.query ? entry.loader : entry;
let entries;
if (rule.loader) {
const optionsKey = rule.options ? 'options' : rule.query && 'query';
entries = [{ loader: rule.loader }];
if (optionsKey) {
entries[0][optionsKey] = rule[optionsKey];
}
delete rule.loader;
if (optionsKey) {
delete rule[optionsKey];
}
} else {
entries = [].concat(rule.use || rule.loaders).map(expandEntry);
}
const newEntries = [].concat(newRule.use || newRule.loaders).map(expandEntry);
const loadersKey = rule.use || newRule.use ? 'use' : 'loaders';
const resolvedKey = `${key}.${loadersKey}`;
switch (rules[resolvedKey]) {
case 'prepend':
rule[loadersKey] = [
...differenceWith(newEntries, entries, uniteEntries),
...entries
].map(unwrapEntry);
break;
case 'replace':
rule[loadersKey] = newRule.use || newRule.loaders;
break;
default:
rule[loadersKey] = combineEntries(newEntries, entries).map(unwrapEntry);
}
}
if (newRule.include) {
rule.include = newRule.include;
}
if (newRule.exclude) {
rule.exclude = newRule.exclude;
}
return true;
}
/**
* Check equality of two values using lodash's isEqual
* Arrays need to be sorted for equality checking
* but clone them first so as not to disrupt the sort order in tests
*/
function isSameValue(a, b) {
const [propA, propB] = [a, b].map(value =>
isArray(value) ? [...value].sort() : value
);
return isEqual(propA, propB);
}
function areEqualEntries(newEntry, entry) {
const loaderNameRe = /^([^?]+)/gi;
const [loaderName] = entry.loader.match(loaderNameRe);
const [newLoaderName] = newEntry.loader.match(loaderNameRe);
return loaderName === newLoaderName;
}
function uniteEntries(newEntry, entry) {
if (areEqualEntries(newEntry, entry)) {
// Replace query values with newer ones
mergeWith(entry, newEntry);
return true;
}
return false;
}
/* Combines entries and newEntries, while respecting the order of loaders in each.
Iterates through new entries. If the new entry also exists in existing entries,
we'll put in all of the loaders from existing entries that come before it (in case
those are pre-requisites). Any remaining existing entries are added at the end.
Since webpack processes right-to-left, we're working backwards through the arrays
*/
function combineEntries(newEntries, existingEntries) {
const resultSet = [];
// We're iterating through newEntries, this keeps track of where we are in the existingEntries
let existingEntriesIteratorIndex = existingEntries.length - 1;
for (let i = newEntries.length - 1; i >= 0; i -= 1) {
const currentEntry = newEntries[i];
const indexInExistingEntries = findLastIndexUsingComparinator(
existingEntries,
currentEntry,
areEqualEntries,
existingEntriesIteratorIndex
);
const hasEquivalentEntryInExistingEntries = indexInExistingEntries !== -1;
if (hasEquivalentEntryInExistingEntries) {
// If the same entry exists in existing entries, we should add all of the entries that
// come before to maintain order
for (
let j = existingEntriesIteratorIndex;
j > indexInExistingEntries;
j -= 1
) {
const existingEntry = existingEntries[j];
// If this entry also exists in new entries, we'll add as part of iterating through
// new entries so that if there's a conflict between existing entries and new entries,
// new entries order wins
const hasMatchingEntryInNewEntries =
findLastIndexUsingComparinator(
newEntries,
existingEntry,
areEqualEntries,
i
) !== -1;
if (!hasMatchingEntryInNewEntries) {
resultSet.unshift(existingEntry);
}
existingEntriesIteratorIndex -= 1;
}
uniteEntries(currentEntry, existingEntries[existingEntriesIteratorIndex]);
// uniteEntries mutates the second parameter to be a merged version, so that's what's pushed
resultSet.unshift(existingEntries[existingEntriesIteratorIndex]);
existingEntriesIteratorIndex -= 1;
} else {
const alreadyHasMatchingEntryInResultSet =
findLastIndexUsingComparinator(
resultSet,
currentEntry,
areEqualEntries
) !== -1;
if (!alreadyHasMatchingEntryInResultSet) {
resultSet.unshift(currentEntry);
}
}
}
// Add remaining existing entries
for (
existingEntriesIteratorIndex;
existingEntriesIteratorIndex >= 0;
existingEntriesIteratorIndex -= 1
) {
const existingEntry = existingEntries[existingEntriesIteratorIndex];
const alreadyHasMatchingEntryInResultSet =
findLastIndexUsingComparinator(resultSet, existingEntry, areEqualEntries) !==
-1;
if (!alreadyHasMatchingEntryInResultSet) {
resultSet.unshift(existingEntry);
}
}
return resultSet;
}
function findLastIndexUsingComparinator(
entries,
entryToFind,
comparinator,
startingIndex
) {
startingIndex = startingIndex || entries.length - 1;
for (let i = startingIndex; i >= 0; i -= 1) {
if (areEqualEntries(entryToFind, entries[i])) {
return i;
}
}
return -1;
}
const { mergeWithCustomize } = require('webpack-merge');
module.exports.mergeSmart = mergeWithCustomize({
customizeArray: (a, b, key) => {
if (isRule(key.split('.').slice(-1)[0])) {
return unionWith(a, b, uniteRules.bind(null, {}, key));
}
return null;
},
// Added by Mix to fix a regex merge bug
customizeObject: (a, b, key) => {
if (a instanceof RegExp) {
if (b instanceof RegExp) {
return b;
}
return a;
}
if (b instanceof RegExp) {
return b;
}
return null;
}
});
+12
View File
@@ -0,0 +1,12 @@
let { mergeSmart } = require('./MergeSmart');
/** @typedef {import('webpack').Configuration} Configuration */
/**
*
* @param {Configuration} configA
* @param {Configuration} configB
*/
module.exports = (configA, configB) => {
return mergeSmart(configA, configB);
};
+195
View File
@@ -0,0 +1,195 @@
let path = require('path');
let Entry = require('./Entry');
let webpackRules = require('./webpack-rules');
let webpackPlugins = require('./webpack-plugins');
let webpackDefaultConfig = require('./webpack-default');
class WebpackConfig {
/**
* Create a new instance.
*
* @param {import("../Mix.js")} mix
*/
constructor(mix) {
this.mix = mix;
this.chunks = mix.chunks;
/** @type {ReturnType<webpackDefaultConfig>} */
this.webpackConfig = {};
}
/**
* Build the Webpack configuration object.
*/
async build() {
this.webpackConfig = webpackDefaultConfig(this.mix);
await this.buildEntry();
this.buildOutput();
this.configureHMR();
await this.buildRules();
await this.buildPlugins();
this.buildChunks();
// We'll announce that the core config object has been
// generated by Mix. At this point, any plugins may
// hook in and modify the config as necessary.
await this.mix.dispatch('configReady', this.webpackConfig);
// Rebuild the chunks as plugins may have added new ones
this.buildChunks();
// Finally, we'll make one last announcement for the user
// to hook into - using mix.override().
await this.mix.dispatch('configReadyForUser', this.webpackConfig);
// Rebuild the chunks as the user may have changed things
this.buildChunks();
return this.webpackConfig;
}
/**
* Build the entry object.
*/
async buildEntry() {
let entry = new Entry(this.mix);
if (!this.mix.bundlingJavaScript) {
entry.addDefault();
}
await this.mix.dispatch('loading-entry', entry);
this.webpackConfig.entry = entry.get();
}
/**
* Build the output object.
*/
buildOutput() {
this.webpackConfig.output = {
hashFunction: 'xxhash64',
path: path.resolve(this.mix.config.publicPath),
filename: '[name].js',
chunkFilename: pathData => {
let hasAbsolutePathChunkName =
pathData.chunk &&
pathData.chunk.name &&
pathData.chunk.name.startsWith('/');
if (
(this.mix.components.get('js') || this.mix.components.get('ts')) &&
!hasAbsolutePathChunkName
) {
let output = this.mix.components.get('ts')
? this.mix.components.get('ts').toCompile[0].output
: this.mix.components.get('js').toCompile[0].output;
return `${output.normalizedOutputPath()}/[name].js`;
}
return '[name].js';
},
publicPath: '/'
};
}
configureHMR() {
if (!this.mix.isUsing('hmr')) {
return;
}
const { https, host, port } = this.mix.config.hmrOptions;
const protocol = https ? 'https' : 'http';
const url = `${protocol}://${host}:${port}/`;
this.webpackConfig.output = {
...this.webpackConfig.output,
publicPath: url
};
this.webpackConfig.devServer = {
host,
port,
client: {
webSocketURL: {
hostname: host,
pathname: '/ws',
port
}
},
liveReload: false,
https,
devMiddleware: {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'Access-Control-Allow-Headers':
'X-Requested-With, Content-Type, Authorization'
}
},
/**
*
* @param {{app: import("express").Application}} param0
*/
onBeforeSetupMiddleware({ app }) {
app.use(function (req, _, next) {
// Something causes hot update chunks (except for the JSON payload)
// to start with a double slash
// e.g. GET http://localhost:8080//js/app.[hash].hot-update.js
// This causes loading those chunks to fail so we patch it up here
// This is super hacky and a proper solution should be found eventually
req.url = req.url.replace(/^\/\//, '/');
next();
});
},
...this.webpackConfig.devServer
};
}
/**
* Build the rules array.
*/
async buildRules() {
this.webpackConfig.module = this.webpackConfig.module || {};
this.webpackConfig.module.rules = this.webpackConfig.module.rules || [];
this.webpackConfig.module.rules.push(...webpackRules(this.mix));
await this.mix.dispatch('loading-rules', this.webpackConfig.module.rules);
}
/**
* Build the plugins array.
*/
async buildPlugins() {
this.webpackConfig.plugins = this.webpackConfig.plugins || [];
this.webpackConfig.plugins.push(...webpackPlugins(this.mix));
await this.mix.dispatch('loading-plugins', this.webpackConfig.plugins);
}
/**
* Build the resolve object.
*/
buildChunks() {
this.webpackConfig = require('./MergeWebpackConfig')(
this.webpackConfig,
this.mix.chunks.config()
);
}
}
module.exports = WebpackConfig;
View File
+71
View File
@@ -0,0 +1,71 @@
let path = require('path');
let TerserPlugin = require('terser-webpack-plugin');
/**
*
* @param {import("../Mix")} mix
* @returns {import("webpack").Configuration & {devServer?: import("webpack").WebpackOptionsNormalized["devServer"]}}
*/
module.exports = function (mix) {
// TODO: Remove in Mix 7 -- Here for backwards compat if a plugin requires this file
mix = mix || global.Mix;
return {
context: mix.paths.root(),
mode: mix.inProduction() ? 'production' : 'development',
infrastructureLogging: mix.isWatching() ? { level: 'none' } : {},
entry: {},
output: {
chunkFilename: '[name].[hash:5].js'
},
module: { rules: [] },
plugins: [],
resolve: {
extensions: ['*', '.wasm', '.mjs', '.js', '.jsx', '.json'],
roots: [path.resolve(mix.config.publicPath)]
},
stats: {
preset: 'errors-warnings',
performance: mix.inProduction()
},
performance: {
hints: false
},
optimization: mix.inProduction()
? {
providedExports: true,
sideEffects: true,
usedExports: true,
// @ts-ignore
minimizer: [new TerserPlugin(mix.config.terser)]
}
: {},
devtool: mix.config.sourcemaps,
// @ts-ignore
devServer: {
headers: {
'Access-Control-Allow-Origin': '*'
},
static: path.resolve(mix.config.publicPath),
historyApiFallback: true,
compress: true,
allowedHosts: 'all'
},
watchOptions: {
ignored: /node_modules/
}
};
};
+57
View File
@@ -0,0 +1,57 @@
let MixDefinitionsPlugin = require('../webpackPlugins/MixDefinitionsPlugin');
let BuildCallbackPlugin = require('../webpackPlugins/BuildCallbackPlugin');
let CustomTasksPlugin = require('../webpackPlugins/CustomTasksPlugin');
let ManifestPlugin = require('../webpackPlugins/ManifestPlugin');
let MockEntryPlugin = require('../webpackPlugins/MockEntryPlugin');
let BuildOutputPlugin = require('../webpackPlugins/BuildOutputPlugin');
let WebpackBar = require('webpackbar');
/**
*
* @param {import("../Mix")} mix
*/
module.exports = function (mix) {
// TODO: Remove in Mix 7 -- Here for backwards compat if a plugin requires this file
mix = mix || global.Mix;
let plugins = [];
// If the user didn't declare any JS compilation, we still need to
// use a temporary script to force a compile. This plugin will
// handle the process of deleting the compiled script.
if (!mix.bundlingJavaScript) {
plugins.push(new MockEntryPlugin(mix));
}
// Activate support for Mix_ .env definitions.
plugins.push(
new MixDefinitionsPlugin(mix.paths.root('.env'), {
NODE_ENV: mix.inProduction()
? 'production'
: process.env.NODE_ENV || 'development'
})
);
// Handle the creation of the mix-manifest.json file.
plugins.push(new ManifestPlugin(mix));
// Handle all custom, non-webpack tasks.
plugins.push(new CustomTasksPlugin(mix));
// Notify the rest of our app when Webpack has finished its build.
plugins.push(new BuildCallbackPlugin(stats => mix.dispatch('build', stats)));
// Enable custom output when the Webpack build completes.
plugins.push(
new BuildOutputPlugin({
clearConsole: mix.config.clearConsole,
showRelated: true
})
);
if (process.env.NODE_ENV !== 'test') {
plugins.push(new WebpackBar({ name: 'Mix' }));
}
return plugins;
};
+113
View File
@@ -0,0 +1,113 @@
/**
*
* @param {import("../Mix")} mix
*/
module.exports = function (mix) {
// TODO: Remove in Mix 7 -- Here for backwards compat if a plugin requires this file
mix = mix || global.Mix;
/** @type {import("webpack").RuleSetRule[]} */
let rules = [];
// Add support for loading HTML files.
rules.push({
test: /\.html$/,
resourceQuery: { not: [/\?vue/i] },
use: [{ loader: mix.resolve('html-loader') }]
});
if (mix.config.imgLoaderOptions) {
// Add support for loading images.
rules.push({
// only include svg that doesn't have font in the path or file name by using negative lookahead
test: /(\.(png|jpe?g|gif|webp|avif)$|^((?!font).)*\.svg$)/,
use: [
{
loader: mix.resolve('file-loader'),
options: {
/**
* @param {string} path
*/
name: path => {
if (!/node_modules|bower_components/.test(path)) {
return (
mix.config.fileLoaderDirs.images +
'/[name].[ext]?[hash]'
);
}
return (
mix.config.fileLoaderDirs.images +
'/vendor/' +
path
.replace(/\\/g, '/')
.replace(
/((.*(node_modules|bower_components))|images|image|img|assets)\//g,
''
) +
'?[hash]'
);
},
publicPath: mix.config.resourceRoot
}
},
{
loader: mix.resolve('img-loader'),
options: mix.config.imgLoaderOptions || {}
}
]
});
}
// Add support for loading fonts.
rules.push({
test: /(\.(woff2?|ttf|eot|otf)$|font.*\.svg$)/,
use: [
{
loader: mix.resolve('file-loader'),
options: {
/**
* @param {string} path
*/
name: path => {
if (!/node_modules|bower_components/.test(path)) {
return (
mix.config.fileLoaderDirs.fonts + '/[name].[ext]?[hash]'
);
}
return (
mix.config.fileLoaderDirs.fonts +
'/vendor/' +
path
.replace(/\\/g, '/')
.replace(
/((.*(node_modules|bower_components))|fonts|font|assets)\//g,
''
) +
'?[hash]'
);
},
publicPath: mix.config.resourceRoot
}
}
]
});
// Add support for loading cursor files.
rules.push({
test: /\.(cur|ani)$/,
use: [
{
loader: mix.resolve('file-loader'),
options: {
name: '[name].[ext]?[hash]',
publicPath: mix.config.resourceRoot
}
}
]
});
return rules;
};
+32
View File
@@ -0,0 +1,32 @@
const { Component } = require('./Component');
module.exports = class Alias extends Component {
/** @type {Record<string, string | {raw: string}>} */
aliases = {};
/**
* Add resolution aliases to webpack's config
*
* @param {Record<string, string | {raw: string}>} paths
*/
register(paths) {
this.aliases = { ...this.aliases, ...paths };
}
/**
* @param {import('webpack').Configuration} config
**/
webpackConfig(config) {
config.resolve = config.resolve || {};
config.resolve.alias = config.resolve.alias || {};
for (const [alias, path] of Object.entries(this.aliases)) {
const resolvedPath =
typeof path === 'object' ? path.raw : this.context.paths.root(path);
config.resolve.alias[alias] = resolvedPath;
}
return config;
}
};
+32
View File
@@ -0,0 +1,32 @@
const { concat } = require('lodash');
const { Component } = require('./Component');
module.exports = class Autoload extends Component {
/** @type {Record<string, string|string[]>} */
aliases = {};
/**
* Register the component.
*
* @param {Record<string, string>} libs
* @return {void}
*/
register(libs) {
Object.keys(libs).forEach(library => {
concat([], libs[library]).forEach(alias => {
this.aliases[alias] = library.includes('.')
? library.split('.')
: library;
});
});
}
/**
* webpack plugins to be appended to the master config.
*/
webpackPlugins() {
const { ProvidePlugin } = require('webpack');
return [new ProvidePlugin(this.aliases)];
}
};
@@ -0,0 +1,8 @@
const { Component } = require('./Component');
/**
* @deprecated Instead extend `Component` and set `passive` to `true`
**/
module.exports = class AutomaticComponent extends Component {
passive = true;
};
+10
View File
@@ -0,0 +1,10 @@
const { Component } = require('./Component');
module.exports = class BabelConfig extends Component {
/**
* @param {import('@babel/core').TransformOptions} config
*/
register(config) {
this.context.config.babelConfig = config;
}
};
+13
View File
@@ -0,0 +1,13 @@
const { Component } = require('./Component');
module.exports = class Before extends Component {
/**
* Register the component.
*
* @param {Function} callback
* @return {void}
*/
register(callback) {
this.context.listen('init', callback);
}
};
+75
View File
@@ -0,0 +1,75 @@
const { Component } = require('./Component');
/** @typedef {import('../../types/browsersync').Options} BrowserSyncOptions */
module.exports = class BrowserSync extends Component {
requiresReload = true;
/** @type {BrowserSyncOptions} */
userConfig = {};
/**
* Required dependencies for the component.
*/
dependencies() {
return ['browser-sync', 'browser-sync-webpack-plugin@^2.3.0'];
}
/**
* Register the component.
*
* @param {string|BrowserSyncOptions} userConfig
*/
register(userConfig) {
this.userConfig =
typeof userConfig == 'string' ? { proxy: userConfig } : userConfig;
}
/**
* webpack plugins to be appended to the master config.
*/
webpackPlugins() {
let BrowserSyncPlugin = require('browser-sync-webpack-plugin');
return [new BrowserSyncPlugin(this.config(), { reload: false })];
}
/**
* The regex used to determine where the Browsersync
* javascript snippet is injected onto each page.
*/
regex() {
return RegExp('(</body>|</pre>)(?!.*(</body>|</pre>))', 'is');
}
/**
* Build the BrowserSync configuration.
*/
config() {
let userConfig = this.userConfig;
let defaultConfig = {
host: 'localhost',
port: 3000,
proxy: 'app.test',
files: [
'app/**/*.php',
'resources/views/**/*.php',
`${this.context.config.publicPath || 'public'}/**/*.(js|css)`
],
snippetOptions: {
rule: {
match: this.regex(),
fn: function (snippet, match) {
return snippet + match;
}
}
}
};
if (userConfig && userConfig.server) {
delete defaultConfig.proxy;
}
return Object.assign(defaultConfig, userConfig);
}
};
+23
View File
@@ -0,0 +1,23 @@
const JavaScript = require('./JavaScript');
module.exports = class Coffee extends JavaScript {
/**
* Required dependencies for the component.
*/
dependencies() {
return ['coffee-loader', 'coffeescript'].concat();
}
/**
* webpack rules to be appended to the master config.
*/
webpackRules() {
return [
{
test: /\.coffee$/,
use: [{ loader: this.context.resolve('coffee-loader') }]
},
...super.webpackRules()
];
}
};
+77
View File
@@ -0,0 +1,77 @@
const File = require('../File');
const Assert = require('../Assert');
const ConcatFilesTask = require('../tasks/ConcatenateFilesTask');
const { Component } = require('./Component');
const { concat } = require('lodash');
module.exports = class Combine extends Component {
/**
* The API name for the component.
*/
name() {
return ['combine', 'scripts', 'babel', 'styles', 'minify'];
}
/**
* Register the component.
*
* @param {string|string[]} src
* @param {string} [output]
* @param {boolean} babel
*/
register(src, output = '', babel = false) {
// Do we need to perform compilation?
babel = babel || this.caller === 'babel';
const sources = concat([], src);
const hasOutputPath = output !== undefined && output !== '';
if (hasOutputPath) {
this.context.addTask(
this.createTask({
src: sources,
dst: output,
babel
})
);
return;
}
if (this.caller !== 'minify') {
throw new Error(
`An output file path is required when using mix.${this.caller}()`
);
}
// We've we're minifying an array of files then the output is the same as the input with a .min extension added
for (const source of sources) {
this.context.addTask(
this.createTask({
src: source,
dst: source.replace(/\.([a-z]{2,})$/i, '.min.$1'),
babel
})
);
}
}
/**
* @param {object} param0
* @param {string|string[]} param0.src
* @param {string} param0.dst
* @param {boolean} param0.babel
*/
createTask({ src, dst, babel }) {
const output = new File(dst);
Assert.combine(src, output);
return new ConcatFilesTask({
src,
output,
babel,
ignore: [output.relativePath()]
});
}
};
+66
View File
@@ -0,0 +1,66 @@
/**
* @abstract
* @internal (for now)
**/
class Component {
/** Whether or not to automatically register this component */
passive = false;
/** Whether or not this component requires dependency reloading */
requiresReload = false;
/**
* The name used to call this component.
*
* @deprecated
**/
caller = '';
/**
*
* @param {import("../Mix")} mix
*/
constructor(mix) {
this.context = mix;
}
/**
* Specifiy one or more dependencies that must
* be installed for this component to work
*
* @returns {import("../Dependencies").Dependency[]}
**/
dependencies() {
return [];
}
/**
* Add rules to the webpack config
*
* @returns {import('webpack').RuleSetRule[]}
**/
webpackRules() {
return [];
}
/**
* Add plugins to the webpack config
*
* @returns {import('webpack').WebpackPluginInstance[]}
**/
webpackPlugins() {
return [];
}
/**
* Update the webpack config
*
* @param {import('webpack').Configuration} config
* @returns {import('webpack').Configuration}
**/
webpackConfig(config) {
return config;
}
}
module.exports.Component = Component;
+240
View File
@@ -0,0 +1,240 @@
let mergeWebpackConfig = require('../builder/MergeWebpackConfig');
const { Component } = require('./Component');
const { concat } = require('lodash');
let components = [
'JavaScript',
'Preact',
'React',
'Coffee',
'Define',
'TypeScript',
'Less',
'Sass',
'Stylus',
'PostCss',
'CssWebpackConfig',
'BrowserSync',
'Combine',
'Copy',
'Autoload',
'Alias',
'Vue',
'React',
'Preact',
'Version',
'Extend',
'Extract',
'Notifications',
'DisableNotifications',
'PurifyCss',
'LegacyNodePolyfills',
'WebpackConfig',
'DumpWebpackConfig',
'Then',
'Override',
'SourceMaps',
'SetPublicPath',
'SetResourceRoot',
'Options',
'When',
'BabelConfig',
'Before'
];
class ComponentRegistrar {
/**
*
* @param {import('../Mix')} [mix]
*/
constructor(mix) {
this.mix = mix || global.Mix;
this.components = {};
}
/**
* Install all default components.
*/
installAll() {
components.map(name => require(`./${name}`)).forEach(c => this.install(c));
return this.components;
}
/**
* Install a component.
*
* @param {import("laravel-mix").Component} ComponentDefinition
* @param {string[]} [names]
*/
install(ComponentDefinition, names) {
/** @type {import("laravel-mix").Component} */
let component;
// If we're extending from the internal `Component` class then we provide the mix API object
if (Component.isPrototypeOf(ComponentDefinition)) {
// @ts-ignore
// This API is not finalized which is why we've restricted to to the internal component class for now
component = new ComponentDefinition(this.mix);
} else if (typeof ComponentDefinition === 'function') {
component = new ComponentDefinition();
} else {
component = ComponentDefinition;
}
this.registerComponent(component, names || this.getComponentNames(component));
this.mix.listen('internal:gather-dependencies', () => {
if (!component.activated && !component.passive) {
return;
}
if (!component.dependencies) {
return;
}
this.mix.dependencies.enqueue(
concat([], component.dependencies()),
component.requiresReload || false
);
});
this.mix.listen('init', () => {
if (!component.activated && !component.passive) {
return;
}
component.boot && component.boot();
component.babelConfig && this.applyBabelConfig(component);
this.mix.listen('loading-entry', entry => {
component.webpackEntry && component.webpackEntry(entry);
});
this.mix.listen('loading-rules', rules => {
component.webpackRules && this.applyRules(rules, component);
});
this.mix.listen('loading-plugins', plugins => {
component.webpackPlugins && this.applyPlugins(plugins, component);
});
this.mix.listen('configReady', config => {
component.webpackConfig && component.webpackConfig(config);
});
});
return this.components;
}
/**
*
* @param {*} component
* @returns {string[]}
*/
getComponentNames(component) {
if (typeof component.name === 'function') {
return concat([], component.name());
}
return [
component.constructor.name.replace(/^([A-Z])/, letter => letter.toLowerCase())
];
}
/**
* Register the component.
*
* @param {Component} component
* @param {string[]} names
*/
registerComponent(component, names) {
/**
*
* @param {string} name
*/
const register = name => {
this.components[name] = (...args) => {
this.mix.components.record(name, component);
component.caller = name;
component.register && component.register(...args);
component.activated = true;
return this.components;
};
// If we're dealing with a passive component that doesn't
// need to be explicitly triggered by the user, we'll
// call it now.
if (component.passive) {
this.components[name]();
}
// Components can optionally write to the Mix API directly.
if (component.mix) {
Object.keys(component.mix()).forEach(name => {
this.components[name] = component.mix()[name];
});
}
};
names.forEach(name => register(name));
}
/**
* Install the component's dependencies.
*
* @deprecated
* @param {Component} component
*/
installDependencies(component) {
throw new Error(
'ComponentRegistrar.installDependencies is an implementation detail and no longer used'
);
}
/**
*
* Apply the Babel configuration for the component.
*
* @param {Component} component
*/
applyBabelConfig(component) {
this.mix.config.babelConfig = mergeWebpackConfig(
this.mix.config.babelConfig,
component.babelConfig()
);
}
/**
*
* Apply the webpack rules for the component.
*
* @param {import('webpack').RuleSetRule[]} rules
* @param {Component} component
*/
applyRules(rules, component) {
const newRules = component.webpackRules() || [];
rules.push(...concat(newRules));
}
/**
*
* Apply the webpack plugins for the component.
*
* @param {import('webpack').WebpackPluginInstance[]} plugins
* @param {Component} component
*/
applyPlugins(plugins, component) {
const newPlugins = component.webpackPlugins() || [];
plugins.push(...concat(newPlugins));
}
}
module.exports = ComponentRegistrar;
+43
View File
@@ -0,0 +1,43 @@
class Components {
/**
* Create a new Components instance.
*/
constructor() {
this.components = [];
}
/**
* Record a newly registered component.
*
* @param {string} name
* @param {Component} component
*/
record(name, component) {
this.components[name] = component;
}
/**
* Retrieve a recorded component.
*
* @param {string} name
*/
get(name) {
return this.components[name];
}
/**
* Determine if the given component name has been registered.
*/
has(name) {
return this.components.hasOwnProperty(name);
}
/**
* Retrieve all components.
*/
all() {
return this.components;
}
}
module.exports = Components;
+22
View File
@@ -0,0 +1,22 @@
const File = require('../File');
const CopyFilesTask = require('../tasks/CopyFilesTask');
const { Component } = require('./Component');
module.exports = class Copy extends Component {
/**
* The API name for the component.
*/
name() {
return ['copy', 'copyDirectory'];
}
/**
* Register the component.
*
* @param {any} from
* @param {string} to
*/
register(from, to) {
this.context.addTask(new CopyFilesTask({ from, to: new File(to) }));
}
};
+299
View File
@@ -0,0 +1,299 @@
let semver = require('semver');
let { concat, mapValues } = require('lodash');
let { Component } = require('./Component');
let MiniCssExtractPlugin = require('mini-css-extract-plugin');
let PostCssPluginsFactory = require('../PostCssPluginsFactory');
class CssWebpackConfig extends Component {
passive = true;
/** @returns {import('../Dependencies').DependencyObject[]} */
dependencies() {
this.requiresReload = true;
return [
{
package: 'postcss@^8.3.1',
check: name =>
semver.satisfies(require(`${name}/package.json`).version, '^8.3.1')
}
];
}
/**
* webpack rules to be appended to the master config.
*/
webpackRules() {
return [
{
command: 'css',
type: 'css',
test: /\.p?css$/
},
{
command: 'sass',
type: 'scss',
test: /\.scss$/,
loader: {
loader: this.context.resolve('sass-loader'),
options: {
sassOptions: {
precision: 8,
outputStyle: 'expanded'
}
}
}
},
{
command: 'sass',
type: 'sass',
test: /\.sass$/,
loader: {
loader: this.context.resolve('sass-loader'),
options: {
sassOptions: {
precision: 8,
outputStyle: 'expanded',
indentedSyntax: true
}
}
}
},
{
command: 'less',
type: 'less',
test: /\.less$/,
loader: { loader: this.context.resolve('less-loader') }
},
{
command: 'stylus',
type: 'stylus',
test: /\.styl(us)?$/,
loader: { loader: this.context.resolve('stylus-loader') }
}
].map(rule => this.createRule(rule));
}
/**
* Build up the appropriate loaders for the given rule.
*
* @param {import('webpack').RuleSetRule & {test: RegExp, command: string, type: string}} rule
* @returns {import('webpack').RuleSetRule}
*/
createRule(rule) {
return {
test: rule.test,
exclude: this.excludePathsFor(rule.command),
oneOf: [
{
// Ex: foo.css?module
resourceQuery: /module/,
use: this.createLoaderList(rule, {
mode: 'local',
auto: undefined,
localIdentName:
this.context.config.cssModuleIdentifier || '[hash:base64]'
})
},
{
// Ex: foo.css
// Ex: foo.module.css
use: this.createLoaderList(rule, {
mode: 'local',
auto: true,
localIdentName:
this.context.config.cssModuleIdentifier || '[hash:base64]'
})
}
]
};
}
/**
* Build up the appropriate loaders for the given rule.
*
* @param {import('webpack').RuleSetRule & {test: RegExp, command: string, type: string}} rule
* @param {object} useCssModules
* @returns {any[]}
*/
createLoaderList(rule, useCssModules) {
return [
...CssWebpackConfig.afterLoaders({
method: 'auto',
location: 'default',
context: this.context
}),
{
loader: this.context.resolve('css-loader'),
options: {
/**
* @param {string} url
**/
url: url => {
if (url.startsWith('/')) {
return false;
}
return this.context.config.processCssUrls;
},
modules: useCssModules
}
},
{
loader: this.context.resolve('postcss-loader'),
options: {
postcssOptions: {
plugins: new PostCssPluginsFactory(this.context).load(),
hideNothingWarning: true
}
}
},
rule.loader,
...CssWebpackConfig.beforeLoaders({
context: this.context,
type: rule.type,
injectGlobalStyles: true
})
].filter(Boolean);
}
/**
* Paths to be excluded from the loader.
*
* @param {string} command
*/
excludePathsFor(command) {
let exclusions = this.context.components.get(command);
if (command === 'css' || !exclusions) {
return [];
}
return exclusions.details.map(preprocessor => preprocessor.src.path());
}
/**
* webpack plugins to be appended to the master config.
*/
webpackPlugins() {
return [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].css'
})
];
}
/**
* Gets a list of loaders to handle CSS
*
* This handles inlining or extraction of CSS based on context.
* The default is to inline styles
*
* @param {object} options
* @param {import('../Mix')} options.context The method to use when handling CSS.
* @param {"auto" | "inline" | "extract"} options.method The method to use when handling CSS.
* @param {"default" | "per-file"} options.location Where these loaders are applied. The `default` set or on a per-file basis (used by preprocessors).
*/
static afterLoaders({ context, method, location }) {
const loaders = [];
if (method === 'auto') {
// TODO: Fix
if (context.extractingStyles !== false) {
method = 'extract';
} else {
method = 'inline';
}
}
if (method === 'inline') {
if (this.wantsVueStyleLoader(context) && location === 'default') {
loaders.push({ loader: context.resolve('vue-style-loader') });
} else {
loaders.push({ loader: context.resolve('style-loader') });
}
} else if (method === 'extract') {
loaders.push({
loader: MiniCssExtractPlugin.loader,
options: {
esModule: true
}
});
} else {
throw new Error(
`Unknown css loader method '${method}'. Expected auto, inline, or extract.`
);
}
return loaders;
}
/**
* @private
* @param {import('../Mix')} context
**/
static wantsVueStyleLoader(context) {
const VueFeature = context.components.get('vue');
return VueFeature && VueFeature.options && VueFeature.options.useVueStyleLoader;
}
/**
* Gets a list of loaders to run
*
* This handles inlining or extraction of CSS based on context.
* The default is to inline styles
*
* @param {object} options
* @param {import('../Mix')} options.context
* @param {string} options.type The file type
* @param {boolean} options.injectGlobalStyles Whether or not to inject global styles
*/
static beforeLoaders({ context, type, injectGlobalStyles }) {
const loaders = [];
if (context.globalStyles && injectGlobalStyles) {
let resources =
CssWebpackConfig.normalizeGlobalStyles(context, context.globalStyles)[
type
] || [];
if (resources.length) {
loaders.push({
loader: context.resolve('sass-resources-loader'),
options: {
hoistUseStatements: true,
resources
}
});
}
}
return loaders;
}
/**
*
* @param {import('../Mix')} context
* @param {string | Record<string, string|string[]>} styles
*/
static normalizeGlobalStyles(context, styles) {
// Backwards compat with existing Vue globalStyles:
// A string only is supported for sass / scss.
if (typeof styles !== 'object') {
styles = {
sass: styles,
scss: styles
};
}
return mapValues(styles, files => {
files = concat([], files);
return files.map(file => context.paths.root(file));
});
}
}
module.exports = CssWebpackConfig;
+58
View File
@@ -0,0 +1,58 @@
const { Component } = require('./Component');
/** @typedef {undefined | null | string | number | bigint | boolean} CodeValuePrimitive */
/** @typedef {() => CodeValuePrimitive} LazyCodeValue */
/** @typedef {CodeValuePrimitive | LazyCodeValue} CodeValue */
module.exports = class Define extends Component {
/**
* @type {Record<string, CodeValue>}
* @internal
**/
definitions = {};
/**
*
* @param {Record<string, CodeValue>} definitions
*/
register(definitions) {
this.definitions = {
...this.definitions,
...definitions
};
}
/** @internal */
get defaults() {
return {
'import.meta.env.PROD': () => this.context.api.inProduction(),
'import.meta.env.DEV': () => !this.context.api.inProduction()
};
}
/**
* @param {Record<string, CodeValue>} definitions
*/
resolve(definitions) {
for (const [key, value] of Object.entries(definitions)) {
if (typeof value === 'function') {
definitions[key] = value();
}
}
return definitions;
}
webpackPlugins() {
const { DefinePlugin } = require('webpack');
return [
new DefinePlugin(
this.resolve({
...this.defaults,
...this.definitions
})
)
];
}
};
@@ -0,0 +1,22 @@
const { Component } = require('./Component');
module.exports = class DisableNotifications extends Component {
/**
* The API name for the component.
*/
name() {
return ['disableNotifications', 'disableSuccessNotifications'];
}
/**
* Register the component.
*/
register() {
const enabled = this.caller === 'disableSuccessNotifications' ? ['failure'] : [];
this.context.config.notifications = {
onSuccess: enabled.includes('success'),
onFailure: enabled.includes('failure')
};
}
};
+47
View File
@@ -0,0 +1,47 @@
const { Component } = require('./Component');
module.exports = class DumpWebpackConfig extends Component {
/**
* The optional name to be used when called by Mix.
*/
name() {
return ['dumpWebpackConfig', 'dump'];
}
/**
* Register the component.
*/
register() {
this.context.listen('configReadyForUser', config => {
this.context.logger.info(this.circularStringify(config));
});
}
/**
*
* @param {any} item
*/
circularStringify(item) {
const cache = new Set();
return JSON.stringify(
item,
(_, value) => {
if (value instanceof RegExp) {
return value.toString();
}
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) {
return undefined;
}
cache.add(value);
}
return value;
},
2
);
}
};
+123
View File
@@ -0,0 +1,123 @@
/**
* This file represents an example component interface
* for Mix. All new components can be "inserted" into
* Mix, like so:
*
* // webpack.mix.js
*
* mix.extend('foo', new Example());
*
* mix.foo();
*/
const { Component } = require('./Component');
module.exports = class Example {
/**
* The optional name to be used when called by Mix.
* Defaults to the class name, lowercased.
*
* Ex: mix.example();
*
* @return {string|string[]}
*/
name() {
// Example:
// return 'example';
// return ['example', 'alias'];
}
/**
* All dependencies that should be installed by Mix.
*
* @return {string[]}
*/
dependencies() {
// Example:
// return ['typeScript', 'ts'];
}
/**
* Register the component.
*
* When your component is called, all user parameters
* will be passed to this method.
*
* Ex: register(src, output) {}
* Ex: mix.yourPlugin('src/path', 'output/path');
*
* @param {any} ...params
* @return {void}
*
*/
register() {
// Example:
// this.config = { proxy: arg };
}
/**
* Boot the component. This method is triggered after the
* user's webpack.mix.js file has executed.
*/
boot() {
// Example:
// if (this.context.config.foo) {}
}
/**
* Append to the master Mix webpack entry object.
*
* @param {Entry} entry
* @return {void}
*/
webpackEntry(entry) {
// Example:
// entry.add('foo', 'bar');
}
/**
* Rules to be merged with the master webpack loaders.
*
* @return {any[]}
*/
webpackRules() {
// Example:
// return {
// test: /\.less$/,
// loaders: ['...']
// });
}
/**
* Plugins to be merged with the master webpack config.
*
* @return {any[]}
*/
webpackPlugins() {
// Example:
// return new webpack.ProvidePlugin(this.aliases);
}
/**
* Override the generated webpack configuration.
*
* @param {import("webpack").Configuration} webpackConfig
* @return {void}
*/
webpackConfig(webpackConfig) {
// Example:
// webpackConfig.resolve.extensions.push('.ts', '.tsx');
}
/**
* Babel config to be merged with Mix's defaults.
*
* @return {import("@babel/core").TransformOptions}
*/
babelConfig() {
// Example:
// return { presets: ['@babel/preset-react'] };
}
};
// Usage:
// mix.extend('example', new Example());
+59
View File
@@ -0,0 +1,59 @@
const { concat } = require('lodash');
const { Component } = require('./Component');
const { createFunctionalComponent } = require('./FunctionalComponent');
/**
* @typedef {import('../../types/component').ClassComponent} ClassComponent
* @typedef {import('../../types/component').ComponentInterface} ComponentInterface
* @typedef {import('../../types/component').FunctionalComponent} FunctionalComponent
* @typedef {import('../../types/component').InstallableComponent} InstallableComponent
* @typedef {import('../../types/component').Component} MixComponent
**/
module.exports = class Extend extends Component {
/**
* Register the component.
*
* @param {string | string[] | FunctionalComponent} name
* @param {Component} component
*/
register(name, component) {
if (typeof name === 'function') {
return this.context.registrar.install(component);
}
const names = concat([], name);
if (this.looksLikeSimpleCallback(component)) {
return this.context.registrar.install(
createFunctionalComponent(names, component)
);
}
return this.context.registrar.install(component, names);
}
/**
* Register the component.
*
* @param {MixComponent} component
* @returns {component is FunctionalComponent} component
*/
looksLikeSimpleCallback(component) {
if (typeof component !== 'function') {
return false;
}
if (!component.prototype) {
return true;
}
return (
typeof component.prototype.name !== 'function' &&
typeof component.prototype.register !== 'function' &&
typeof component.prototype.boot !== 'function' &&
typeof component.prototype.mix !== 'function' &&
typeof component.prototype.dependencies !== 'function'
);
}
};
+129
View File
@@ -0,0 +1,129 @@
const path = require('path');
const File = require('../File');
const { Component } = require('./Component');
/** @typedef {import('../../types/extract').Extraction} Extraction */
/** @typedef {import('../../types/extract').ExtractConfig} ExtractConfig */
/** @typedef {import('../builder/Entry')} Entry */
module.exports = class Extract extends Component {
/** @type {Entry|null} */
entry = null;
/** @type {Extraction[]} */
extractions = [];
/**
* The name of the component.
*
* mix.extract() or mix.extractVendor()
*/
name() {
return ['extract', 'extractVendors'];
}
/**
* Register the component.
*
* @param {ExtractConfig} [config]
* @param {string} [output]
*/
register(config = null, output = null) {
this.context.chunks.runtime = true;
this.extractions.push(this.normalizeExtraction(config, output));
}
/**
* Assets to append to the webpack entry.
*
* @param {Entry} entry
*/
webpackEntry(entry) {
this.entry = entry;
this.context.chunks.entry = entry;
if (!this.context.bundlingJavaScript) {
throw new Error('You must compile JS to extract vendor code');
}
this.extractions.forEach(extraction => {
const path = this.extractionPath(extraction.to);
const isDefaultVendorChunk =
extraction.to === null ||
extraction.to === undefined ||
extraction.test.source ===
'(?<!node_modules)[\\\\/]node_modules[\\\\/]()';
this.context.chunks.add(
`vendor${this.extractions.indexOf(extraction)}`,
path.replace(/\.js$/, ''),
extraction.test,
{
chunks: 'all',
enforce: true,
priority: isDefaultVendorChunk ? -10 : 0
}
);
});
}
extractionPath(outputPath) {
if (outputPath) {
return new File(outputPath).normalizedOutputPath();
}
return path.join(this.entry.base, 'vendor').replace(/\\/g, '/');
}
/**
*
* @param {ExtractConfig|null} [config]
* @param {string} [output]
* @returns {Extraction}
*/
normalizeExtraction(config = null, output = null) {
config = config || {};
if (typeof config === 'string') {
if (output !== null || !config.endsWith('.js')) {
throw new Error(
'mix.extract(string) expects a file path as its only argument'
);
}
config = { to: config };
} else if (typeof config === 'function') {
config = { test: config };
} else if (Array.isArray(config)) {
config = { test: this.buildLibraryRegex(config) };
}
return {
to: output || null,
...config,
test: config.test || this.buildLibraryRegex(config.libraries || [])
};
}
/**
*
* @param {string[]|RegExp} libraries
*/
buildLibraryRegex(libraries = []) {
let pattern = '(?<!node_modules)[\\\\/]node_modules[\\\\/]';
let extra = '';
if (Array.isArray(libraries)) {
extra = libraries.map(lib => `${lib}[\\\\/]`).join('|');
} else if (libraries instanceof RegExp) {
extra = libraries.source;
} else {
throw new Error(
`Unexpected type [${typeof libraries}] passed to mix.extract({ libraries: … }). ` +
`You may pass either an array of strings or a regular expression.`
);
}
return new RegExp(`${pattern}(${extra})`, 'i');
}
};
@@ -0,0 +1,34 @@
/** @typedef {import('../../types/component').ClassComponent} ClassComponent */
/** @typedef {import('../../types/component').FunctionalComponent} FunctionalComponent */
/**
* @param {string[]} names
* @param {FunctionalComponent} component
* @returns {ClassComponent}
*/
exports.createFunctionalComponent = function createFunctionalComponent(names, component) {
return class {
/** @type {any[]} */
args = [];
name() {
return names;
}
/**
*
* @param {...any} args
*/
register(...args) {
this.args = args;
}
/**
*
* @param {import('webpack').Configuration} config
*/
webpackConfig(config) {
component.call(this, config, ...this.args);
}
};
};
+71
View File
@@ -0,0 +1,71 @@
const glob = require('glob');
const File = require('../File');
const Assert = require('../Assert');
const { Component } = require('./Component');
module.exports = class JavaScript extends Component {
/** @type {{entry: File[], output: File}[]} */
toCompile = [];
/**
* The API name for the component.
*/
name() {
let name = this.constructor.name.toLowerCase();
return [name === 'javascript' ? 'js' : name];
}
/**
* Register the component.
*
* @param {any} entry
* @param {string} output
*/
register(entry, output) {
if (typeof entry === 'string' && entry.includes('*')) {
entry = glob.sync(entry);
}
Assert.js(entry, output);
entry = [].concat(entry).map(file => new File(file));
this.toCompile.push({ entry, output: new File(output) });
this.context.bundlingJavaScript = true;
}
/**
* Assets to append to the webpack entry.
*
* @param {import('../builder/Entry')} entry
*/
webpackEntry(entry) {
this.toCompile.forEach(js => {
entry.addFromOutput(
js.entry.map(file => file.path()),
js.output,
js.entry[0]
);
});
}
/**
* webpack rules to be appended to the master config.
*/
webpackRules() {
return [
{
test: /\.(cjs|mjs|jsx?|tsx?)$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: this.context.resolve('babel-loader'),
options: this.context.config.babel()
}
]
}
];
}
};
@@ -0,0 +1,51 @@
const { Component } = require('./Component');
module.exports = class LegacyNodePolyfills extends Component {
passive = true;
dependencies() {
return this.context.config.legacyNodePolyfills ? ['process', 'buffer'] : [];
}
webpackPlugins() {
if (!this.context.config.legacyNodePolyfills) {
return [];
}
const { ProvidePlugin } = require('webpack');
return [
new ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
process: 'process/browser.js'
})
];
}
/**
*
* @param {import('webpack').Configuration} _
* @returns {import('webpack').Configuration}
*/
webpackConfig(_) {
if (!this.context.config.legacyNodePolyfills) {
return {
resolve: {
fallback: {
Buffer: false,
process: false
}
}
};
}
return {
resolve: {
fallback: {
buffer: this.context.resolve('buffer/'),
process: this.context.resolve('process/browser.js')
}
}
};
}
};
+26
View File
@@ -0,0 +1,26 @@
const Preprocessor = require('./Preprocessor');
module.exports = class Less extends Preprocessor {
/**
* Required dependencies for the component.
*/
dependencies() {
return ['less-loader', 'less'];
}
/**
* Register the component.
*
* @param {any} src
* @param {string} output
* @param {any} pluginOptions
* @param {import('postcss').AcceptedPlugin[]} postCssPlugins
*/
register(src, output, pluginOptions = {}, postCssPlugins = []) {
return this.preprocess('less', src, output, pluginOptions, postCssPlugins);
}
chunkRegex() {
return /\.(css|less)$/;
}
};
+40
View File
@@ -0,0 +1,40 @@
const { Component } = require('./Component');
module.exports = class Notifications extends Component {
passive = true;
/**
* webpack plugins to be appended to the master config.
*/
webpackPlugins() {
if (!this.enabled) {
return [];
}
if (process.env.DISABLE_NOTIFICATIONS === '1') {
return [];
}
const WebpackNotifierPlugin = require('webpack-notifier');
return [
new WebpackNotifierPlugin({
appID: 'Laravel Mix',
title: 'Laravel Mix',
alwaysNotify: this.context.config.notifications.onSuccess,
timeout: false,
hint: process.platform === 'linux' ? 'int:transient:1' : undefined,
contentImage: this.context.paths.root(
'node_modules/laravel-mix/icons/laravel.png'
)
})
];
}
get enabled() {
const { notifications } = this.context.config;
return notifications && (notifications.onSuccess || notifications.onFailure);
}
};
+63
View File
@@ -0,0 +1,63 @@
const { Component } = require('./Component');
module.exports = class Options extends Component {
/**
* @param {Record<string, any>} options
*/
register(options) {
this.context.config.merge(options);
this.messages(options).forEach(msg => this.context.logger.message(msg));
}
/**
*
* @param {any} options
*/
messages(options) {
/** @type {import("../Log").LogMessage[]} */
const messages = [];
if ('extractVueStyles' in options) {
messages.push({
type: 'warn',
text: 'The option extractVueStyles has been moved. Please pass the extractStyles option to mix.vue() instead.'
});
}
if ('globalVueStyles' in options) {
messages.push({
type: 'warn',
text: 'The option globalVueStyles has been moved. Please pass the globalStyles option to mix.vue() instead.'
});
}
if ('extractVueStyles' in options || 'globalVueStyles' in options) {
messages.push({
type: 'info',
text: `Example:\n${this.buildVueExample(options)}`
});
}
return messages;
}
/**
*
* @param {any} options
* @returns {string}
*/
buildVueExample(options) {
const props = {};
if ('extractVueStyles' in options) {
props['extractStyles'] = options.extractVueStyles;
}
if ('globalVueStyles' in options) {
props['globalStyles'] = options.globalVueStyles;
}
return `mix.vue(${JSON.stringify(props, null, 2)}})`;
}
};
+11
View File
@@ -0,0 +1,11 @@
const { Component } = require('./Component');
module.exports = class Override extends Component {
/**
*
* @param {()=>void|Promise<void>} callback
*/
register(callback) {
this.context.listen('configReadyForUser', callback);
}
};
+67
View File
@@ -0,0 +1,67 @@
const Assert = require('../Assert');
const File = require('../File');
const Preprocessor = require('./Preprocessor');
module.exports = class PostCss extends Preprocessor {
/**
* The Mix API name for the component.
*/
name() {
return ['postCss', 'css'];
}
/**
* Register the component.
*
* @param {any} src
* @param {string} output
* @param {import('postcss').AcceptedPlugin[] | Record<string, any>} pluginOptions
* @param {import('postcss').AcceptedPlugin[]} postCssPlugins
*/
register(src, output, pluginOptions = {}, postCssPlugins = []) {
// Backwards compat with earlier versions of Mix
if (Array.isArray(pluginOptions) && postCssPlugins.length === 0) {
postCssPlugins = pluginOptions;
pluginOptions = {};
}
if (!Array.isArray(postCssPlugins)) {
postCssPlugins = [postCssPlugins];
}
Assert.preprocessor('postCss', src, output);
const srcFile = new File(src);
const outputFile = this.normalizeOutput(
new File(output),
srcFile.nameWithoutExtension() + '.css'
);
this.details.push({
type: 'postCss',
src: srcFile,
output: outputFile,
pluginOptions,
postCssPlugins
});
// Register a split chunk that takes everything generated
// by this file and puts it in a separate file
// We use a output-specific chunk name so we don't accidentally merge multiple files
this._addChunks(
`styles-${outputFile.relativePathWithoutExtension()}`,
srcFile,
outputFile
);
}
/**
* Override the generated webpack configuration.
* @param {import('webpack').Configuration} config
*/
webpackConfig(config) {
config.module.rules.find(rule => rule.test.toString() === '/\\.p?css$/').exclude =
this.details.map(postCss => postCss.src.path());
}
};
+31
View File
@@ -0,0 +1,31 @@
const { Component } = require('./Component');
module.exports = class Preact extends Component {
/**
* Required dependencies for the component.
*/
dependencies() {
return ['babel-preset-preact'];
}
register() {
if (
arguments.length === 2 &&
typeof arguments[0] === 'string' &&
typeof arguments[1] === 'string'
) {
throw new Error(
'mix.preact() is now a feature flag. Use mix.js(source, destination).preact() instead'
);
}
}
/**
* Babel config to be merged with Mix's defaults.
*/
babelConfig() {
return {
presets: ['preact']
};
}
};
+244
View File
@@ -0,0 +1,244 @@
const Assert = require('../Assert');
const path = require('path');
const File = require('../File');
const CssWebpackConfig = require('./CssWebpackConfig');
const PostCssPluginsFactory = require('../PostCssPluginsFactory');
const Entry = require('../builder/Entry');
/**
* @typedef {object} Detail
* @property {string} type
* @property {File} src
* @property {File} output
* @property {any} pluginOptions
* @property {any[]} postCssPlugins
* @property {boolean} [processUrls]
*/
const { Component } = require('./Component');
module.exports = class Preprocessor extends Component {
/** @type {Detail[]} */
details = [];
/**
* Assets to append to the webpack entry.
*
* @param {Entry} entry
*/
webpackEntry(entry) {
this.details.forEach(detail => {
entry.add(entry.keys()[0], detail.src.path());
});
}
/**
* webpack rules to be appended to the master config.
*/
webpackRules() {
return this.details.map(preprocessor => ({
test: preprocessor.src.path(),
use: this.webpackLoaders(preprocessor)
}));
}
/**
* Fetch all necessary webpack loaders.
*
* @param {Detail} preprocessor
*/
webpackLoaders(preprocessor) {
let processUrls = this.shouldProcessUrls(preprocessor);
/** @type {import('webpack').RuleSetRule[]} */
let loaders = [
...CssWebpackConfig.afterLoaders({
context: this.context,
method: 'extract',
location: 'per-file'
}),
{
loader: this.context.resolve('css-loader'),
options: {
/**
*
* @param {string} url
*/
url: url => {
if (url.startsWith('/')) {
return false;
}
return processUrls;
},
sourceMap: this.context.isUsing('sourcemaps'),
importLoaders: 1
}
},
{
loader: this.context.resolve('postcss-loader'),
options: this.postCssLoaderOptions(preprocessor)
}
];
if (preprocessor.type === 'sass' && processUrls) {
loaders.push({
loader: this.context.resolve('resolve-url-loader'),
options: {
sourceMap: true
}
});
}
if (preprocessor.type !== 'postCss') {
loaders.push({
loader: `${preprocessor.type}-loader`,
options: this.loaderOptions(preprocessor, processUrls)
});
}
loaders.push(
...CssWebpackConfig.beforeLoaders({
context: this.context,
type: preprocessor.type,
injectGlobalStyles: false
})
);
return loaders;
}
/**
* Prepare the preprocessor plugin options.
*
* @param {Detail} preprocessor
* @param {Boolean} processUrls
*/
loaderOptions(preprocessor, processUrls) {
return Object.assign(preprocessor.pluginOptions, {
sourceMap:
preprocessor.type === 'sass' && processUrls
? true
: this.context.isUsing('sourcemaps')
});
}
/**
* Generate the options object for the PostCSS Loader.
*
* @param {Detail} preprocessor
*/
postCssLoaderOptions(preprocessor) {
return {
sourceMap: this.context.isUsing('sourcemaps'),
postcssOptions: {
plugins: new PostCssPluginsFactory(this.context).load(
preprocessor.postCssPlugins
),
hideNothingWarning: true
}
};
}
/**
* Register a generic CSS preprocessor.
*
* @param {string} type
* @param {string} src
* @param {string} output
* @param {object} pluginOptions
* @param {import('postcss').AcceptedPlugin[]} postCssPlugins
*/
preprocess(type, src, output, pluginOptions = {}, postCssPlugins = []) {
Assert.preprocessor(type, src, output);
const srcFile = new File(src);
const outputFile = this.normalizeOutput(
new File(output),
srcFile.nameWithoutExtension() + '.css'
);
this.details.push({
type: this.constructor.name.toLowerCase(),
src: srcFile,
output: outputFile,
pluginOptions,
postCssPlugins
});
this._addChunks(
`styles-${outputFile.relativePathWithoutExtension()}`,
srcFile,
outputFile
);
return this;
}
/**
* Determine whether to apply url preprocessing.
*
* @param {Detail} preprocessor
* @returns {boolean}
*/
shouldProcessUrls(preprocessor) {
const processUrls =
preprocessor.pluginOptions.processUrls !== undefined
? preprocessor.pluginOptions.processUrls
: this.context.config.processCssUrls;
delete preprocessor.pluginOptions.processUrls;
return processUrls;
}
/**
* Generate a full output path, using a fallback
* file name, if a directory is provided.
*
* @param {File} output
* @param {string} fallbackName
*/
normalizeOutput(output, fallbackName) {
if (output.isDirectory()) {
output = new File(path.join(output.filePath, fallbackName));
}
return output;
}
chunkRegex() {
return /\.p?css$/;
}
/**
* Add the necessary chunks for this preprocessor
*
* This method is for internal use only for now.
*
* @internal
*
* @param {string} name
* @param {File} src
* @param {File} output
*/
_addChunks(name, src, output) {
const tests = [
// 1. Ensure the file is a CSS file
this.chunkRegex(),
// 2. Ensure that just this file is included in this chunk
// _or any dependencies_
src.path()
];
const attrs = {
chunks: 'all',
enforce: true,
type: 'css/mini-extract'
};
this.context.chunks.add(name, output.normalizedOutputPath(), tests, attrs);
}
};
+18
View File
@@ -0,0 +1,18 @@
const { Component } = require('./Component');
module.exports = class PurifyCss extends Component {
passive = true;
/**
* Required dependencies for the component.
*/
dependencies() {
if (this.context.config.purifyCss) {
throw new Error(
'PurifyCSS support is no longer available. We recommend using PurgeCss + postCss instead.'
);
}
return [];
}
};
+182
View File
@@ -0,0 +1,182 @@
const semver = require('semver');
const { Component } = require('./Component');
const File = require('../File');
module.exports = class React extends Component {
/** @type {import('laravel-mix').ReactConfig} */
options = {
extractStyles: false
};
/**
* Required dependencies for the component.
*/
dependencies() {
const dependencies = ['@babel/preset-react'];
if (this.supportsFastRefreshing()) {
return dependencies.concat([
{
package: '@pmmmwh/react-refresh-webpack-plugin@^0.5.0-rc.0',
check: name =>
semver.satisfies(
require(`${name}/package.json`).version,
'^0.5.0-rc.0'
)
},
'react-refresh'
]);
}
return dependencies;
}
/**
* Register the component.
*
* @param {import('laravel-mix').ReactConfig} options
*/
register(options = {}) {
if (
arguments.length === 2 &&
typeof arguments[0] === 'string' &&
typeof arguments[1] === 'string'
) {
throw new Error(
'mix.react() is now a feature flag. Use mix.js(source, destination).react() instead'
);
}
this.options = Object.assign(this.options, options);
this.context.extractingStyles =
this.context.extractingStyles || !!this.options.extractStyles;
}
/**
* webpack plugins to be appended to the master config.
*/
webpackPlugins() {
if (!this.supportsFastRefreshing()) {
return [];
}
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
return [new ReactRefreshPlugin({ overlay: { sockPath: 'ws' } })];
}
/**
* Babel config to be merged with Mix's defaults.
*/
babelConfig() {
const plugins = this.supportsFastRefreshing()
? [this.context.resolve('react-refresh/babel')]
: [];
return {
presets: [['@babel/preset-react', { runtime: 'automatic' }]],
plugins
};
}
/**
* Determine if the React version supports fast refreshing.
*/
supportsFastRefreshing() {
return (
this.context.isHot() && semver.satisfies(this.library().version, '>=16.9.0')
);
}
/**
* Load the currently installed React library.
*/
library() {
return require('react');
}
/**
* Update CSS chunks to extract React styles
*/
updateChunks() {
if (this.options.extractStyles === false) {
return;
}
this.context.chunks.add(
'styles-js',
this.styleChunkName(),
[/.(j|t)s$/, module => module.type === 'css/mini-extract'],
{
chunks: 'all',
enforce: true,
type: 'css/mini-extract'
}
);
this.context.chunks.add(
'styles-jsx',
this.styleChunkName(),
[/.(j|t)sx?$/, module => module.type === 'css/mini-extract'],
{
chunks: 'all',
enforce: true,
type: 'css/mini-extract'
}
);
}
/**
* Override the generated webpack configuration.
*
* @param {Object} config
*/
webpackConfig(config) {
this.updateChunks();
return config;
}
/**
* Get the name of the style chunk.
*
* @returns {string}
*/
styleChunkName() {
// If the user set extractStyles: true, we'll try
// to append the React styles to an existing CSS chunk.
if (this.options.extractStyles === true) {
let chunk = this.context.chunks.find((chunk, id) => id.startsWith('styles-'));
if (chunk) {
return chunk.name;
}
}
return this.extractFile().relativePathWithoutExtension();
}
/**
* Get a new File instance for the extracted file.
*
* @returns {File}
*/
extractFile() {
return new File(this.extractFileName());
}
/**
* Determine the extract file name.
*
* @return {string}
*/
extractFileName() {
let fileName =
typeof this.options.extractStyles === 'string'
? this.options.extractStyles
: '/css/react-styles.css';
return fileName.replace(this.context.config.publicPath, '').replace(/^\//, '');
}
};
+59
View File
@@ -0,0 +1,59 @@
const Preprocessor = require('./Preprocessor');
module.exports = class Sass extends Preprocessor {
/**
* Required dependencies for the component.
*/
dependencies() {
this.requiresReload = true;
const deps = ['sass-loader@^12.1.0', 'sass'];
if (this.context.config.processCssUrls) {
deps.push('resolve-url-loader@^5.0.0');
}
return deps;
}
/**
* Register the component.
*
* @param {any} src
* @param {string} output
* @param {Record<string, any>} pluginOptions
* @param {import('postcss').AcceptedPlugin[]} postCssPlugins
*/
register(src, output, pluginOptions = {}, postCssPlugins = []) {
return this.preprocess(
'sass',
src,
output,
this.pluginOptions(pluginOptions),
postCssPlugins
);
}
/**
* Build the plugin options for sass-loader.
*
* @param {Object} pluginOptions
* @returns {Object}
*/
pluginOptions(pluginOptions) {
return Object.assign(
{
sassOptions: {
precision: 8,
outputStyle: 'expanded'
}
},
pluginOptions,
{ sourceMap: true }
);
}
chunkRegex() {
return /\.(css|s[ac]ss)$/;
}
};
+11
View File
@@ -0,0 +1,11 @@
const path = require('path');
const { Component } = require('./Component');
module.exports = class SetPublicPath extends Component {
/**
* @param {string} defaultPath
*/
register(defaultPath) {
this.context.config.publicPath = path.normalize(defaultPath.replace(/\/$/, ''));
}
};
+10
View File
@@ -0,0 +1,10 @@
const { Component } = require('./Component');
module.exports = class SetResourceRoot extends Component {
/**
* @param {string} path
*/
register(path) {
this.context.config.resourceRoot = path;
}
};
+18
View File
@@ -0,0 +1,18 @@
const { Component } = require('./Component');
module.exports = class SourceMaps extends Component {
register(
generateForProduction = true,
devType = 'eval-source-map',
productionType = 'source-map'
) {
/** @type {string|false} */
let type = devType;
if (this.context.api.inProduction()) {
type = generateForProduction ? productionType : false;
}
this.context.config.sourcemaps = type;
}
};
+28
View File
@@ -0,0 +1,28 @@
let Preprocessor = require('./Preprocessor');
class Stylus extends Preprocessor {
/**
* Required dependencies for the component.
*/
dependencies() {
return ['stylus', 'stylus-loader'];
}
/**
* Register the component.
*
* @param {any} src
* @param {string} output
* @param {Record<string, any>} pluginOptions
* @param {import('postcss').AcceptedPlugin[]} postCssPlugins
*/
register(src, output, pluginOptions = {}, postCssPlugins = []) {
return this.preprocess('stylus', src, output, pluginOptions, postCssPlugins);
}
chunkRegex() {
return /\.(css|styl(us)?)$/;
}
}
module.exports = Stylus;
+17
View File
@@ -0,0 +1,17 @@
const { Component } = require('./Component');
module.exports = class Then extends Component {
/**
* The API name for the component.
*/
name() {
return ['then', 'after'];
}
/**
* @param {() => void | Promise<void>} callback
*/
register(callback) {
this.context.listen('build', callback);
}
};
+67
View File
@@ -0,0 +1,67 @@
const JavaScript = require('./JavaScript');
class TypeScript extends JavaScript {
/** @type {Record<string, any>} */
options = {};
/**
* The API name for the component.
*/
name() {
return ['typeScript', 'ts'];
}
/**
* Register the component.
*
* @param {any} entry
* @param {string} output
* @param {Record<string, any>} options
*/
register(entry, output, options = {}) {
super.register(entry, output);
this.options = options;
}
/**
* Required dependencies for the component.
*/
dependencies() {
return ['ts-loader', 'typescript'].concat();
}
/**
* webpack rules to be appended to the master config.
*/
webpackRules() {
return [
...super.webpackRules(),
{
test: /\.tsx?$/,
loader: this.context.resolve('ts-loader'),
exclude: /node_modules/,
options: Object.assign(
{},
// TODO: Maybe move to Vue plugin?
{ appendTsSuffixTo: [/\.vue$/] },
this.options
)
}
];
}
/**
* Override the generated webpack configuration.
*
* @param {import('webpack').Configuration} config
*/
webpackConfig(config) {
config.resolve = config.resolve || {};
config.resolve.extensions = config.resolve.extensions || [];
config.resolve.extensions.push('.ts', '.tsx');
return config;
}
}
module.exports = TypeScript;
+14
View File
@@ -0,0 +1,14 @@
const { concat } = require('lodash');
const VersionFilesTask = require('../tasks/VersionFilesTask');
const { Component } = require('./Component');
module.exports = class Version extends Component {
/**
* Register the component.
*
* @param {string|string[]} paths
*/
register(paths = []) {
this.context.addTask(new VersionFilesTask({ files: concat([], paths) }));
}
};
+216
View File
@@ -0,0 +1,216 @@
const { Component } = require('./Component');
const File = require('../File');
const VueVersion = require('../VueVersion');
const AppendVueStylesPlugin = require('../webpackPlugins/Css/AppendVueStylesPlugin');
module.exports = class Vue extends Component {
version = 2;
/**
* @type {import('laravel-mix').VueConfig} options
*/
options = {
version: undefined,
runtimeOnly: false,
options: null,
globalStyles: null,
extractStyles: false,
useVueStyleLoader: false
};
/**
* Register the component.
*
* @param {import('laravel-mix').VueConfig} options
*/
register(options = {}) {
if (
arguments.length === 2 &&
typeof arguments[0] === 'string' &&
typeof arguments[1] === 'string'
) {
throw new Error(
'mix.vue() is a feature flag. Use mix.js(source, destination).vue() instead'
);
}
Object.assign(this.options, options);
this.version = new VueVersion(this.context).detect(this.options.version);
if (this.options.globalStyles !== undefined) {
this.context.globalStyles = this.options.globalStyles;
}
if (this.options.extractStyles !== undefined) {
this.context.extractingStyles =
this.context.extractingStyles || !!this.options.extractStyles;
}
this.addDefines();
this.context.api.alias({
vue$: {
raw: this.aliasPath()
}
});
}
/**
* Required dependencies for the component.
*/
dependencies() {
this.requiresReload = true;
let dependencies = [
this.version === 2 ? 'vue-template-compiler' : '@vue/compiler-sfc',
this.version === 2 ? 'vue-loader@^15.9.7' : 'vue-loader@^16.2.0'
];
if (this.options.extractStyles && this.options.globalStyles) {
dependencies.push('sass-resources-loader');
}
return dependencies;
}
/**
* Override the generated webpack configuration.
*
* @param {import('webpack').Configuration} config
*/
webpackConfig(config) {
config.module = config.module || {};
config.module.rules = config.module.rules || [];
config.resolve = config.resolve || {};
config.resolve.extensions = config.resolve.extensions || [];
// push -> unshift to combat vue loader webpack 5 bug
config.module.rules.unshift({
test: /\.vue$/,
use: [
{
loader: this.context.resolve('vue-loader'),
options: this.options.options || this.context.config.vue || {}
}
]
});
// Alias Vue to its ESM build if the user has not already given an alias
config.resolve.extensions.push('.vue');
this.updateChunks();
return config;
}
aliasPath() {
if (this.version === 2) {
return this.options.runtimeOnly
? 'vue/dist/vue.runtime.esm.js'
: 'vue/dist/vue.esm.js';
}
return this.options.runtimeOnly
? 'vue/dist/vue.runtime.esm-bundler.js'
: 'vue/dist/vue.esm-bundler.js';
}
/**
* webpack plugins to be appended to the master config.
*/
webpackPlugins() {
let { VueLoaderPlugin } = require(this.context.resolve('vue-loader'));
return [new VueLoaderPlugin(), new AppendVueStylesPlugin()];
}
/**
* Update CSS chunks to extract vue styles
*/
updateChunks() {
if (this.options.extractStyles === false) {
return;
}
this.context.chunks.add(
'styles-vue',
this.styleChunkName(),
[/.vue$/, module => module.type === 'css/mini-extract'],
{
chunks: 'all',
enforce: true,
type: 'css/mini-extract'
}
);
this.context.chunks.add(
'styles-jsx',
this.styleChunkName(),
[/.jsx$/, module => module.type === 'css/mini-extract'],
{
chunks: 'all',
enforce: true,
type: 'css/mini-extract'
}
);
}
/**
* Get the name of the style chunk.
*
* @returns {string}
*/
styleChunkName() {
// If the user set extractStyles: true, we'll try
// to append the Vue styles to an existing CSS chunk.
if (this.options.extractStyles === true) {
let chunk = this.context.chunks.find((chunk, id) => id.startsWith('styles-'));
if (chunk) {
return chunk.name;
}
}
return this.extractFile().relativePathWithoutExtension();
}
/**
* Get a new File instance for the extracted file.
*
* @returns {File}
*/
extractFile() {
return new File(this.extractFileName());
}
/**
* Determine the extract file name.
*
* @return {string}
*/
extractFileName() {
let fileName =
typeof this.options.extractStyles === 'string'
? this.options.extractStyles
: '/css/vue-styles.css';
return fileName.replace(this.context.config.publicPath, '').replace(/^\//, '');
}
/**
* Determine the extract file name.
*
* @internal
*/
addDefines() {
if (this.version === 2) {
return;
}
this.context.api.define({
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false'
});
}
};
+22
View File
@@ -0,0 +1,22 @@
const { Component } = require('./Component');
/** @typedef {import('webpack').Configuration} Configuration */
/** @typedef {import('webpack')} webpack */
module.exports = class WebpackConfig extends Component {
/**
*
* @param {((webpack: webpack, config: Configuration) => Configuration) | Configuration} config
*/
register(config) {
const merge = require('../builder/MergeWebpackConfig');
const webpack = require('webpack');
this.context.api.override(webpackConfig => {
config =
typeof config === 'function' ? config(webpack, webpackConfig) : config;
Object.assign(webpackConfig, merge(webpackConfig, config));
});
}
};
+14
View File
@@ -0,0 +1,14 @@
const { Component } = require('./Component');
module.exports = class When extends Component {
/**
*
* @param {boolean} condition
* @param {(api: import("laravel-mix").Api) => void} callback
*/
register(condition, callback) {
if (condition) {
callback(this.context.api);
}
}
};
+248
View File
@@ -0,0 +1,248 @@
// @ts-check
const yargs = require('yargs/yargs');
/** @typedef {import("@babel/core").TransformOptions} BabelConfig */
/**
*
* @param {import("./Mix")} mix
*/
module.exports = function (mix) {
// TODO: Remove in Mix 7 -- Here for backwards compat if a plugin requires this file
mix = mix || global.Mix;
const argv = yargs(process.argv.slice(2))
.options({
https: { type: 'boolean', default: false },
hmrPort: { type: 'string', default: '8080' },
p: { type: 'boolean', default: false },
hot: { type: 'boolean', default: false }
})
.parseSync();
return {
/**
* Determine if webpack should be triggered in a production environment.
*
* @type {Boolean}
*/
production: process.env.NODE_ENV === 'production' || argv.p,
/**
* Determine if we should enable hot reloading.
*
* @type {Boolean}
*/
hmr: argv.hot,
/**
* Hostname and port used for the hot reload module
*/
hmrOptions: {
https: argv.https,
host: 'localhost',
port: argv.hmrPort
},
/**
* PostCSS plugins to be applied to compiled CSS.
*
* See: https://github.com/postcss/postcss/blob/master/docs/plugins.md
*
* @type {import('postcss').AcceptedPlugin[]}
*/
postCss: [],
/**
* Determine if we should enable autoprefixer by default.
* May be set to false to disable it.
*
* @type {false|import('autoprefixer').Options}
*/
autoprefixer: {},
/**
* The public path for the build.
*
* @type {String}
*/
publicPath: '',
/**
* The path for the runtime chunk (`manifest.js`).
*
* Defaults to being placed next to compiled JS files.
*
* @type {String|null}
*/
runtimeChunkPath: null,
/**
* Determine if error notifications should be displayed for each build.
*
* @type {false | {onSuccess: boolean, onFailure: boolean}}
*/
notifications: {
onSuccess: true,
onFailure: true
},
/**
* Determine if sourcemaps should be created for the build.
*
* @type {false | string}
*/
sourcemaps: false,
/**
* The resource root for the build.
*
* @type {String}
*/
resourceRoot: '/',
/**
* Image Loader defaults.
* See: https://github.com/thetalecrafter/img-loader#options
*
* @type {Boolean|Object}
*/
imgLoaderOptions: {
enabled: true,
gifsicle: {},
mozjpeg: {},
optipng: {},
svgo: {}
},
/**
* File Loader directory defaults.
*/
fileLoaderDirs: {
images: 'images',
fonts: 'fonts'
},
/**
* The Babel configuration to use when compiling
*/
babel: function () {
const BabelConfig = require('./BabelConfig');
return new BabelConfig(mix).generate();
},
/**
* Determine if CSS relative url()s should be resolved by webpack.
* Disabling this can improve performance greatly.
*
* @type {Boolean}
*/
processCssUrls: true,
/**
* Terser-specific settings for Webpack.
*
* See: https://github.com/webpack-contrib/terser-webpack-plugin#options
*
* @type {import('../types/terser').TerserPluginOptions}
*/
terser: {
parallel: true,
terserOptions: {
compress: true,
output: {
comments: false
}
}
},
/**
* cssnano-specific settings for Webpack.
* Disabled if set to false.
*
* See: https://cssnano.co/docs/optimisations
*
* @type {false|Object}
*/
cssNano: {},
/**
* CleanCss-specific settings for Webpack.
*
* See: https://github.com/clean-css/clean-css#constructor-options
*
* @type {Object}
*/
cleanCss: {},
/**
* Custom Webpack-specific configuration to merge/override Mix's.
*
* @type {Object}
*/
webpackConfig: {},
/**
* Custom Babel configuration to be merged with Mix's defaults.
*
* @type {BabelConfig}
*/
babelConfig: {},
/**
* 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,
/**
* Enable legacy node -> browser polyfills for things like `process` and `Buffer`.
*
* @type {Boolean}
*/
legacyNodePolyfills: true,
/**
* Options to pass to vue-loader
*
* @deprecated Use `.vue({options: {…}})` instead
*
* @type {any}
*/
vue: {},
/**
* The name / path to the mix manifest.
* The path is relative to the public path.
*
* Set to `false` to disable manifest generation.
*
* @type {string | false}
*/
manifest: `mix-manifest.json`,
/**
* Sets the css module identifier pattern
*
* For more information see https://github.com/webpack-contrib/css-loader/tree/v5.2.7#localidentname
*/
cssModuleIdentifier: '[hash:base64]',
/**
* Merge the given options with the current defaults.
*
* @param {Record<string, any>} options
*/
merge(options) {
Object.keys(options).forEach(key => {
this[key] = options[key];
});
}
};
};
+74
View File
@@ -0,0 +1,74 @@
/**
* Generic tap function.
* @deprecated
**/
Object.defineProperty(global, 'tap', {
/**
* @template T
* @param {T} val
* @param {(val: T) => void} callback
* @returns {T}
*/
value(val, callback) {
callback(val);
return val;
}
});
/**
* Add tap to arrays.
* @deprecated
**/
if (!Array.prototype.hasOwnProperty('tap')) {
Object.defineProperty(Array.prototype, 'tap', {
/**
* @param {(arr: self) => void} callback
* @returns {self}
*/
value(callback) {
if (this.length) {
callback(this);
}
return this;
}
});
}
/**
* Add wrap to arrays.
* @deprecated
**/
if (!Array.hasOwnProperty('wrap')) {
Object.defineProperty(Array, 'wrap', {
/**
* @template T
* @param {T|T[]} value
* @returns {T[]}
*/
value(value) {
if (Array.isArray(value)) {
return value;
}
return [value];
}
});
}
/**
* Flatten an array.
* @deprecated
**/
Object.defineProperty(global, 'flatten', {
/**
* @template T
* @param {T[]} arr
* @returns {T[]}
*/
value(arr) {
// @ts-ignore
return [].concat.apply([], Object.values(arr));
}
});
+20
View File
@@ -0,0 +1,20 @@
const Mix = require('./Mix');
require('./helpers');
/*
|--------------------------------------------------------------------------
| Welcome to Laravel Mix!
|--------------------------------------------------------------------------
|
| Laravel Mix provides a clean, fluent API for defining basic webpack
| build steps for your Laravel application. Mix supports a variety
| of common CSS and JavaScript pre-processors out of the box.
|
*/
let mix = Mix.primary;
mix.boot();
module.exports = mix.api;
+40
View File
@@ -0,0 +1,40 @@
let Task = require('./Task');
let FileCollection = require('../FileCollection');
const { FileGlob } = require('./FileGlob');
const File = require('../File');
/**
* @extends {Task<{ src: string|string[], output: File, babel: boolean, ignore?: string[] }>}
*/
class ConcatenateFilesTask extends Task {
/**
* Run the task.
*/
run() {
return this.merge();
}
/**
* Merge the files into one.
*/
async merge() {
const files = await FileGlob.expand(this.data.src, {
ignore: this.data.ignore || []
});
this.files = new FileCollection(files);
return this.files
.merge(this.data.output, this.data.babel)
.then(this.assets.push.bind(this.assets));
}
/**
* Handle when a relevant source file is changed.
*/
onChange() {
return this.merge();
}
}
module.exports = ConcatenateFilesTask;
+46
View File
@@ -0,0 +1,46 @@
let Task = require('./Task');
let FileCollection = require('../FileCollection');
let Log = require('../Log');
const path = require('path');
const File = require('../File');
/**
* @extends {Task<{ from: string|string[], to: File }>}
*/
class CopyFilesTask extends Task {
/**
* Run the task.
*/
run() {
let copy = this.data;
this.files = new FileCollection(copy.from);
this.files.copyTo(copy.to);
this.assets = this.files.assets;
}
/**
* Handle when a relevant source file is changed.
*
* @param {string} updatedFile
*/
onChange(updatedFile) {
let destination = this.data.to;
// If we're copying a src directory recursively, we have to calculate
// the correct destination path, based on the src directory tree.
if (!Array.isArray(this.data.from) && new File(this.data.from).isDirectory()) {
destination = destination.append(
path.normalize(updatedFile).replace(path.normalize(this.data.from), '')
);
}
Log.feedback(`Copying ${updatedFile} to ${destination.path()}`);
this.files.copyTo(destination, new File(updatedFile));
}
}
module.exports = CopyFilesTask;
+56
View File
@@ -0,0 +1,56 @@
let path = require('path');
let glob = require('glob');
let File = require('../File');
let { promisify } = require('util');
let { concat } = require('lodash');
let globAsync = promisify(glob);
/** @internal */
module.exports.FileGlob = class FileGlob {
/**
* Find all relevant files matching the given source path.
*
* @param {string|string[]} src
* @param {{ ignore?: string[] }} options
* @returns {Promise<string[]>}
*/
static async expand(src, { ignore = [] } = {}) {
const paths = concat([], src);
const results = await Promise.all(
paths.map(async srcPath => {
const result = await this.find(srcPath);
if (!result.isDir && result.matches.length === 0) {
return [srcPath];
}
return result.matches;
})
);
const filepaths = results.flatMap(files => files);
return filepaths.filter(filepath => {
return !ignore.includes(filepath);
});
}
/**
*
* @internal
* @param {string} src
* @returns {Promise<{isDir: boolean, matches: string[]}>}
*/
static async find(src) {
const isDir = File.find(src).isDirectory();
const pattern = isDir ? path.join(src, '**/*') : src;
const matches = await globAsync(pattern, { nodir: true });
return {
isDir,
matches
};
}
};
+70
View File
@@ -0,0 +1,70 @@
let chokidar = require('chokidar');
let File = require('../File');
let FileCollection = require('../FileCollection');
let NOOP = () => {};
/**
* @template {object} TData
*/
class Task {
/**
* Create a new task instance.
*
* @param {TData} data
*/
constructor(data) {
/** @type {TData} */
this.data = data;
/** @type {File[]} */
this.assets = [];
/** @type {FileCollection} */
this.files = new FileCollection();
this.isBeingWatched = false;
}
/**
* Watch all relevant files for changes.
*
* @param {boolean} usePolling
*/
watch(usePolling = false) {
if (this.isBeingWatched) return;
let files = this.files.get();
let watcher = chokidar
.watch(files, { usePolling, persistent: true })
.on('change', this.onChange.bind(this));
// Workaround for issue with atomic writes.
// See https://github.com/paulmillr/chokidar/issues/591
if (!usePolling) {
watcher.on('raw', event => {
if (event === 'rename') {
watcher.unwatch(files);
watcher.add(files);
}
});
}
this.isBeingWatched = true;
}
/**
*/
run() {
throw new Error('Task.run is an abstract method. Please override it.');
}
/**
*
* @param {string} filepath
*/
onChange(filepath) {
throw new Error('Task.onChange is an abstract method. Please override it.');
}
}
module.exports = Task;
+48
View File
@@ -0,0 +1,48 @@
let Task = require('./Task');
let File = require('../File');
let FileCollection = require('../FileCollection');
const { FileGlob } = require('./FileGlob');
/**
* @extends {Task<{ files: string[] }>}
*/
class VersionFilesTask extends Task {
/**
* Run the task.
*/
async run() {
const fileGroups = await Promise.all(
this.data.files.map(async filepath => {
const relativePath = new File(filepath).forceFromPublic().relativePath();
return FileGlob.expand(relativePath);
})
);
const files = fileGroups.flat();
this.files = new FileCollection(files);
this.assets = files.map(filepath => {
const file = new File(filepath);
this.mix.manifest.hash(file.pathFromPublic());
return file;
});
}
/**
* Handle when a relevant source file is changed.
*
* @param {string} updatedFile
*/
onChange(updatedFile) {
this.mix.manifest.hash(new File(updatedFile).pathFromPublic()).refresh();
}
get mix() {
return global.Mix;
}
}
module.exports = VersionFilesTask;
@@ -0,0 +1,23 @@
class BuildCallbackPlugin {
/**
* Create a new plugin instance.
*
* @param {(stats: import("webpack").Stats) => any|Promise<any>} callback
*/
constructor(callback) {
this.callback = callback;
}
/**
* Apply the plugin.
*
* @param {import("webpack").Compiler} compiler
*/
apply(compiler) {
compiler.hooks.done.tapPromise('BuildCallbackPlugin', async stats => {
return await this.callback(stats);
});
}
}
module.exports = BuildCallbackPlugin;
@@ -0,0 +1,236 @@
const _ = require('lodash');
const chalk = require('chalk');
const Table = require('cli-table3');
const readline = require('readline');
const stripAnsi = require('strip-ansi');
const { formatSize } = require('webpack/lib/SizeFormatHelpers');
const { version } = require('../../package.json');
/**
* @typedef {object} BuildOutputOptions
* @property {boolean} clearConsole
* @property {boolean} showRelated
**/
/**
* @typedef {object} StatsAsset
* @property {string} name
* @property {number} size
* @property {StatsAsset[]|{}} related
**/
/**
* @typedef {object} StatsData
* @property {StatsAsset[]} assets
**/
class BuildOutputPlugin {
/**
*
* @param {BuildOutputOptions} options
*/
constructor(options) {
this.options = options;
this.patched = false;
}
/**
* Apply the plugin.
*
* @param {import("webpack").Compiler} compiler
*/
apply(compiler) {
// TODO: Refactor setup to allow removing this check
if (process.env.NODE_ENV === 'test') {
return;
}
compiler.hooks.done.tap('BuildOutputPlugin', stats => {
if (stats.hasErrors()) {
return false;
}
if (this.options.clearConsole) {
this.clearConsole();
}
let data = stats.toJson({
assets: true,
builtAt: true,
hash: true,
performance: true,
relatedAssets: this.options.showRelated
});
this.heading(`Laravel Mix v${version}`);
console.log(chalk.green.bold(`✔ Compiled Successfully in ${data.time}ms`));
if (data.assets.length) {
console.log(this.statsTable(data));
}
});
}
/**
* Print a block section heading.
*
* @param {string} text
*/
heading(text) {
console.log();
console.log(chalk.bgBlue.white.bold(this.section(text)));
console.log();
}
/**
* Create a block section.
*
* @param {string} text
*/
section(text) {
const padLength = 3;
const padding = ' '.repeat(padLength);
text = `${padding}${text}${padding}`;
const line = ' '.repeat(text.length);
return `${line}\n${text}\n${line}`;
}
/**
* Generate the stats table.
*
* @param {StatsData} data
* @returns {string}
*/
statsTable(data) {
const assets = this.sortAssets(data);
const table = new Table({
head: [chalk.bold('File'), chalk.bold('Size')],
colWidths: [35],
colAligns: ['right'],
style: {
head: [],
compact: true
}
});
for (const asset of assets) {
table.push([chalk.green(asset.name), formatSize(asset.size)]);
}
this.extendTableWidth(table);
this.monkeyPatchTruncate();
return table.toString();
}
/**
*
* @param {StatsData} data
*/
sortAssets(data) {
let assets = data.assets;
assets = _.flatMap(assets, asset => [
asset,
...(Array.isArray(asset.related) ? asset.related : [])
]);
assets = _.orderBy(assets, ['name', 'size'], ['asc', 'asc']);
return assets;
}
/**
* Clear the entire screen.
*/
clearConsole() {
const blank = '\n'.repeat(process.stdout.rows);
console.log(blank);
readline.cursorTo(process.stdout, 0, 0);
readline.clearScreenDown(process.stdout);
}
/**
* Extend the width of the table
*
* Currently only increases the file column size
*
* @param {import("cli-table3").Table} table
* @param {number|null} targetWidth
* @param {number} maxWidth
*/
extendTableWidth(table, targetWidth = null, maxWidth = Infinity) {
targetWidth = targetWidth === null ? process.stdout.columns : targetWidth;
if (!targetWidth) {
return;
}
const tableWidth = this.calculateTableWidth(table);
const fileColIncrease = Math.min(targetWidth - tableWidth, maxWidth - tableWidth);
if (fileColIncrease <= 0) {
return;
}
// @ts-ignore
table.options.colWidths[0] += fileColIncrease;
}
// Yeah, I know.
monkeyPatchTruncate() {
if (this.patched) {
return;
}
this.patched = true;
// @ts-ignore
const utils = require('cli-table3/src/utils');
const oldTruncate = utils.truncate;
// cli-table3 can only do truncation at the end
// We want the asset name to be truncated at the beginning if it's too long
// FIXME: We really should set truncation location via a paramter or something
// (or PR support for alignment-based truncation)
/**
*
* @param {string} str
* @param {number} desiredLength
* @param {string} truncateChar
*/
utils.truncate = (str, desiredLength, truncateChar) => {
if (stripAnsi(str).length > desiredLength) {
str = `${str.substr(-desiredLength + 2)}`;
}
return oldTruncate(str, desiredLength, truncateChar);
};
}
/**
* Calculate the width of the CLI Table
*
* `table.width` does not report the correct width
* because it includes ANSI control characters
*
* @internal
* @param {import("cli-table3").Table} table
*/
calculateTableWidth(table) {
const firstRow = table.toString().split('\n')[0];
return stripAnsi(firstRow).length;
}
}
module.exports = BuildOutputPlugin;
@@ -0,0 +1,67 @@
// @ts-check
/**
* This plugin ensures that vue styles are always appended to the end of CSS files
*/
class AppendVueStylesPlugin {
/** @param {import("webpack").Compiler} compiler */
apply(compiler) {
const name = 'AppendVueStylesPlugin';
compiler.hooks.compilation.tap(name, compilation => {
compilation.hooks.optimizeChunks.tap(name, chunks => {
this.reorderModules(compilation.chunkGraph, chunks);
});
});
}
/**
*
* @param {import("webpack").ChunkGraph} graph
* @param {Iterable<import("webpack").Chunk>} chunks
*/
reorderModules(graph, chunks) {
const queue = this.collectCssChunks(graph, chunks);
// Find the last module in the bundle
let largestIndex = 0;
for (const { module, group } of queue) {
largestIndex = Math.max(largestIndex, group.getModulePostOrderIndex(module));
}
// Push all vue assets after it in their original order
for (const { module, group } of queue) {
if (module.identifier().includes('?vue')) {
group.setModulePostOrderIndex(
module,
largestIndex + group.getModulePostOrderIndex(module)
);
}
}
}
/**
* @param {import("webpack").ChunkGraph} graph
* @param {Iterable<import("webpack").Chunk>} chunks
*/
collectCssChunks(graph, chunks) {
const queue = [];
for (const chunk of chunks) {
for (const module of graph.getChunkModulesIterable(chunk)) {
if (module.type !== 'css/mini-extract') {
continue;
}
for (const group of chunk.groupsIterable) {
queue.push({ module, chunk, group });
}
}
}
return queue;
}
}
module.exports = AppendVueStylesPlugin;
@@ -0,0 +1,123 @@
let Log = require('../Log');
let collect = require('collect.js');
class CustomTasksPlugin {
/**
*
* @param {import('../Mix')} mix
*/
constructor(mix) {
this.mix = mix || global.Mix;
}
/**
* Apply the plugin.
*
* @param {import("webpack").Compiler} compiler
*/
apply(compiler) {
compiler.hooks.done.tapPromise(this.constructor.name, async stats => {
await this.runTasks(stats);
if (this.mix.components.get('version') && !this.mix.isUsing('hmr')) {
this.applyVersioning();
}
if (this.mix.inProduction()) {
await this.minifyAssets();
}
if (this.mix.isWatching()) {
this.mix.tasks.forEach(task => task.watch(this.mix.isPolling()));
}
this.mix.manifest.refresh();
});
}
/**
* Execute the task.
*
* @param {import("../tasks/Task")} task
* @param {import("webpack").Stats} stats
*/
async runTask(task, stats) {
await Promise.resolve(task.run());
await Promise.allSettled(task.assets.map(asset => this.addAsset(asset, stats)));
}
/**
* Add asset to the webpack stats.
*
* @param {import("../File")} asset
* @param {import("webpack").Stats} stats
*/
async addAsset(asset, stats) {
// Skip adding directories to the manifest
// TODO: We should probably add the directory but skip hashing
if (asset.isDirectory()) {
return;
}
const path = asset.pathFromPublic();
// Add the asset to the manifest
this.mix.manifest.add(path);
// Update the Webpack assets list for better terminal output.
stats.compilation.assets[path] = {
size: () => asset.size(),
emitted: true
};
}
/**
* Execute potentially asynchronous tasks sequentially.
*
* @param stats
* @param index
*/
runTasks(stats, index = 0) {
if (index === this.mix.tasks.length) return Promise.resolve();
const task = this.mix.tasks[index];
return this.runTask(task, stats).then(() => this.runTasks(stats, index + 1));
}
/**
* Minify the given asset file.
*/
async minifyAssets() {
const assets = collect(this.mix.tasks)
.where('constructor.name', '!==', 'VersionFilesTask')
.flatMap(({ assets }) => assets);
const tasks = assets.map(async asset => {
try {
await asset.minify();
} catch (e) {
Log.error(
`Whoops! We had trouble minifying "${asset.relativePath()}". ` +
`Perhaps you need to use mix.babel() instead?`
);
throw e;
}
});
await Promise.allSettled(tasks);
}
/**
* Version all files that are present in the manifest.
*/
applyVersioning() {
collect(this.mix.manifest.get()).each((value, key) =>
this.mix.manifest.hash(key)
);
}
}
module.exports = CustomTasksPlugin;
+28
View File
@@ -0,0 +1,28 @@
class ManifestPlugin {
/**
*
* @param {import("../Mix")} mix
*/
constructor(mix) {
// TODO: Simplify in Mix 7 -- Here for backwards compat if a plugin creates this class directly
this.mix = mix || global.Mix;
}
/**
* Apply the plugin.
*
* @param {import("webpack").Compiler} compiler
*/
apply(compiler) {
compiler.hooks.emit.tapAsync('ManifestPlugin', (curCompiler, callback) => {
let stats = curCompiler.getStats().toJson();
// Handle the creation of the mix-manifest.json file.
this.mix.manifest.transform(stats).refresh();
callback();
});
}
}
module.exports = ManifestPlugin;
@@ -0,0 +1,84 @@
let dotenv = require('dotenv');
let expand = require('dotenv-expand');
/** @internal */
class MixDefinitionsPlugin {
/**
*
* @param {string} envPath
* @param {Record<string, string>} [additionalEnv]
*/
constructor(envPath, additionalEnv = {}) {
this.envPath = envPath;
this.additionalEnv = additionalEnv;
}
/**
*
* @param {import('webpack').Compiler} compiler
*/
apply(compiler) {
this.plugin.apply(compiler);
}
/**
* Build all MIX_ definitions for Webpack's DefinePlugin.
*/
get env() {
// Load .env, if it exists, into process.env
expand(dotenv.config({ path: this.envPath }));
// Take everything from process.env that beings with MIX_
const regex = /^MIX_/i;
const existing = Object.fromEntries(
Object.entries(process.env).filter(([key]) => regex.test(key))
);
// Merge in env vaiues from:
// - process.env
// - the .env file
// - the additional env provided to the plugin
return {
...existing,
...this.additionalEnv
};
}
/**
* Build up the necessary definitions and add them to the DefinePlugin.
*/
get plugin() {
const { EnvironmentPlugin } = require('webpack');
return new EnvironmentPlugin(this.env);
}
/**
* Build all MIX_ definitions for Webpack's DefinePlugin.
* This is no longer used but here for backwards compat.
*
* @deprecated
* @param {Record<string, string>} additionalEnv
*/
getDefinitions(additionalEnv) {
return Object.fromEntries(
Object.entries({ ...this.env, ...additionalEnv }).map(([key, value]) => {
return [`process.env.${key}`, JSON.stringify(value)];
})
);
}
/**
* Build up the necessary definitions and add them to the DefinePlugin.
*
* Here for backwards compat only
* @deprecated
* @param {Record<string, string>} additionalEnv
*/
static build(additionalEnv) {
return new MixDefinitionsPlugin(global.Mix.paths.root('.env'), additionalEnv)
.plugin;
}
}
module.exports = MixDefinitionsPlugin;
@@ -0,0 +1,41 @@
let File = require('../File');
let path = require('path');
class MockEntryPlugin {
/**
* @param {import('../Mix')} mix
*/
constructor(mix) {
// TODO: Remove in Mix 7 -- Here for backwards compat if a plugin requires this file
this.mix = mix || global.Mix;
}
/**
* Handle the deletion of the temporary mix.js
* output file that was generated by webpack.
*
* This file is created when the user hasn't
* requested any JavaScript compilation, but
* webpack still requires an entry.
*
* @param {import("webpack").Compiler} compiler
*/
apply(compiler) {
compiler.hooks.done.tap('MockEntryPlugin', stats => {
const assets = stats.toJson().assets || [];
const temporaryOutputFile = assets.find(asset => asset.name === 'mix.js');
if (!temporaryOutputFile) {
return;
}
delete stats.compilation.assets[temporaryOutputFile.name];
File.find(
path.resolve(this.mix.config.publicPath, temporaryOutputFile.name)
).delete();
});
}
}
module.exports = MockEntryPlugin;
+461
View File
@@ -0,0 +1,461 @@
// Type definitions for laravel-mix 6.0
// Pulled from @types/browser-sync because reliance on outdated chokidar
// -------- @types/browser-sync/index.d.ts --------
// Type definitions for browser-sync 2.26
// Project: http://www.browsersync.io/
// Definitions by: Asana <https://asana.com>,
// Joe Skeen <https://github.com/joeskeen>
// Thomas "Thasmo" Deinhamer <https://thasmo.com/>
// Kiyotoshi Ichikawa <https://github.com/aznnomness>
// Yuma Hashimoto <https://github.com/yuma84>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
/// <reference types="node" />
/// <reference types="serve-static" />
import * as chokidar from 'chokidar';
import * as http from 'http';
import { ServeStaticOptions } from 'serve-static';
export interface Options {
/**
* Browsersync includes a user-interface that is accessed via a separate port. The UI allows to controls
* all devices, push sync updates and much more.
*
* port - Default: 3001
* weinre.port - Default: 8080
* Note: Requires at least version 2.0.0.
*/
ui?: UIOptions | boolean;
/**
* Browsersync can watch your files as you work. Changes you make will either be injected into the page (CSS
* & images) or will cause all browsers to do a full-page refresh. See anymatch for more information on glob
* patterns.
* Default: false
*/
files?: string | (string | FileCallback | object)[];
/**
* Specify which file events to respond to.
* Available events: `add`, `change`, `unlink`, `addDir`, `unlinkDir`
*/
watchEvents?: WatchEvents | string[];
/**
* Watch files automatically.
*/
watch?: boolean;
/**
* Patterns for any watchers to ignore.
* Anything provided here will end up inside 'watchOptions.ignored'.
*/
ignore?: string[];
/**
* Serve an index.html file for all non-asset routes.
* Useful when using client-routers.
*/
single?: boolean;
/**
* File watching options that get passed along to Chokidar. Check their docs for available options
* Default: undefined
* Note: Requires at least version 2.6.0.
*/
watchOptions?: chokidar.WatchOptions;
/**
* Use the built-in static server for basic HTML/JS/CSS websites.
* Default: false
*/
server?: string | boolean | string[] | ServerOptions;
/**
* Proxy an EXISTING vhost. Browsersync will wrap your vhost with a proxy URL to view your site.
* Passing only a URL as a string equates to passing only target property of ProxyOptions type.
* target - Default: undefined
* ws - Default: undefined
* middleware - Default: undefined
* reqHeaders - Default: undefined
* proxyRes - Default: undefined (http.ServerResponse if expecting single parameter)
* proxyReq - Default: undefined
*/
proxy?: string | ProxyOptions;
/**
* Use a specific port (instead of the one auto-detected by Browsersync)
* Default: 3000
*/
port?: number;
/**
* Functions or actual plugins used as middleware.
*/
middleware?:
| MiddlewareHandler
| PerRouteMiddleware
| (MiddlewareHandler | PerRouteMiddleware)[];
/**
* Add additional directories from which static files should be served.
* Should only be used in proxy or snippet mode.
* Default: []
* Note: Requires at least version 2.8.0.
*/
serveStatic?: StaticOptions[] | string[];
/**
* Options that are passed to the serve-static middleware when you use the
* string[] syntax: eg: `serveStatic: ['./app']`.
* Please see [serve-static](https://github.com/expressjs/serve-static) for details.
*/
serveStaticOptions?: ServeStaticOptions;
/**
* Enable https for localhost development.
* Note: This may not be needed for proxy option as it will try to infer from your target url.
* Note: If privacy error is encountered please see HttpsOptions below, setting those will resolve.
* Note: Requires at least version 1.3.0.
*/
https?: boolean | HttpsOptions;
/**
* Override http module to allow using 3rd party server modules (such as http2).
*/
httpModule?: string;
/**
* Clicks, Scrolls & Form inputs on any device will be mirrored to all others.
* clicks - Default: true
* scroll - Default: true
* forms - Default: true
*/
ghostMode?: GhostOptions | boolean;
/**
* Can be either "info", "debug", "warn", or "silent"
* Default: info
*/
logLevel?: LogLevel;
/**
* Change the console logging prefix. Useful if you're creating your own project based on Browsersync
* Default: BS
* Note: Requires at least version 1.5.1.
*/
logPrefix?: string;
/**
* Whether or not to log connections
* Default: false
*/
logConnections?: boolean;
/**
* Whether or not to log information about changed files
* Default: false
*/
logFileChanges?: boolean;
/**
* Log the snippet to the console when you're in snippet mode (no proxy/server)
* Default: true
* Note: Requires at least version 1.5.2.
*/
logSnippet?: boolean;
/**
* You can control how the snippet is injected onto each page via a custom regex + function.
* You can also provide patterns for certain urls that should be ignored from the snippet injection.
* Note: Requires at least version 2.0.0.
*/
snippetOptions?: SnippetOptions;
/**
* Add additional HTML rewriting rules.
* Default: false
* Note: Requires at least version 2.4.0.
*/
rewriteRules?: boolean | RewriteRules[];
/**
* Tunnel the Browsersync server through a random Public URL
* Default: null
*/
tunnel?: string | boolean;
/**
* Some features of Browsersync (such as xip & tunnel) require an internet connection, but if you're
* working offline, you can reduce start-up time by setting this option to false
*/
online?: boolean;
/**
* Default: true
* Decide which URL to open automatically when Browsersync starts. Defaults to "local" if none set.
* Can be true, local, external, ui, ui-external, tunnel or false
*/
open?: OpenOptions | boolean;
/**
* The browser(s) to open
* Default: default
*/
browser?: string | string[];
/**
* Add HTTP access control (CORS) headers to assets served by Browsersync.
* Default: false
* Note: Requires at least version 2.16.0.
*/
cors?: boolean;
/**
* Requires an internet connection - useful for services such as Typekit as it allows you to configure
* domains such as *.xip.io in your kit settings
* Default: false
*/
xip?: boolean;
/**
* Reload each browser when Browsersync is restarted.
* Default: false
*/
reloadOnRestart?: boolean;
/**
* The small pop-over notifications in the browser are not always needed/wanted.
* Default: true
*/
notify?: boolean;
/**
* scrollProportionally: false // Sync viewports to TOP position
* Default: true
*/
scrollProportionally?: boolean;
/**
* How often to send scroll events
* Default: 0
*/
scrollThrottle?: number;
/**
* Decide which technique should be used to restore scroll position following a reload.
* Can be window.name or cookie
* Default: 'window.name'
*/
scrollRestoreTechnique?: string;
/**
* Sync the scroll position of any element on the page. Add any amount of CSS selectors
* Default: []
* Note: Requires at least version 2.9.0.
*/
scrollElements?: string[];
/**
* Default: []
* Note: Requires at least version 2.9.0.
* Sync the scroll position of any element on the page - where any scrolled element will cause
* all others to match scroll position. This is helpful when a breakpoint alters which element
* is actually scrolling
*/
scrollElementMapping?: string[];
/**
* Time, in milliseconds, to wait before instructing the browser to reload/inject following a file
* change event
* Default: 0
*/
reloadDelay?: number;
/**
* Restrict the frequency in which browser:reload events can be emitted to connected clients
* Default: 0
* Note: Requires at least version 2.6.0.
*/
reloadDebounce?: number;
/**
* Emit only the first event during sequential time windows of a specified duration.
* Note: Requires at least version 2.13.0.
*/
reloadThrottle?: number;
/**
* User provided plugins
* Default: []
* Note: Requires at least version 2.6.0.
*/
plugins?: any[];
/**
* Whether to inject changes (rather than a page refresh)
* Default: true
*/
injectChanges?: boolean;
/**
* The initial path to load
*/
startPath?: string;
/**
* Whether to minify the client script
* Default: true
*/
minify?: boolean;
/**
* Override host detection if you know the correct IP to use
*/
host?: string;
/**
* Support environments where dynamic hostnames are not required (ie: electron).
*/
localOnly?: boolean;
/**
* Send file-change events to the browser
* Default: true
*/
codeSync?: boolean;
/**
* Append timestamps to injected files
* Default: true
*/
timestamps?: boolean;
/**
* ¯\_(ツ)_/¯
* Best guess, when ghostMode (or SocketIO?) is setup the events
* listed here will be emitted and able to hook into.
*/
clientEvents?: string[];
/**
* Alter the script path for complete control over where the Browsersync Javascript is served
* from. Whatever you return from this function will be used as the script path.
* Note: Requires at least version 1.5.0.
*/
scriptPath?: (path: string) => string;
/**
* Configure the Socket.IO path and namespace & domain to avoid collisions.
* path - Default: "/browser-sync/socket.io"
* clientPath - Default: "/browser-sync"
* namespace - Default: "/browser-sync"
* domain - Default: undefined
* port - Default: undefined
* clients.heartbeatTimeout - Default: 5000
* Note: Requires at least version 1.6.2.
*/
socket?: SocketOptions;
/**
* ¯\_(ツ)_/¯
*/
tagNames?: TagNamesOptions;
/**
* ¯\_(ツ)_/¯
*/
injectFileTypes?: string[];
/**
* ¯\_(ツ)_/¯
*/
excludeFileTypes?: string[];
}
type WatchEvents = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir';
type LogLevel = 'info' | 'debug' | 'warn' | 'silent';
type OpenOptions = 'local' | 'external' | 'ui' | 'ui-external' | 'tunnel';
interface Hash<T> {
[path: string]: T;
}
interface UIOptions {
/** set the default port */
port?: number;
/** set the default weinre port */
weinre?: {
port?: number;
};
}
interface FileCallback {
match?: string | string[];
fn: (event: string, file: string) => any;
options?: chokidar.WatchOptions;
}
interface ServerOptions {
/** set base directory */
baseDir?: string | string[];
/** enable directory listing */
directory?: boolean;
/** set index filename */
index?: string;
/**
* key-value object hash, where the key is the url to match,
* and the value is the folder to serve (relative to your working directory)
*/
routes?: Hash<string>;
/** configure custom middleware */
middleware?: (MiddlewareHandler | PerRouteMiddleware)[];
serveStaticOptions?: ServeStaticOptions;
}
interface ProxyOptions {
target?: string;
middleware?: MiddlewareHandler;
ws?: boolean;
reqHeaders?: (config: object) => Hash<object>;
proxyRes?: ProxyResponseMiddleware | ProxyResponseMiddleware[];
proxyReq?:
| ((res: http.IncomingMessage) => void)[]
| ((res: http.IncomingMessage) => void);
error?: (
err: NodeJS.ErrnoException,
req: http.IncomingMessage,
res: http.ServerResponse
) => void;
}
interface ProxyResponseMiddleware {
(
proxyRes: http.ServerResponse | http.IncomingMessage,
res: http.ServerResponse,
req: http.IncomingMessage
): void;
}
interface HttpsOptions {
key?: string;
cert?: string;
}
interface StaticOptions {
route: string | string[];
dir: string | string[];
}
interface MiddlewareHandler {
(req: http.IncomingMessage, res: http.ServerResponse, next: () => void): any;
}
interface PerRouteMiddleware {
id?: string;
route: string;
handle: MiddlewareHandler;
}
interface GhostOptions {
clicks?: boolean;
scroll?: boolean;
forms?: FormsOptions | boolean;
}
interface FormsOptions {
inputs: boolean;
submit: boolean;
toggles: boolean;
}
interface SnippetOptions {
async?: boolean;
whitelist?: string[];
blacklist?: string[];
rule?: {
match?: RegExp;
fn?: (snippet: string, match: string) => any;
};
}
interface SocketOptions {
path?: string;
clientPath?: string;
namespace?: string;
domain?: string;
port?: number;
clients?: { heartbeatTimeout?: number };
}
interface TagNamesOptions {
less?: string;
scss?: string;
css?: string;
jpg?: string;
jpeg?: string;
png?: string;
svg?: string;
gif?: string;
js?: string;
}
interface RewriteRules {
match: RegExp;
replace?: string;
fn?: (req: http.IncomingMessage, res: http.ServerResponse, match: string) => string;
}
+89
View File
@@ -0,0 +1,89 @@
// Type definitions for laravel-mix 6.0
import * as webpack from 'webpack';
import { TransformOptions as BabelConfig } from '@babel/core';
import Entry from '../src/builder/Entry';
export type DependencyObject = {
/** The name of the package */
package: string;
/**
* An additional, manual check to see if the package that is installed matches necessary expectations.
*
* For example you may use this to see if the installed package is the right version or has an expected API.
*
* The passed object is `require(package)`
**/
check?: (obj: any) => boolean;
};
/** A required depedency for this component to work */
export type Dependency = string | DependencyObject;
export interface ClassComponent {
prototype: ComponentInterface;
new (): ComponentInterface;
}
export interface ComponentInterface {
/** Whether or not to automatically register this component */
passive?: boolean;
/** Whether or not this component requires dependency reloading */
requiresReload?: boolean;
/**
* Register this component
*
* `args` reflect what was passed in via `mix.component_name(...args)`
**/
register?(...args: any[]): void;
/**
* The public API name / names for this component.
* These get attached to the mix object and when called will register this compoent.
*
* For example if name returns `['foo', 'bar']` then you may
* register this component via both `mix.foo()` and `mix.bar()`
**/
name?(): string | string[];
/**
* Specifiy one or more dependencies that must
* be installed for this component to work
**/
dependencies?(): Dependency | Dependency[];
/**
* Boot this component
* This is called after registration and dependencies have been installed
* but before any configuration changes have taken place
**/
boot?(): void;
/** Modify the babel config */
babelConfig?(): BabelConfig;
/** Update the build webpack entries */
webpackEntry?(entry: Entry): void;
/** Add one or more rules to the webpack config */
webpackRules?(): webpack.RuleSetRule | webpack.RuleSetRule[];
/** Add one or more plugins to the webpack config */
webpackPlugins?(): webpack.WebpackPluginInstance[];
/** Update the webpack config */
webpackConfig?(config: webpack.Configuration): void;
/** Add additional components to the Mix API */
mix?(): Record<string, Component>;
}
export interface FunctionalComponent {
(config: webpack.Configuration, ...args: any[]): void;
}
export type InstallableComponent = ComponentInterface | ClassComponent;
export type Component = InstallableComponent | FunctionalComponent;
+168
View File
@@ -0,0 +1,168 @@
// Type definitions for laravel-mix 6.0
import * as webpack from 'webpack';
import { Options as AutoprefixerConfig } from 'autoprefixer';
import { Options as CleanCssConfig } from 'clean-css';
// import { CssNanoOptions as CssNanoConfig } from 'cssnano';
import { CssNanoOptions as CssNanoConfig } from './cssnano';
import { Options as GifsicleConfig } from 'imagemin-gifsicle';
import { Options as MozjpegConfig } from 'imagemin-mozjpeg';
import { Options as OptipngConfig } from 'imagemin-optipng';
import { Options as SvgoConfig } from 'imagemin-svgo';
import { TransformOptions as BabelConfig } from '@babel/core';
// import { TerserPluginOptions } from 'terser-webpack-plugin';
import { TerserPluginOptions } from './terser';
import { AcceptedPlugin } from 'postcss';
interface MixConfig {
/** Determine if webpack should be triggered in a production environment. */
production?: boolean;
/** Determine if we should enable hot reloading. */
hmr?: boolean;
/** Hostname and port used for the hot reload module */
hmrOptions?: {
host: string;
port: string;
};
/** Determine if we should enable https on hot reloading. */
https?: boolean;
/**
* PostCSS plugins to be applied to compiled CSS.
*
* See: https://github.com/postcss/postcss/blob/master/docs/plugins.md
**/
postCss?: AcceptedPlugin[];
/**
* Determine if we should enable autoprefixer by default.
* May be set to false to disable it.
**/
autoprefixer?: false | AutoprefixerConfig;
/** The public path for the build. */
publicPath?: string;
/**
* The name / path to the mix manifest.
* The path is relative to the public path.
*
* Set to `false` to disable manifest generation.
**/
manifest?: string | false;
/**
* The path for the runtime chunk (`manifest.js`).
*
* Defaults to being placed next to compiled JS files.
**/
runtimeChunkPath?: string | null;
/** Determine if error notifications should be displayed for each build. */
notifications?: {
onSuccess?: boolean;
onFailure?: boolean;
};
/**
* Determine if sourcemaps should be created for the build.
*
* @deprecated Please use mix.sourceMaps() instead
**/
sourcemaps?: false | string;
/** The resource root for the build. */
resourceRoot?: string;
/**
* Image Loader defaults.
* See: https://github.com/thetalecrafter/img-loader#options
**/
imgLoaderOptions?: {
enabled?: boolean;
gifsicle?: GifsicleConfig;
mozjpeg?: MozjpegConfig;
optipng?: OptipngConfig;
svgo?: SvgoConfig;
};
/** File Loader directory defaults. */
fileLoaderDirs?: {
images?: string;
fonts?: string;
};
/**
* Determine if CSS relative url()s should be resolved by webpack.
* Disabling this can improve performance greatly.
**/
processCssUrls?: boolean;
/**
* Terser-specific settings for Webpack.
*
* See: https://github.com/webpack-contrib/terser-webpack-plugin#options
**/
terser?: TerserPluginOptions;
/**
* Sets the css module identifier pattern
*
* For more information see https://github.com/webpack-contrib/css-loader/tree/v5.2.7#localidentname
*/
cssModuleIdentifier?: string;
/**
* cssnano-specific settings for Webpack.
* Disabled if set to false.
*
* See: https://cssnano.co/optimisations/
**/
cssNano?: false | CssNanoConfig;
/**
* CleanCss-specific settings for Webpack.
*
* See: https://github.com/jakubpawlowicz/clean-css#constructor-options
**/
cleanCss?: CleanCssConfig;
/**
* Custom Webpack-specific configuration to merge/override Mix's.
*
* @see mix.webpackConfig()
**/
webpackConfig?: webpack.Configuration;
/** Custom Babel configuration to be merged with Mix's defaults. */
babelConfig?: BabelConfig;
/**
* 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
**/
clearConsole?: boolean;
/**
* Enable legacy node -> browser polyfills for things like `process` and `Buffer`.
*/
legacyNodePolyfills?: boolean;
/**
* Options to pass to vue-loader
*
* @deprecated Use `.vue({options: {…}})` instead
**/
vue?: any;
/** The default Babel configuration. */
babel?: (babelRcPath?: string) => BabelConfig;
/** Merge the given options with the current defaults. */
merge?: (options: MixConfig) => void;
}
+25
View File
@@ -0,0 +1,25 @@
// Type definitions for laravel-mix 6.0
// cssnano v5 does not have a types file yet
import { AcceptedPlugin } from 'postcss';
type CSSNanoPluginResolved = [AcceptedPlugin, Record<string, any>];
type CSSNanoPlugin =
| string
| AcceptedPlugin
| [string, Record<string, any>]
| CSSNanoPluginResolved;
type CSSNanoPresetFn = () => CSSNanoPresetResolved;
type CSSNanoPresetResolved = { plugins: CSSNanoPluginResolved[] };
type CSSNanoPreset =
| string
| CSSNanoPresetFn
| [string | CSSNanoPresetFn, Record<string, any>]
| CSSNanoPresetResolved;
interface CssNanoOptions {
configFile?: string;
preset?: CSSNanoPreset;
plugins?: CSSNanoPlugin[];
}
+37
View File
@@ -0,0 +1,37 @@
// Type definitions for laravel-mix 6.0
import * as webpack from 'webpack';
/**
* Specify a custom extraction test
* Equivalent to providing a function to webpack's split chunks
*/
export type ExtractTestCallback = (module: webpack.Module, context: any) => boolean;
/**
* Specify a custom extraction test
* Can be a regular expression or a function
*/
export type Extraction = {
/** The path to extract files to */
to: string;
/** A custom test regex or callback — takes precedence over libraries */
test: RegExp | ExtractTestCallback;
/** A list of libraries to match against */
libraries: string[] | RegExp;
};
export type ExtractConfig =
// A filename for node_modules extractions
| string
// A list of vendor files
| string[]
// Custom extraction test function
| ExtractTestCallback
// Extraction config
| Partial<Extraction>;
+28
View File
@@ -0,0 +1,28 @@
import api from './index';
import { MixConfig } from './config';
import MixHelpers from '../src/Mix';
import WebpackConfig from '../src/builder/WebpackConfig';
type ElementType<T> = T extends ReadonlyArray<infer U> ? ElementType<U> : T;
declare global {
// Mix test-only globals
declare var mix = api;
// General globals added by including Mix
declare var Mix: MixHelpers;
declare var Config: MixConfig;
declare var webpackConfig: WebpackConfig;
// Helpers added by including Mix
declare var tap: <T>(val: T, callback: (v: T) => void) => T;
declare var flatten: <T extends unknown[]>(arr: T) => ElementType<T>[];
interface ArrayConstructor {
wrap<T>(val: T | T[] | ArrayLike<T>): T[];
}
interface Array {
tap(callback: (arr: this) => void): this;
}
}
+331
View File
@@ -0,0 +1,331 @@
// Type definitions for laravel-mix 6.0
// Project: https://github.com/JeffreyWay/laravel-mix#readme
// Definitions by: Geoff Garbers <https://github.com/garbetjie>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
import * as webpack from 'webpack';
import { Options } from 'ts-loader';
import { TransformOptions as BabelConfig } from '@babel/core';
import { Options as BrowserSyncConfig } from './browsersync';
import * as ExtractTypes from './extract';
import { MixConfig } from './config';
import { Component } from './component';
// @ts-ignore - May not be installed initially
import { AcceptedPlugin } from 'postcss';
// @ts-ignore - May not be installed initially
import { VueLoaderOptions } from 'vue-loader';
// @ts-ignore - May not be installed initially
import MixHelpers from '../src/Mix';
// General API
interface Api {
/** Set the public path */
setPublicPath(path: string): Api;
/** Set the resource path */
setResourceRoot(path: string): Api;
/** Override Mix config options */
options(options: MixConfig): Api;
/** Determine if mix is building for production */
inProduction(): boolean;
/** Extend the mix api. This makes the component available as mix.name_here */
extend(name: string, component: Component): Api;
/** Wait for a callback before starting the build */
before(callback: (Mix: MixHelpers) => void | Promise<void>): Api;
/** Run a callback after the build has completed */
after(callback: (stats: webpack.Stats) => void | Promise<void>): Api;
/** Run a callback after the build has completed */
then(callback: (stats: webpack.Stats) => void | Promise<void>): Api;
/** Run a callback if a condition is true */
when(condition: boolean, callback: (mix: Api) => void | Promise<void>): Api;
}
// Webpack config related abilities
interface Api {
/** Merge custom webpack config */
webpackConfig(config: webpack.Configuration): Api;
/** Merge custom webpack config */
webpackConfig(
callback: (
webpack: typeof import('webpack'),
config: webpack.Configuration
) => webpack.Configuration
): Api;
/** Override the webpack config after it is built */
override(callback: (config: webpack.Configuration) => void): Api;
/** Override the used babel config */
babelConfig(config: BabelConfig): Api;
/** Dump the webpack config when mix builds */
dump(): Api;
/** Dump the webpack config when mix builds */
dumpWebpackConfig(): Api;
}
type CodeValuePrimitive = undefined | null | string | number | bigint | boolean;
type LazyCodeValue = () => CodeValuePrimitive;
type CodeValue = CodeValuePrimitive | LazyCodeValue;
// Assorted capabilities
interface Api {
/** Add webpack-resolution aliases */
alias(paths: Record<string, string | { raw: string }>): Api;
/** Autoload libraries */
autoload(libraries: Record<string, string | string[]>): Api;
/** Use BrowserSync to monitor files for changes and inject changes into the browser */
browserSync(proxy: string): Api;
/** Use BrowserSync to monitor files for changes and inject changes into the browser */
browserSync(config?: BrowserSyncConfig): Api;
/** Generate source maps */
sourceMaps(
generateForProduction?: boolean,
devType?: string,
productionType?: string
): Api;
/**
* Version assets by hashing them and placing this hash url used in the mix manifest
*
* You may optionally pass an additional list of files to version in the mix manifest.
**/
version(files?: string | string[]): Api;
/** Disable system notifications when mix builds assets */
disableNotifications(): Api;
/** Disable only success notifications when mix builds assets */
disableSuccessNotifications(): Api;
/** Replace variables in code with other values */
define(definitions: Record<string, CodeValue>): Api;
}
// JS / Transpilation capabilities
interface Api {
/**
* Compile modern javascript
*
* `src` may be a glob pattern
**/
js(src: string | string[], output: string): Api;
/**
* Compile typescript into JS
*
* `src` may be a glob pattern
**/
ts(src: string | string[], output: string, options?: Partial<Options>): Api;
/**
* Compile TypeScript into JS
*
* `src` may be a glob pattern
**/
typeScript(src: string | string[], output: string, options?: Partial<Options>): Api;
/**
* Compile CoffeeScript into JS
*
* `src` may be a glob pattern
**/
coffee(src: string | string[], output: string): Api;
}
// File related capabilities
interface Api {
/** Copy files specified by `from` and place them in `to` */
copy(from: string | string[], to: string): Api;
/** Copy files specified by `from` and place them in `to` */
copyDirectory(from: string | string[], to: string): Api;
/**
* Concatenate files specified by `src` and place them in `output` (or the public dir by default)
*
* You may pass a glob pattern to `src` to match several files.
**/
combine(src: string | string[], output?: string, babel?: boolean): Api;
/**
* Concatenate files specified by `src` and place them in `output` (or the public dir by default)
*
* You may pass a glob pattern to `src` to match several files.
**/
babel(src: string | string[], output?: string, babel?: boolean): Api;
/**
* Concatenate files specified by `src` and place them in `output` (or the public dir by default)
*
* You may pass a glob pattern to `src` to match several files.
**/
minify(src: string | string[], output?: string, babel?: boolean): Api;
/**
* Concatenate files specified by `src` and place them in `output` (or the public dir by default)
*
* You may pass a glob pattern to `src` to match several files.
**/
scripts(src: string | string[], output?: string, babel?: boolean): Api;
/**
* Concatenate files specified by `src` and place them in `output` (or the public dir by default)
*
* You may pass a glob pattern to `src` to match several files.
**/
styles(src: string | string[], output?: string, babel?: boolean): Api;
}
// Styling related capabilities
interface Api {
/** Compile CSS via PostCSS */
css(src: string, output: string, plugins?: AcceptedPlugin[]): Api;
/** Compile CSS via PostCSS */
css(
src: string,
output: string,
pluginOptions?: object,
plugins?: AcceptedPlugin[]
): Api;
/** Compile CSS via PostCSS */
postCss(src: string, output: string, plugins?: AcceptedPlugin[]): Api;
/** Compile CSS via PostCSS */
postCss(
src: string,
output: string,
pluginOptions?: object,
plugins?: AcceptedPlugin[]
): Api;
/** Compile Stylus into CSS */
stylus(
src: string,
output: string,
pluginOptions?: object,
postCssPlugins?: AcceptedPlugin[]
): Api;
/** Compile Less into CSS */
less(
src: string,
output: string,
pluginOptions?: object,
postCssPlugins?: AcceptedPlugin[]
): Api;
/** Compile Sass/SCSS into CSS */
sass(
src: string,
output: string,
pluginOptions?: object,
postCssPlugins?: AcceptedPlugin[]
): Api;
}
// Vendor extraction
interface Api {
/** Extract all matching chunks to `config.to` / `output` / vendor.js */
extract(config: Partial<ExtractTypes.Extraction>, output?: string): Api;
/** Extract all chunks matching the given test to `output` / vendor.js */
extract(test: ExtractTypes.ExtractTestCallback, output?: string): Api;
/** Extract the given libraries into `output` / vendor.js */
extract(libs: string[], output?: string): Api;
/** Extract libraries from node_modules into `output` / vendor.js */
extract(output?: string): Api;
}
type VueConfig = {
/** Which version of Vue to support. Detected automatically if not given. */
version?: number;
/** A file to include w/ every vue style block. */
globalStyles?: false | string | string[] | Record<string, string | string[]> | null;
/** Whether or not to extract vue styles. If given a string the name of the file to extract to. */
extractStyles?: boolean | string;
/** Use vue-style-loader to extract Vue Styles. */
useVueStyleLoader?: boolean;
/** Whether or not to use the runtime only version of Vue. */
runtimeOnly?: boolean;
/** Options to pass to Vue Loader */
options?: VueLoaderOptions;
};
type ReactConfig = {
/** Whether or not to extract React styles. If given a string the name of the file to extract to. */
extractStyles?: boolean | string;
};
// Third-party support "feature flags"
interface Api {
/** Enable support for Preact */
preact(): Api;
/** Enable support for React */
react(config?: ReactConfig): Api;
/**
* Enable support for Vue 2 or 3
*
* The version of Vue in use is detected but may be specified explicitly using `version`
*
* Pass options to configure global styles and Vue component style extraction
**/
vue(config?: VueConfig): Api;
}
declare const exports: Api;
declare namespace exports {
export { Api, ReactConfig, VueConfig };
}
declare global {
/**
* The context for the current mix build group
*
* @deprecated This will go away in a future release
**/
var Mix: import('../src/Mix.js');
/**
* The webpack config helper for the current mix build group
*
* @deprecated This will go away in a future release
**/
var webpackConfig: import('../src/builder/WebpackConfig');
/**
* The options for the current mix build group
*
* @deprecated This will go away in a future release
**/
var Config: ReturnType<typeof import('../src/config')>;
}
export = exports;
+50
View File
@@ -0,0 +1,50 @@
// Type definitions for laravel-mix 6.0
// This is a workaround because `TerserPluginOptions` isn't exported from webpack-terser-plugin
// import { Rules } from 'webpack';
import { MinifyOptions } from 'terser';
import { RawSourceMap } from 'source-map';
// This type isn't exported from webpack…
type Rules = string | RegExp | (string | RegExp)[];
export type TerserPluginOptions = {
test?: Rules;
include?: Rules;
exclude?: Rules;
terserOptions?: MinifyOptions;
extractComments?: ExtractCommentsOptions;
parallel?: boolean;
minify?: CustomMinifyFunction;
};
// None of these types are exported from Terser
type ExtractCommentsBanner = boolean | string | ((commentsFile: string) => string);
type ExtractCommentsCondition = boolean | string | RegExp | ExtractCommentsFunction;
type ExtractCommentsFilename = string | ((fileData: any) => string);
type ExtractCommentsObject = {
condition: ExtractCommentsCondition;
filename: ExtractCommentsFilename;
banner: ExtractCommentsBanner;
};
type ExtractCommentsFunction = (
astNode: any,
comment: {
value: string;
type: 'comment1' | 'comment2' | 'comment3' | 'comment4';
pos: number;
line: number;
col: number;
}
) => boolean;
type ExtractCommentsOptions = ExtractCommentsCondition | ExtractCommentsObject;
type CustomMinifyFunction = (
file: Record<string, string>,
sourceMap: RawSourceMap,
minifyOptions: MinifyOptions
) => void;