Last commit july 5th

This commit is contained in:
2024-07-05 13:46:23 +02:00
parent dad0d86e8c
commit b0e4dfbb76
24982 changed files with 2621219 additions and 413 deletions

View File

@@ -0,0 +1,102 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const chalk = require('chalk');
const levenshtein = require('fast-levenshtein');
const prettyError = require('./utils/pretty-error');
module.exports = {
createProxy: (Encore) => {
const EncoreProxy = new Proxy(Encore, {
get: (target, prop) => {
if (typeof prop !== 'string') {
// Only care about strings there since prop
// could also be a number or a symbol
return target[prop];
}
if (prop === '__esModule') {
// When using Babel to preprocess a webpack.config.babel.js file
// (for instance if we want to use ES6 syntax) the __esModule
// property needs to be whitelisted to avoid an "Unknown property"
// error.
return target[prop];
}
if (typeof target[prop] === 'function') {
// These methods of the public API can be called even if the
// webpackConfig object hasn't been initialized yet.
const safeMethods = [
'configureRuntimeEnvironment',
'clearRuntimeEnvironment',
'isRuntimeEnvironmentConfigured',
];
if (!Encore.isRuntimeEnvironmentConfigured() && !safeMethods.includes(prop)) {
throw new Error(`Encore.${prop}() cannot be called yet because the runtime environment doesn't appear to be configured. Make sure you're using the encore executable or call Encore.configureRuntimeEnvironment() first if you're purposely not calling Encore directly.`);
}
// Either a safe method has been called or the webpackConfig
// object is already available. In this case act as a passthrough.
return (...parameters) => {
try {
const res = target[prop](...parameters);
return (res === target) ? EncoreProxy : res;
} catch (error) {
prettyError(error);
process.exit(1); // eslint-disable-line
}
};
}
if (typeof target[prop] === 'undefined') {
// Find the property with the closest Levenshtein distance
let similarProperty;
let minDistance = Number.MAX_VALUE;
const encorePrototype = Object.getPrototypeOf(Encore);
for (const apiProperty of Object.getOwnPropertyNames(encorePrototype)) {
// Ignore class constructor
if (apiProperty === 'constructor') {
continue;
}
const distance = levenshtein.get(apiProperty, prop);
if (distance <= minDistance) {
similarProperty = apiProperty;
minDistance = distance;
}
}
let errorMessage = `${chalk.red(`Encore.${prop}`)} is not a recognized property or method.`;
if (minDistance < (prop.length / 3)) {
errorMessage += ` Did you mean ${chalk.green(`Encore.${similarProperty}`)}?`;
}
// Prettify the error message.
// Only keep the 2nd line of the stack trace:
// - First line should be the index.js file
// - Second line should be the Webpack config file
prettyError(
new Error(errorMessage),
{ skipTrace: (traceLine, lineNumber) => lineNumber !== 1 }
);
process.exit(1); // eslint-disable-line
}
return target[prop];
}
});
return EncoreProxy;
}
};

View File

@@ -0,0 +1,975 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const RuntimeConfig = require('./config/RuntimeConfig'); //eslint-disable-line no-unused-vars
const logger = require('./logger');
const regexpEscaper = require('./utils/regexp-escaper');
const { calculateDevServerUrl } = require('./config/path-util');
/**
* @param {RuntimeConfig|null} runtimeConfig
* @return {void}
*/
function validateRuntimeConfig(runtimeConfig) {
// if you're using the encore executable, these things should never happen
if (null === runtimeConfig) {
throw new Error('RuntimeConfig must be initialized');
}
if (null === runtimeConfig.context) {
throw new Error('RuntimeConfig.context must be set.');
}
if (null === runtimeConfig.babelRcFileExists) {
throw new Error('RuntimeConfig.babelRcFileExists must be set.');
}
}
class WebpackConfig {
constructor(runtimeConfig) {
validateRuntimeConfig(runtimeConfig);
if (runtimeConfig.verbose) {
logger.verbose();
}
this.runtimeConfig = runtimeConfig;
this.entries = new Map();
this.styleEntries = new Map();
this.plugins = [];
this.loaders = [];
// Global settings
this.outputPath = null;
this.publicPath = null;
this.manifestKeyPrefix = null;
this.cacheGroups = {};
this.providedVariables = {};
this.configuredFilenames = {};
this.aliases = {};
this.externals = [];
this.integrityAlgorithms = [];
this.shouldUseSingleRuntimeChunk = null;
this.shouldSplitEntryChunks = false;
// Features/Loaders flags
this.useVersioning = false;
this.useSourceMaps = false;
this.cleanupOutput = false;
this.usePersistentCache = false;
this.extractCss = true;
this.imageRuleOptions = {
type: 'asset/resource',
maxSize: null,
filename: 'images/[name].[hash:8][ext]',
enabled: true,
};
this.fontRuleOptions = {
type: 'asset/resource',
maxSize: null,
filename: 'fonts/[name].[hash:8][ext]',
enabled: true,
};
this.usePostCssLoader = false;
this.useLessLoader = false;
this.useStylusLoader = false;
this.useSassLoader = false;
this.useStimulusBridge = false;
this.useReact = false;
this.usePreact = false;
this.useVueLoader = false;
this.useEslintPlugin = false;
this.useTypeScriptLoader = false;
this.useForkedTypeScriptTypeChecking = false;
this.useBabelTypeScriptPreset = false;
this.useWebpackNotifier = false;
this.useHandlebarsLoader = false;
this.useSvelte = false;
// Features/Loaders options
this.copyFilesConfigs = [];
this.sassOptions = {
resolveUrlLoader: true,
resolveUrlLoaderOptions: {}
};
this.preactOptions = {
preactCompat: false
};
this.babelOptions = {
exclude: /(node_modules|bower_components)/,
useBuiltIns: false,
corejs: null,
};
this.babelTypeScriptPresetOptions = {};
this.vueOptions = {
useJsx: false,
version: null,
runtimeCompilerBuild: null
};
this.persistentCacheBuildDependencies = {};
// Features/Loaders options callbacks
this.imageRuleCallback = () => {};
this.fontRuleCallback = () => {};
this.postCssLoaderOptionsCallback = () => {};
this.sassLoaderOptionsCallback = () => {};
this.lessLoaderOptionsCallback = () => {};
this.stylusLoaderOptionsCallback = () => {};
this.babelConfigurationCallback = () => {};
this.babelPresetEnvOptionsCallback = () => {};
this.cssLoaderConfigurationCallback = () => {};
this.styleLoaderConfigurationCallback = () => {};
this.splitChunksConfigurationCallback = () => {};
this.watchOptionsConfigurationCallback = () => {};
this.devServerOptionsConfigurationCallback = () => {};
this.vueLoaderOptionsCallback = () => {};
this.eslintPluginOptionsCallback = () => {};
this.tsConfigurationCallback = () => {};
this.handlebarsConfigurationCallback = () => {};
this.miniCssExtractLoaderConfigurationCallback = () => {};
this.miniCssExtractPluginConfigurationCallback = () => {};
this.loaderConfigurationCallbacks = {
javascript: () => {},
css: () => {},
images: () => {},
fonts: () => {},
sass: () => {},
less: () => {},
stylus: () => {},
vue: () => {},
eslint: () => {},
typescript: () => {},
handlebars: () => {},
svelte: () => {},
};
// Plugins options
this.cleanWebpackPluginPaths = ['**/*'];
// Plugins callbacks
this.cleanWebpackPluginOptionsCallback = () => {};
this.definePluginOptionsCallback = () => {};
this.forkedTypeScriptTypesCheckOptionsCallback = () => {};
this.friendlyErrorsPluginOptionsCallback = () => {};
this.manifestPluginOptionsCallback = () => {};
this.terserPluginOptionsCallback = () => {};
this.cssMinimizerPluginOptionsCallback = () => {};
this.notifierPluginOptionsCallback = () => {};
this.persistentCacheCallback = () => {};
}
getContext() {
return this.runtimeConfig.context;
}
doesBabelRcFileExist() {
return this.runtimeConfig.babelRcFileExists;
}
setOutputPath(outputPath) {
if (!path.isAbsolute(outputPath)) {
outputPath = path.resolve(this.getContext(), outputPath);
}
if (!fs.existsSync(outputPath)) {
// If the parent of the output directory does not exist either
// check if it is located under the context directory before
// creating it and its parent.
const parentPath = path.dirname(outputPath);
if (!fs.existsSync(parentPath)) {
const context = path.resolve(this.getContext());
if (outputPath.indexOf(context) !== 0) {
throw new Error(`outputPath directory "${outputPath}" does not exist and is not located under the context directory "${context}". Please check the path you're passing to setOutputPath() or create this directory.`);
}
parentPath.split(path.sep).reduce((previousPath, directory) => {
const newPath = path.resolve(previousPath, directory);
if (!fs.existsSync(newPath)) {
fs.mkdirSync(newPath);
}
return newPath;
}, path.sep);
}
fs.mkdirSync(outputPath);
}
this.outputPath = outputPath;
}
setPublicPath(publicPath) {
if (publicPath.includes('://') === false && publicPath.indexOf('/') !== 0) {
// technically, not starting with "/" is legal, but not
// what you want in most cases. Let's warn the user that
// they might be making a mistake.
logger.warning('The value passed to setPublicPath() should *usually* start with "/" or be a full URL (http://...). If you\'re not sure, then you should probably change your public path and make this message disappear.');
}
// guarantee a single trailing slash
publicPath = publicPath.replace(/\/$/,'');
publicPath = publicPath + '/';
this.publicPath = publicPath;
}
setManifestKeyPrefix(manifestKeyPrefix) {
/*
* Normally, we make sure that the manifest keys don't start
* with an opening "/" ever... for consistency. If you need
* to manually specify the manifest key (e.g. because you're
* publicPath is absolute), it's easy to accidentally add
* an opening slash (thereby changing your key prefix) without
* intending to. Hence, the warning.
*/
if (manifestKeyPrefix.indexOf('/') === 0) {
logger.warning(`The value passed to setManifestKeyPrefix "${manifestKeyPrefix}" starts with "/". This is allowed, but since the key prefix does not normally start with a "/", you may have just changed the prefix accidentally.`);
}
// guarantee a single trailing slash, except for blank strings
if (manifestKeyPrefix !== '') {
manifestKeyPrefix = manifestKeyPrefix.replace(/\/$/, '');
manifestKeyPrefix = manifestKeyPrefix + '/';
}
this.manifestKeyPrefix = manifestKeyPrefix;
}
configureDefinePlugin(definePluginOptionsCallback = () => {}) {
if (typeof definePluginOptionsCallback !== 'function') {
throw new Error('Argument 1 to configureDefinePlugin() must be a callback function');
}
this.definePluginOptionsCallback = definePluginOptionsCallback;
}
configureFriendlyErrorsPlugin(friendlyErrorsPluginOptionsCallback = () => {}) {
if (typeof friendlyErrorsPluginOptionsCallback !== 'function') {
throw new Error('Argument 1 to configureFriendlyErrorsPlugin() must be a callback function');
}
this.friendlyErrorsPluginOptionsCallback = friendlyErrorsPluginOptionsCallback;
}
configureManifestPlugin(manifestPluginOptionsCallback = () => {}) {
if (typeof manifestPluginOptionsCallback !== 'function') {
throw new Error('Argument 1 to configureManifestPlugin() must be a callback function');
}
this.manifestPluginOptionsCallback = manifestPluginOptionsCallback;
}
configureTerserPlugin(terserPluginOptionsCallback = () => {}) {
if (typeof terserPluginOptionsCallback !== 'function') {
throw new Error('Argument 1 to configureTerserPlugin() must be a callback function');
}
this.terserPluginOptionsCallback = terserPluginOptionsCallback;
}
configureCssMinimizerPlugin(cssMinimizerPluginOptionsCallback = () => {}) {
if (typeof cssMinimizerPluginOptionsCallback !== 'function') {
throw new Error('Argument 1 to configureCssMinimizerPlugin() must be a callback function');
}
this.cssMinimizerPluginOptionsCallback = cssMinimizerPluginOptionsCallback;
}
/**
* Returns the value that should be used as the publicPath,
* which can be overridden by enabling the webpackDevServer
*
* @returns {string}
*/
getRealPublicPath() {
if (!this.useDevServer()) {
return this.publicPath;
}
if (this.runtimeConfig.devServerKeepPublicPath) {
return this.publicPath;
}
if (this.publicPath.includes('://')) {
return this.publicPath;
}
const devServerUrl = calculateDevServerUrl(this.runtimeConfig);
// if using dev-server, prefix the publicPath with the dev server URL
return devServerUrl.replace(/\/$/,'') + this.publicPath;
}
addEntry(name, src) {
this.validateNameIsNewEntry(name);
this.entries.set(name, src);
}
/**
* Provide a has of entries at once, as an alternative to calling `addEntry` several times.
*
* @param {Object.<string, string|string[]>} entries
* @returns {Void}
*/
addEntries(entries = {}) {
if (typeof entries !== 'object') {
throw new Error('Argument 1 to addEntries() must be an object.');
}
Object.entries(entries).forEach((entry) => this.addEntry(entry[0], entry[1]));
}
addStyleEntry(name, src) {
this.validateNameIsNewEntry(name);
this.styleEntries.set(name, src);
}
addPlugin(plugin, priority = 0) {
if (typeof priority !== 'number') {
throw new Error('Argument 2 to addPlugin() must be a number.');
}
this.plugins.push({
plugin: plugin,
priority: priority
});
}
addLoader(loader) {
this.loaders.push(loader);
}
addAliases(aliases = {}) {
if (typeof aliases !== 'object') {
throw new Error('Argument 1 to addAliases() must be an object.');
}
Object.assign(this.aliases, aliases);
}
addExternals(externals = []) {
if (!Array.isArray(externals)) {
externals = [externals];
}
this.externals = this.externals.concat(externals);
}
enableVersioning(enabled = true) {
this.useVersioning = enabled;
}
enableSourceMaps(enabled = true) {
this.useSourceMaps = enabled;
}
configureBabel(callback, options = {}) {
if (callback) {
if (typeof callback !== 'function') {
throw new Error('Argument 1 to configureBabel() must be a callback function or null.');
}
if (this.doesBabelRcFileExist()) {
throw new Error('The "callback" argument of configureBabel() will not be used because your app already provides an external Babel configuration (e.g. a ".babelrc" or "babel.config.js" file or "babel" key in "package.json"). Use null as the first argument to remove this error.');
}
}
this.babelConfigurationCallback = callback || (() => {});
// Whitelist some options that can be used even if there
// is an external Babel config. The other ones won't be
// applied and a warning message will be displayed instead.
const allowedOptionsWithExternalConfig = ['includeNodeModules', 'exclude'];
for (const optionKey of Object.keys(options)) {
if (this.doesBabelRcFileExist() && !allowedOptionsWithExternalConfig.includes(optionKey)) {
logger.warning(`The "${optionKey}" option of configureBabel() will not be used because your app already provides an external Babel configuration (e.g. a ".babelrc" or "babelrc.config.js" file or "babel" key in "package.json").`);
continue;
}
if (optionKey === 'includeNodeModules') {
if (Object.keys(options).includes('exclude')) {
throw new Error('"includeNodeModules" and "exclude" options can\'t be used together when calling configureBabel().');
}
if (!Array.isArray(options[optionKey])) {
throw new Error('Option "includeNodeModules" passed to configureBabel() must be an Array.');
}
this.babelOptions['exclude'] = (filePath) => {
// Don't exclude modules outside of node_modules/bower_components
if (!/(node_modules|bower_components)/.test(filePath)) {
return false;
}
// Don't exclude whitelisted Node modules
const whitelistedModules = options[optionKey].map(
module => path.join('node_modules', module) + path.sep
);
for (const modulePath of whitelistedModules) {
if (filePath.includes(modulePath)) {
return false;
}
}
// Exclude other modules
return true;
};
} else if (!(optionKey in this.babelOptions)) {
throw new Error(`Invalid option "${optionKey}" passed to configureBabel(). Valid keys are ${[...Object.keys(this.babelOptions), 'includeNodeModules'].join(', ')}`);
} else {
this.babelOptions[optionKey] = options[optionKey];
}
}
}
configureBabelPresetEnv(callback) {
if (typeof callback !== 'function') {
throw new Error('Argument 1 to configureBabelPresetEnv() must be a callback function.');
}
if (this.doesBabelRcFileExist()) {
throw new Error('The "callback" argument of configureBabelPresetEnv() will not be used because your app already provides an external Babel configuration (e.g. a ".babelrc" or "babel.config.js" file or "babel" key in "package.json").');
}
this.babelPresetEnvOptionsCallback = callback;
}
configureCssLoader(callback) {
if (typeof callback !== 'function') {
throw new Error('Argument 1 to configureCssLoader() must be a callback function.');
}
this.cssLoaderConfigurationCallback = callback;
}
configureStyleLoader(callback) {
if (typeof callback !== 'function') {
throw new Error('Argument 1 to configureStyleLoader() must be a callback function.');
}
this.styleLoaderConfigurationCallback = callback;
}
configureMiniCssExtractPlugin(loaderOptionsCallback, pluginOptionsCallback = () => {}) {
if (typeof loaderOptionsCallback !== 'function') {
throw new Error('Argument 1 to configureMiniCssExtractPluginLoader() must be a callback function.');
}
if (typeof pluginOptionsCallback !== 'function') {
throw new Error('Argument 2 to configureMiniCssExtractPluginLoader() must be a callback function.');
}
this.miniCssExtractLoaderConfigurationCallback = loaderOptionsCallback;
this.miniCssExtractPluginConfigurationCallback = pluginOptionsCallback;
}
enableSingleRuntimeChunk() {
this.shouldUseSingleRuntimeChunk = true;
}
disableSingleRuntimeChunk() {
this.shouldUseSingleRuntimeChunk = false;
}
splitEntryChunks() {
this.shouldSplitEntryChunks = true;
}
configureSplitChunks(callback) {
if (typeof callback !== 'function') {
throw new Error('Argument 1 to configureSplitChunks() must be a callback function.');
}
this.splitChunksConfigurationCallback = callback;
}
configureWatchOptions(callback) {
if (typeof callback !== 'function') {
throw new Error('Argument 1 to configureWatchOptions() must be a callback function.');
}
this.watchOptionsConfigurationCallback = callback;
}
configureDevServerOptions(callback) {
if (typeof callback !== 'function') {
throw new Error('Argument 1 to configureDevServerOptions() must be a callback function.');
}
this.devServerOptionsConfigurationCallback = callback;
}
addCacheGroup(name, options) {
if (typeof name !== 'string') {
throw new Error('Argument 1 to addCacheGroup() must be a string.');
}
if (typeof options !== 'object') {
throw new Error('Argument 2 to addCacheGroup() must be an object.');
}
if (!options['test'] && !options['node_modules']) {
throw new Error('Either the "test" option or the "node_modules" option of addCacheGroup() must be set');
}
if (options['node_modules']) {
if (!Array.isArray(options['node_modules'])) {
throw new Error('The "node_modules" option of addCacheGroup() must be an array');
}
options.test = new RegExp(`[\\\\/]node_modules[\\\\/](${
options['node_modules']
.map(regexpEscaper)
.join('|')
})[\\\\/]`);
delete options['node_modules'];
}
this.cacheGroups[name] = options;
}
copyFiles(configs = []) {
if (!Array.isArray(configs)) {
configs = [configs];
}
if (configs.some(elt => typeof elt !== 'object')) {
throw new Error('copyFiles() must be called with either a config object or an array of config objects.');
}
const defaultConfig = {
from: null,
pattern: /.*/,
to: null,
includeSubdirectories: true,
context: null,
};
for (const config of configs) {
if (!config.from) {
throw new Error('Config objects passed to copyFiles() must have a "from" property.');
}
for (const configKey of Object.keys(config)) {
if (!(configKey in defaultConfig)) {
throw new Error(`Invalid config option "${configKey}" passed to copyFiles(). Valid keys are ${Object.keys(defaultConfig).join(', ')}`);
}
}
if (typeof config.pattern !== 'undefined' && !(config.pattern instanceof RegExp)) {
let validPattern = false;
if (typeof config.pattern === 'string') {
const regexPattern = /^\/(.*)\/([a-z]*)?$/;
if (regexPattern.test(config.pattern)) {
validPattern = true;
}
}
if (!validPattern) {
throw new Error(`Invalid pattern "${config.pattern}" passed to copyFiles(). Make sure it contains a valid regular expression.`);
}
}
this.copyFilesConfigs.push(
Object.assign({}, defaultConfig, config)
);
}
}
enablePostCssLoader(postCssLoaderOptionsCallback = () => {}) {
this.usePostCssLoader = true;
if (typeof postCssLoaderOptionsCallback !== 'function') {
throw new Error('Argument 1 to enablePostCssLoader() must be a callback function.');
}
this.postCssLoaderOptionsCallback = postCssLoaderOptionsCallback;
}
enableSassLoader(sassLoaderOptionsCallback = () => {}, options = {}) {
this.useSassLoader = true;
if (typeof sassLoaderOptionsCallback !== 'function') {
throw new Error('Argument 1 to enableSassLoader() must be a callback function.');
}
this.sassLoaderOptionsCallback = sassLoaderOptionsCallback;
for (const optionKey of Object.keys(options)) {
if (!(optionKey in this.sassOptions)) {
throw new Error(`Invalid option "${optionKey}" passed to enableSassLoader(). Valid keys are ${Object.keys(this.sassOptions).join(', ')}`);
}
this.sassOptions[optionKey] = options[optionKey];
}
}
enableLessLoader(lessLoaderOptionsCallback = () => {}) {
this.useLessLoader = true;
if (typeof lessLoaderOptionsCallback !== 'function') {
throw new Error('Argument 1 to enableLessLoader() must be a callback function.');
}
this.lessLoaderOptionsCallback = lessLoaderOptionsCallback;
}
enableStylusLoader(stylusLoaderOptionsCallback = () => {}) {
this.useStylusLoader = true;
if (typeof stylusLoaderOptionsCallback !== 'function') {
throw new Error('Argument 1 to enableStylusLoader() must be a callback function.');
}
this.stylusLoaderOptionsCallback = stylusLoaderOptionsCallback;
}
enableStimulusBridge(controllerJsonPath) {
this.useStimulusBridge = true;
if (!fs.existsSync(controllerJsonPath)) {
throw new Error(`File "${controllerJsonPath}" could not be found.`);
}
// Add configured entrypoints
const controllersData = JSON.parse(fs.readFileSync(controllerJsonPath));
const rootDir = path.dirname(path.resolve(controllerJsonPath));
for (let name in controllersData.entrypoints) {
this.addEntry(name, rootDir + '/' + controllersData.entrypoints[name]);
}
this.addAliases({
'@symfony/stimulus-bridge/controllers.json': path.resolve(controllerJsonPath),
});
}
enableBuildCache(buildDependencies, callback = (cache) => {}) {
if (typeof buildDependencies !== 'object') {
throw new Error('Argument 1 to enableBuildCache() must be an object.');
}
if (!buildDependencies.config) {
throw new Error('Argument 1 to enableBuildCache() should contain an object with at least a "config" key. See the documentation for this method.');
}
this.usePersistentCache = true;
this.persistentCacheBuildDependencies = buildDependencies;
if (typeof callback !== 'function') {
throw new Error('Argument 2 to enableBuildCache() must be a callback function.');
}
this.persistentCacheCallback = callback;
}
enableReactPreset() {
this.useReact = true;
}
enablePreactPreset(options = {}) {
this.usePreact = true;
for (const optionKey of Object.keys(options)) {
if (!(optionKey in this.preactOptions)) {
throw new Error(`Invalid option "${optionKey}" passed to enablePreactPreset(). Valid keys are ${Object.keys(this.preactOptions).join(', ')}`);
}
this.preactOptions[optionKey] = options[optionKey];
}
}
enableSvelte() {
this.useSvelte = true;
}
enableTypeScriptLoader(callback = () => {}) {
if (this.useBabelTypeScriptPreset) {
throw new Error('Encore.enableTypeScriptLoader() can not be called when Encore.enableBabelTypeScriptPreset() has been called.');
}
this.useTypeScriptLoader = true;
if (typeof callback !== 'function') {
throw new Error('Argument 1 to enableTypeScriptLoader() must be a callback function.');
}
this.tsConfigurationCallback = callback;
}
enableForkedTypeScriptTypesChecking(forkedTypeScriptTypesCheckOptionsCallback = () => {}) {
if (this.useBabelTypeScriptPreset) {
throw new Error('Encore.enableForkedTypeScriptTypesChecking() can not be called when Encore.enableBabelTypeScriptPreset() has been called.');
}
if (typeof forkedTypeScriptTypesCheckOptionsCallback !== 'function') {
throw new Error('Argument 1 to enableForkedTypeScriptTypesChecking() must be a callback function.');
}
this.useForkedTypeScriptTypeChecking = true;
this.forkedTypeScriptTypesCheckOptionsCallback =
forkedTypeScriptTypesCheckOptionsCallback;
}
enableBabelTypeScriptPreset(options = {}) {
if (this.useTypeScriptLoader) {
throw new Error('Encore.enableBabelTypeScriptPreset() can not be called when Encore.enableTypeScriptLoader() has been called.');
}
if (this.useForkedTypeScriptTypeChecking) {
throw new Error('Encore.enableBabelTypeScriptPreset() can not be called when Encore.enableForkedTypeScriptTypesChecking() has been called.');
}
this.useBabelTypeScriptPreset = true;
this.babelTypeScriptPresetOptions = options;
}
enableVueLoader(vueLoaderOptionsCallback = () => {}, vueOptions = {}) {
this.useVueLoader = true;
if (typeof vueLoaderOptionsCallback !== 'function') {
throw new Error('Argument 1 to enableVueLoader() must be a callback function.');
}
this.vueLoaderOptionsCallback = vueLoaderOptionsCallback;
// Check allowed keys
for (const key of Object.keys(vueOptions)) {
if (!(key in this.vueOptions)) {
throw new Error(`"${key}" is not a valid key for enableVueLoader(). Valid keys: ${Object.keys(this.vueOptions).join(', ')}.`);
}
if (key === 'version') {
const validVersions = [2, 3];
if (!validVersions.includes(vueOptions.version)) {
throw new Error(`"${vueOptions.version}" is not a valid value for the "version" option passed to enableVueLoader(). Valid versions are: ${validVersions.join(', ')}.`);
}
}
this.vueOptions[key] = vueOptions[key];
}
// useJsx and vue 3 are not currently supported by Encore
if (this.vueOptions.useJsx && this.vueOptions.version === 3) {
throw new Error('Setting both "useJsx: true" and "version: 3" for enableVueLoader() is not currently supported. Please use version: 2 or disable useJsx.');
}
}
enableEslintPlugin(eslintPluginOptionsOrCallback = () => {}) {
this.useEslintPlugin = true;
if (typeof eslintPluginOptionsOrCallback === 'function') {
this.eslintPluginOptionsCallback = eslintPluginOptionsOrCallback;
} else if (typeof eslintPluginOptionsOrCallback === 'object') {
this.eslintPluginOptionsCallback = (options) => {
Object.assign(options, eslintPluginOptionsOrCallback);
};
} else {
throw new Error('Argument 1 to enableEslintPlugin() must be either an object or callback function.');
}
}
enableBuildNotifications(enabled = true, notifierPluginOptionsCallback = () => {}) {
if (typeof notifierPluginOptionsCallback !== 'function') {
throw new Error('Argument 2 to enableBuildNotifications() must be a callback function.');
}
this.useWebpackNotifier = enabled;
this.notifierPluginOptionsCallback = notifierPluginOptionsCallback;
}
enableHandlebarsLoader(callback = () => {}) {
this.useHandlebarsLoader = true;
if (typeof callback !== 'function') {
throw new Error('Argument 1 to enableHandlebarsLoader() must be a callback function.');
}
this.handlebarsConfigurationCallback = callback;
}
disableCssExtraction(disabled = true) {
this.extractCss = !disabled;
}
configureFilenames(configuredFilenames = {}) {
if (typeof configuredFilenames !== 'object') {
throw new Error('Argument 1 to configureFilenames() must be an object.');
}
// Check allowed keys
const validKeys = ['js', 'css', 'assets'];
for (const key of Object.keys(configuredFilenames)) {
if (!validKeys.includes(key)) {
throw new Error(`"${key}" is not a valid key for configureFilenames(). Valid keys: ${validKeys.join(', ')}. Use configureImageRule() or configureFontRule() to control image or font filenames.`);
}
}
this.configuredFilenames = configuredFilenames;
}
configureImageRule(options = {}, ruleCallback = () => {}) {
for (const optionKey of Object.keys(options)) {
if (!(optionKey in this.imageRuleOptions)) {
throw new Error(`Invalid option "${optionKey}" passed to configureImageRule(). Valid keys are ${Object.keys(this.imageRuleOptions).join(', ')}`);
}
this.imageRuleOptions[optionKey] = options[optionKey];
}
if (this.imageRuleOptions.maxSize && this.imageRuleOptions.type !== 'asset') {
throw new Error('Invalid option "maxSize" passed to configureImageRule(): this option is only valid when "type" is set to "asset".');
}
if (typeof ruleCallback !== 'function') {
throw new Error('Argument 2 to configureImageRule() must be a callback function.');
}
this.imageRuleCallback = ruleCallback;
}
configureFontRule(options = {}, ruleCallback = () => {}) {
for (const optionKey of Object.keys(options)) {
if (!(optionKey in this.fontRuleOptions)) {
throw new Error(`Invalid option "${optionKey}" passed to configureFontRule(). Valid keys are ${Object.keys(this.fontRuleOptions).join(', ')}`);
}
this.fontRuleOptions[optionKey] = options[optionKey];
}
if (this.fontRuleOptions.maxSize && this.fontRuleOptions.type !== 'asset') {
throw new Error('Invalid option "maxSize" passed to configureFontRule(): this option is only valid when "type" is set to "asset".');
}
if (typeof ruleCallback !== 'function') {
throw new Error('Argument 2 to configureFontRule() must be a callback function.');
}
this.fontRuleCallback = ruleCallback;
}
cleanupOutputBeforeBuild(paths = ['**/*'], cleanWebpackPluginOptionsCallback = () => {}) {
if (!Array.isArray(paths)) {
throw new Error('Argument 1 to cleanupOutputBeforeBuild() must be an Array of paths - e.g. [\'**/*\']');
}
if (typeof cleanWebpackPluginOptionsCallback !== 'function') {
throw new Error('Argument 2 to cleanupOutputBeforeBuild() must be a callback function');
}
this.cleanupOutput = true;
this.cleanWebpackPluginPaths = paths;
this.cleanWebpackPluginOptionsCallback = cleanWebpackPluginOptionsCallback;
}
autoProvideVariables(variables) {
// do a few sanity checks, so we can give better user errors
if (typeof variables === 'string' || Array.isArray(variables)) {
throw new Error('Invalid argument passed to autoProvideVariables: you must pass an object map - e.g. { $: "jquery" }');
}
// merge new variables into the object
this.providedVariables = Object.assign(
{},
this.providedVariables,
variables
);
}
autoProvidejQuery() {
this.autoProvideVariables({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery',
});
}
configureLoaderRule(name, callback) {
logger.warning('Be careful when using Encore.configureLoaderRule(), this is a low-level method that can potentially break Encore and Webpack when not used carefully.');
// Key: alias, Value: existing loader in `this.loaderConfigurationCallbacks`
const aliases = {
js: 'javascript',
ts: 'typescript',
scss: 'sass',
};
if (name in aliases) {
name = aliases[name];
}
if (!(name in this.loaderConfigurationCallbacks)) {
throw new Error(`Loader "${name}" is not configurable. Valid loaders are "${Object.keys(this.loaderConfigurationCallbacks).join('", "')}" and the aliases "${Object.keys(aliases).join('", "')}".`);
}
if (typeof callback !== 'function') {
throw new Error('Argument 2 to configureLoaderRule() must be a callback function.');
}
this.loaderConfigurationCallbacks[name] = callback;
}
enableIntegrityHashes(enabled = true, algorithms = ['sha384']) {
if (!Array.isArray(algorithms)) {
algorithms = [algorithms];
}
const availableHashes = crypto.getHashes();
for (const algorithm of algorithms) {
if (typeof algorithm !== 'string') {
throw new Error('Argument 2 to enableIntegrityHashes() must be a string or an array of strings.');
}
if (!availableHashes.includes(algorithm)) {
throw new Error(`Invalid hash algorithm "${algorithm}" passed to enableIntegrityHashes().`);
}
}
this.integrityAlgorithms = enabled ? algorithms : [];
}
useDevServer() {
return this.runtimeConfig.useDevServer;
}
isProduction() {
return this.runtimeConfig.environment === 'production';
}
isDev() {
return this.runtimeConfig.environment === 'dev';
}
isDevServer() {
return this.isDev() && this.runtimeConfig.useDevServer;
}
validateNameIsNewEntry(name) {
const entryNamesOverlapMsg = 'The entry names between addEntry(), addEntries(), and addStyleEntry() must be unique.';
if (this.entries.has(name)) {
throw new Error(`Duplicate name "${name}" already exists as an Entrypoint. ${entryNamesOverlapMsg}`);
}
if (this.styleEntries.has(name)) {
throw new Error(`The "${name}" already exists as a Style Entrypoint. ${entryNamesOverlapMsg}`);
}
}
}
module.exports = WebpackConfig;

View File

@@ -0,0 +1,641 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('./WebpackConfig'); //eslint-disable-line no-unused-vars
const cssExtractLoaderUtil = require('./loaders/css-extract');
const pathUtil = require('./config/path-util');
const featuresHelper = require('./features');
// loaders utils
const cssLoaderUtil = require('./loaders/css');
const sassLoaderUtil = require('./loaders/sass');
const lessLoaderUtil = require('./loaders/less');
const stylusLoaderUtil = require('./loaders/stylus');
const babelLoaderUtil = require('./loaders/babel');
const tsLoaderUtil = require('./loaders/typescript');
const vueLoaderUtil = require('./loaders/vue');
const handlebarsLoaderUtil = require('./loaders/handlebars');
// plugins utils
const miniCssExtractPluginUtil = require('./plugins/mini-css-extract');
const deleteUnusedEntriesPluginUtil = require('./plugins/delete-unused-entries');
const entryFilesManifestPlugin = require('./plugins/entry-files-manifest');
const manifestPluginUtil = require('./plugins/manifest');
const variableProviderPluginUtil = require('./plugins/variable-provider');
const cleanPluginUtil = require('./plugins/clean');
const definePluginUtil = require('./plugins/define');
const terserPluginUtil = require('./plugins/terser');
const optimizeCssAssetsUtil = require('./plugins/optimize-css-assets');
const vuePluginUtil = require('./plugins/vue');
const friendlyErrorPluginUtil = require('./plugins/friendly-errors');
const assetOutputDisplay = require('./plugins/asset-output-display');
const notifierPluginUtil = require('./plugins/notifier');
const eslintPluginUtil = require('./plugins/eslint');
const PluginPriorities = require('./plugins/plugin-priorities');
const applyOptionsCallback = require('./utils/apply-options-callback');
const copyEntryTmpName = require('./utils/copyEntryTmpName');
const getVueVersion = require('./utils/get-vue-version');
const tmp = require('tmp');
const fs = require('fs');
const path = require('path');
const stringEscaper = require('./utils/string-escaper');
const logger = require('./logger');
class ConfigGenerator {
/**
* @param {WebpackConfig} webpackConfig
*/
constructor(webpackConfig) {
this.webpackConfig = webpackConfig;
}
getWebpackConfig() {
const devServerConfig = this.webpackConfig.useDevServer() ? this.buildDevServerConfig() : null;
/*
* An unfortunate situation where we need to configure the final runtime
* config later in the process. The problem is that devServer https can
* be activated with either a --server-type=https flag or by setting the devServer.server.type='https'
* config to true. So, only at this moment can we determine
* if https has been activated by either method.
*/
if (this.webpackConfig.useDevServer() &&
(
devServerConfig.https
|| (devServerConfig.server && devServerConfig.server.type === 'https')
|| this.webpackConfig.runtimeConfig.devServerHttps
)) {
this.webpackConfig.runtimeConfig.devServerFinalIsHttps = true;
if (devServerConfig.https) {
logger.deprecation('The "https" option inside of configureDevServerOptions() is deprecated. Use "server = { type: \'https\' }" instead.');
}
} else {
this.webpackConfig.runtimeConfig.devServerFinalIsHttps = false;
}
const config = {
context: this.webpackConfig.getContext(),
entry: this.buildEntryConfig(),
mode: this.webpackConfig.isProduction() ? 'production' : 'development',
output: this.buildOutputConfig(),
module: {
rules: this.buildRulesConfig(),
},
plugins: this.buildPluginsConfig(),
optimization: this.buildOptimizationConfig(),
watchOptions: this.buildWatchOptionsConfig(),
devtool: false,
};
if (this.webpackConfig.usePersistentCache) {
config.cache = this.buildCacheConfig();
}
if (this.webpackConfig.useSourceMaps) {
if (this.webpackConfig.isProduction()) {
// https://webpack.js.org/configuration/devtool/#for-production
config.devtool = 'source-map';
} else {
// https://webpack.js.org/configuration/devtool/#for-development
config.devtool = 'inline-source-map';
}
}
if (null !== devServerConfig) {
config.devServer = devServerConfig;
}
config.performance = {
// silence performance hints
hints: false
};
config.stats = this.buildStatsConfig();
config.resolve = {
extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx', '.vue', '.ts', '.tsx', '.svelte'],
alias: {}
};
if (this.webpackConfig.useVueLoader && (this.webpackConfig.vueOptions.runtimeCompilerBuild === true || this.webpackConfig.vueOptions.runtimeCompilerBuild === null)) {
if (this.webpackConfig.vueOptions.runtimeCompilerBuild === null) {
logger.recommendation('To create a smaller (and CSP-compliant) build, see https://symfony.com/doc/current/frontend/encore/vuejs.html#runtime-compiler-build');
}
const vueVersion = getVueVersion(this.webpackConfig);
switch (vueVersion) {
case 2:
case '2.7':
config.resolve.alias['vue$'] = 'vue/dist/vue.esm.js';
break;
case 3:
config.resolve.alias['vue$'] = 'vue/dist/vue.esm-bundler.js';
break;
default:
throw new Error(`Invalid vue version ${vueVersion}`);
}
}
if (this.webpackConfig.usePreact && this.webpackConfig.preactOptions.preactCompat) {
config.resolve.alias['react'] = 'preact/compat';
config.resolve.alias['react-dom'] = 'preact/compat';
}
Object.assign(config.resolve.alias, this.webpackConfig.aliases);
config.externals = [...this.webpackConfig.externals];
return config;
}
buildEntryConfig() {
const entry = {};
for (const [entryName, entryChunks] of this.webpackConfig.entries) {
// entryFile could be an array, we don't care
entry[entryName] = entryChunks;
}
for (const [entryName, entryChunks] of this.webpackConfig.styleEntries) {
// entryFile could be an array, we don't care
entry[entryName] = entryChunks;
}
if (this.webpackConfig.copyFilesConfigs.length > 0) {
featuresHelper.ensurePackagesExistAndAreCorrectVersion('copy_files');
}
const copyFilesConfigs = this.webpackConfig.copyFilesConfigs.filter(entry => {
const copyFrom = path.resolve(
this.webpackConfig.getContext(),
entry.from
);
if (!fs.existsSync(copyFrom)) {
logger.warning(`The "from" option of copyFiles() should be set to an existing directory but "${entry.from}" does not seem to exist. Nothing will be copied for this copyFiles() config object.`);
return false;
}
if (!fs.lstatSync(copyFrom).isDirectory()) {
logger.warning(`The "from" option of copyFiles() should be set to an existing directory but "${entry.from}" seems to be a file. Nothing will be copied for this copyFiles() config object.`);
return false;
}
return true;
});
if (copyFilesConfigs.length > 0) {
const tmpFileObject = tmp.fileSync();
fs.writeFileSync(
tmpFileObject.name,
copyFilesConfigs.reduce((buffer, entry, index) => {
const copyFrom = path.resolve(
this.webpackConfig.getContext(),
entry.from
);
let copyTo = entry.to;
if (copyTo === null) {
copyTo = this.webpackConfig.useVersioning ? '[path][name].[hash:8].[ext]' : '[path][name].[ext]';
}
const copyFilesLoaderPath = require.resolve('./webpack/copy-files-loader');
const copyFilesLoaderConfig = `${copyFilesLoaderPath}?${JSON.stringify({
// file-loader options
context: entry.context ? path.resolve(this.webpackConfig.getContext(), entry.context) : copyFrom,
name: copyTo,
// custom copy-files-loader options
// the patternSource is base64 encoded in case
// it contains characters that don't work with
// the "inline loader" syntax
patternSource: Buffer.from(entry.pattern.source).toString('base64'),
patternFlags: entry.pattern.flags,
})}`;
return buffer + `
const context_${index} = require.context(
'${stringEscaper(`!!${copyFilesLoaderConfig}!${copyFrom}?copy-files-loader`)}',
${!!entry.includeSubdirectories},
${entry.pattern}
);
context_${index}.keys().forEach(context_${index});
`;
}, '')
);
entry[copyEntryTmpName] = tmpFileObject.name;
}
return entry;
}
buildOutputConfig() {
// Default filename can be overridden using Encore.configureFilenames({ js: '...' })
let filename = this.webpackConfig.useVersioning ? '[name].[contenthash:8].js' : '[name].js';
if (this.webpackConfig.configuredFilenames.js) {
filename = this.webpackConfig.configuredFilenames.js;
}
return {
path: this.webpackConfig.outputPath,
filename: filename,
// default "asset module" filename
// this is overridden for the image & font rules
assetModuleFilename: this.webpackConfig.configuredFilenames.assets ? this.webpackConfig.configuredFilenames.assets : 'assets/[name].[hash:8][ext]',
// will use the CDN path (if one is available) so that split
// chunks load internally through the CDN.
publicPath: this.webpackConfig.getRealPublicPath(),
pathinfo: !this.webpackConfig.isProduction()
};
}
buildRulesConfig() {
const applyRuleConfigurationCallback = (name, defaultRules) => {
return applyOptionsCallback(this.webpackConfig.loaderConfigurationCallbacks[name], defaultRules);
};
const generateAssetRuleConfig = (testRegex, ruleOptions, ruleCallback, ruleName) => {
const generatorOptions = {};
if (ruleOptions.filename) {
generatorOptions.filename = ruleOptions.filename;
}
const parserOptions = {};
if (ruleOptions.maxSize) {
parserOptions.dataUrlCondition = {
maxSize: ruleOptions.maxSize,
};
}
// apply callback from, for example, configureImageRule()
const ruleConfig = applyOptionsCallback(
ruleCallback,
{
test: testRegex,
oneOf: [
{
resourceQuery: /copy-files-loader/,
type: 'javascript/auto',
},{
type: ruleOptions.type,
generator: generatorOptions,
parser: parserOptions
}
]
},
);
// apply callback from lower-level configureLoaderRule()
return applyRuleConfigurationCallback(ruleName, ruleConfig);
};
// When the PostCSS loader is enabled, allow to use
// files with the `.postcss` extension. It also
// makes it possible to use `lang="postcss"` in Vue
// files.
const cssExtensions = ['css'];
if (this.webpackConfig.usePostCssLoader) {
cssExtensions.push('pcss');
cssExtensions.push('postcss');
}
let rules = [
applyRuleConfigurationCallback('javascript', {
test: babelLoaderUtil.getTest(this.webpackConfig),
exclude: this.webpackConfig.babelOptions.exclude,
use: babelLoaderUtil.getLoaders(this.webpackConfig)
}),
applyRuleConfigurationCallback('css', {
resolve: {
mainFields: ['style', 'main'],
extensions: cssExtensions.map(ext => `.${ext}`),
},
test: new RegExp(`\\.(${cssExtensions.join('|')})$`),
oneOf: [
{
resourceQuery: /module/,
use: cssExtractLoaderUtil.prependLoaders(
this.webpackConfig,
cssLoaderUtil.getLoaders(this.webpackConfig, true)
)
},
{
use: cssExtractLoaderUtil.prependLoaders(
this.webpackConfig,
cssLoaderUtil.getLoaders(this.webpackConfig)
)
}
]
})
];
if (this.webpackConfig.imageRuleOptions.enabled) {
rules.push(generateAssetRuleConfig(
/\.(png|jpg|jpeg|gif|ico|svg|webp|avif)$/,
this.webpackConfig.imageRuleOptions,
this.webpackConfig.imageRuleCallback,
'images'
));
}
if (this.webpackConfig.fontRuleOptions.enabled) {
rules.push(generateAssetRuleConfig(
/\.(woff|woff2|ttf|eot|otf)$/,
this.webpackConfig.fontRuleOptions,
this.webpackConfig.fontRuleCallback,
'fonts'
));
}
if (this.webpackConfig.useSassLoader) {
rules.push(applyRuleConfigurationCallback('sass', {
resolve: {
mainFields: ['sass', 'style', 'main'],
extensions: ['.scss', '.sass', '.css']
},
test: /\.s[ac]ss$/,
oneOf: [
{
resourceQuery: /module/,
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, sassLoaderUtil.getLoaders(this.webpackConfig, true))
},
{
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, sassLoaderUtil.getLoaders(this.webpackConfig))
}
]
}));
}
if (this.webpackConfig.useLessLoader) {
rules.push(applyRuleConfigurationCallback('less', {
test: /\.less/,
oneOf: [
{
resourceQuery: /module/,
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, lessLoaderUtil.getLoaders(this.webpackConfig, true))
},
{
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, lessLoaderUtil.getLoaders(this.webpackConfig))
}
]
}));
}
if (this.webpackConfig.useStylusLoader) {
rules.push(applyRuleConfigurationCallback('stylus', {
test: /\.styl/,
oneOf: [
{
resourceQuery: /module/,
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, stylusLoaderUtil.getLoaders(this.webpackConfig, true))
},
{
use: cssExtractLoaderUtil.prependLoaders(this.webpackConfig, stylusLoaderUtil.getLoaders(this.webpackConfig))
}
]
}));
}
if (this.webpackConfig.useSvelte) {
rules.push(applyRuleConfigurationCallback('svelte', {
resolve: {
mainFields: ['svelte', 'browser', 'module', 'main'],
extensions: ['.mjs', '.js', '.svelte'],
},
test: /\.svelte$/,
loader: 'svelte-loader',
}));
}
if (this.webpackConfig.useVueLoader) {
rules.push(applyRuleConfigurationCallback('vue', {
test: /\.vue$/,
use: vueLoaderUtil.getLoaders(this.webpackConfig)
}));
}
if (this.webpackConfig.useTypeScriptLoader) {
rules.push(applyRuleConfigurationCallback('typescript', {
test: /\.tsx?$/,
exclude: /node_modules/,
use: tsLoaderUtil.getLoaders(this.webpackConfig)
}));
}
if (this.webpackConfig.useHandlebarsLoader) {
rules.push(applyRuleConfigurationCallback('handlebars', {
test: /\.(handlebars|hbs)$/,
use: handlebarsLoaderUtil.getLoaders(this.webpackConfig)
}));
}
this.webpackConfig.loaders.forEach((loader) => {
rules.push(loader);
});
return rules;
}
buildPluginsConfig() {
const plugins = [];
miniCssExtractPluginUtil(plugins, this.webpackConfig);
// register the pure-style entries that should be deleted
deleteUnusedEntriesPluginUtil(plugins, this.webpackConfig);
entryFilesManifestPlugin(plugins, this.webpackConfig);
// Dump the manifest.json file
manifestPluginUtil(plugins, this.webpackConfig);
variableProviderPluginUtil(plugins, this.webpackConfig);
cleanPluginUtil(plugins, this.webpackConfig);
definePluginUtil(plugins, this.webpackConfig);
notifierPluginUtil(plugins, this.webpackConfig);
vuePluginUtil(plugins, this.webpackConfig);
eslintPluginUtil(plugins, this.webpackConfig);
if (!this.webpackConfig.runtimeConfig.outputJson) {
const friendlyErrorPlugin = friendlyErrorPluginUtil(this.webpackConfig);
plugins.push({
plugin: friendlyErrorPlugin,
priority: PluginPriorities.FriendlyErrorsWebpackPlugin
});
assetOutputDisplay(plugins, this.webpackConfig, friendlyErrorPlugin);
}
this.webpackConfig.plugins.forEach(function(plugin) {
plugins.push(plugin);
});
// Return sorted plugins
return plugins
.map((plugin, position) => Object.assign({}, plugin, { position: position }))
.sort((a, b) => {
// Keep the original order if two plugins have the same priority
if (a.priority === b.priority) {
return a.position - b.position;
}
// A plugin with a priority of -10 will be placed after one
// that has a priority of 0.
return b.priority - a.priority;
})
.map((plugin) => plugin.plugin);
}
buildOptimizationConfig() {
const optimization = {
};
if (this.webpackConfig.isProduction()) {
optimization.minimizer = [
terserPluginUtil(this.webpackConfig),
optimizeCssAssetsUtil(this.webpackConfig)
];
}
const splitChunks = {
chunks: this.webpackConfig.shouldSplitEntryChunks ? 'all' : 'async'
};
const cacheGroups = {};
for (const groupName in this.webpackConfig.cacheGroups) {
cacheGroups[groupName] = Object.assign(
{
name: groupName,
chunks: 'all',
enforce: true
},
this.webpackConfig.cacheGroups[groupName]
);
}
splitChunks.cacheGroups = cacheGroups;
if (this.webpackConfig.shouldUseSingleRuntimeChunk === null) {
throw new Error('Either the Encore.enableSingleRuntimeChunk() or Encore.disableSingleRuntimeChunk() method should be called. The recommended setting is Encore.enableSingleRuntimeChunk().');
}
if (this.webpackConfig.shouldUseSingleRuntimeChunk) {
optimization.runtimeChunk = 'single';
}
optimization.splitChunks = applyOptionsCallback(
this.webpackConfig.splitChunksConfigurationCallback,
splitChunks
);
return optimization;
}
buildCacheConfig() {
const cache = {};
cache.type = 'filesystem';
cache.buildDependencies = this.webpackConfig.persistentCacheBuildDependencies;
applyOptionsCallback(
this.webpackConfig.persistentCacheCallback,
cache
);
return cache;
}
buildStatsConfig() {
// try to silence as much as possible: the output is rarely helpful
// this still doesn't remove all output
let stats = {};
if (!this.webpackConfig.runtimeConfig.outputJson && !this.webpackConfig.runtimeConfig.profile) {
stats = {
hash: false,
version: false,
timings: false,
assets: false,
chunks: false,
modules: false,
reasons: false,
children: false,
source: false,
errors: false,
errorDetails: false,
warnings: false,
publicPath: false,
builtAt: false,
};
}
return stats;
}
buildWatchOptionsConfig() {
const watchOptions = {
ignored: /node_modules/
};
return applyOptionsCallback(
this.webpackConfig.watchOptionsConfigurationCallback,
watchOptions
);
}
buildDevServerConfig() {
const contentBase = pathUtil.getContentBase(this.webpackConfig);
const devServerOptions = {
static: {
directory: contentBase,
},
// avoid CORS concerns trying to load things like fonts from the dev server
headers: { 'Access-Control-Allow-Origin': '*' },
compress: true,
historyApiFallback: true,
// In webpack-dev-server v4 beta 0, liveReload always causes
// the page to refresh, not allowing HMR to update the page.
// This is somehow related to the "static" option, but it's
// unknown if there is a better option.
// See https://github.com/webpack/webpack-dev-server/issues/2893
liveReload: false,
// see https://github.com/symfony/webpack-encore/issues/931#issuecomment-784483725
host: this.webpackConfig.runtimeConfig.devServerHost,
// see https://github.com/symfony/webpack-encore/issues/941#issuecomment-787568811
// we cannot let webpack-dev-server find an open port, because we need
// to know the port for sure at Webpack config build time
port: this.webpackConfig.runtimeConfig.devServerPort,
};
return applyOptionsCallback(
this.webpackConfig.devServerOptionsConfigurationCallback,
devServerOptions
);
}
}
/**
* @param {WebpackConfig} webpackConfig A configured WebpackConfig object
*
* @return {*} The final webpack config object
*/
module.exports = function(webpackConfig) {
const generator = new ConfigGenerator(webpackConfig);
return generator.getWebpackConfig();
};

View File

@@ -0,0 +1,37 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
class RuntimeConfig {
constructor() {
this.command = null;
this.context = null;
this.isValidCommand = false;
this.environment = process.env.NODE_ENV ? process.env.NODE_ENV : 'dev';
this.useDevServer = false;
this.devServerHttps = null;
// see config-generator - getWebpackConfig()
this.devServerFinalIsHttps = null;
this.devServerHost = null;
this.devServerPort = null;
this.devServerPublic = null;
this.devServerKeepPublicPath = false;
this.outputJson = false;
this.profile = false;
this.babelRcFileExists = null;
this.helpRequested = false;
this.verbose = false;
}
}
module.exports = RuntimeConfig;

View File

@@ -0,0 +1,102 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const RuntimeConfig = require('./RuntimeConfig');
const pkgUp = require('pkg-up');
const path = require('path');
const babel = require('@babel/core');
/**
* @param {object} argv
* @param {String} cwd
* @returns {RuntimeConfig}
*/
module.exports = function(argv, cwd) {
const runtimeConfig = new RuntimeConfig();
runtimeConfig.command = argv._[0];
switch (runtimeConfig.command) {
case 'dev':
runtimeConfig.isValidCommand = true;
runtimeConfig.environment = 'dev';
runtimeConfig.verbose = true;
break;
case 'production':
case 'prod':
runtimeConfig.isValidCommand = true;
runtimeConfig.environment = 'production';
runtimeConfig.verbose = false;
break;
case 'dev-server':
runtimeConfig.isValidCommand = true;
runtimeConfig.environment = 'dev';
runtimeConfig.verbose = true;
runtimeConfig.useDevServer = true;
runtimeConfig.devServerKeepPublicPath = argv.keepPublicPath || false;
if (argv.https || argv.serverType === 'https') {
runtimeConfig.devServerHttps = true;
}
if (typeof argv.public === 'string') {
runtimeConfig.devServerPublic = argv.public;
}
runtimeConfig.devServerHost = argv.host ? argv.host : 'localhost';
runtimeConfig.devServerPort = argv.port ? argv.port : '8080';
break;
}
runtimeConfig.context = argv.context;
if (typeof runtimeConfig.context === 'undefined') {
const packagesPath = pkgUp.sync({ cwd });
if (null === packagesPath) {
throw new Error('Cannot determine webpack context. (Are you executing webpack from a directory outside of your project?). Try passing the --context option.');
}
runtimeConfig.context = path.dirname(packagesPath);
}
if (argv.h || argv.help) {
runtimeConfig.helpRequested = true;
}
if (argv.j || argv.json) {
runtimeConfig.outputJson = true;
}
if (argv.profile) {
runtimeConfig.profile = true;
}
const partialConfig = babel.loadPartialConfig({
/*
* There are two types of babel configuration:
* - project-wide configuration in babel.config.* files
* - file-relative configuration in .babelrc.* files
* or package.json files with a "babel" key
*
* To detect the file-relative configuration we need
* to set the following values. The filename is needed
* for Babel as an example so that it knows where it
* needs to search the relative config for.
*/
root: cwd,
cwd: cwd,
filename: path.join(cwd, 'webpack.config.js')
});
runtimeConfig.babelRcFileExists = partialConfig.hasFilesystemConfig();
return runtimeConfig;
};

View File

@@ -0,0 +1,144 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const path = require('path');
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const RuntimeConfig = require('./RuntimeConfig'); //eslint-disable-line no-unused-vars
const logger = require('../logger');
module.exports = {
/**
* Determines the "contentBase" to use for the devServer.
*
* @param {WebpackConfig} webpackConfig
* @return {String}
*/
getContentBase(webpackConfig) {
// strip trailing slash (for Unix or Windows)
const outputPath = webpackConfig.outputPath.replace(/\/$/,'').replace(/\\$/, '');
// use the manifestKeyPrefix if available
const publicPath = webpackConfig.manifestKeyPrefix ? webpackConfig.manifestKeyPrefix.replace(/\/$/,'') : webpackConfig.publicPath.replace(/\/$/,'');
/*
* We use the intersection of the publicPath (or manifestKeyPrefix) and outputPath
* to determine the "document root" of the web server. For example:
* * outputPath = /var/www/public/build
* * publicPath = /build/
* => contentBase should be /var/www/public
*
* At this point, if the publicPath is non-standard (e.g. it contains
* a sub-directory or is absolute), then the user will already see
* an error that they must set the manifestKeyPrefix.
*/
// start with outputPath, then join publicPath with it, see if it equals outputPath
// in loop, do dirname on outputPath and repeat
// eventually, you (may) get to the right path
let contentBase = outputPath;
while (path.dirname(contentBase) !== contentBase) {
if (path.join(contentBase, publicPath) === outputPath) {
return contentBase;
}
// go up one directory
contentBase = path.dirname(contentBase);
}
throw new Error(`Unable to determine contentBase option for webpack's devServer configuration. The ${webpackConfig.manifestKeyPrefix ? 'manifestKeyPrefix' : 'publicPath'} (${webpackConfig.manifestKeyPrefix ? webpackConfig.manifestKeyPrefix : webpackConfig.publicPath}) string does not exist in the outputPath (${webpackConfig.outputPath}), and so the "document root" cannot be determined.`);
},
/**
* Returns the output path, but as a relative string (e.g. web/build)
*
* @param {WebpackConfig} webpackConfig
* @return {String}
*/
getRelativeOutputPath(webpackConfig) {
return webpackConfig.outputPath.replace(webpackConfig.getContext() + path.sep, '');
},
/**
* If the manifestKeyPrefix is not set, this uses the publicPath to generate it.
*
* Most importantly, this runs some sanity checks to make sure that it's
* ok to use the publicPath as the manifestKeyPrefix.
*
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
validatePublicPathAndManifestKeyPrefix(webpackConfig) {
if (webpackConfig.manifestKeyPrefix !== null) {
// nothing to check - they have manually set the key prefix
return;
}
if (webpackConfig.publicPath.includes('://')) {
/*
* If publicPath is absolute, you probably don't want your manifests.json
* keys to be prefixed with the CDN URL. Instead, we force you to
* choose your manifestKeyPrefix.
*/
throw new Error('Cannot determine how to prefix the keys in manifest.json. Call Encore.setManifestKeyPrefix() to choose what path (e.g. build/) to use when building your manifest keys. This is happening because you passed an absolute URL to setPublicPath().');
}
let outputPath = webpackConfig.outputPath;
// for comparison purposes, change \ to / on Windows
outputPath = outputPath.replace(/\\/g, '/');
// remove trailing slash on each
outputPath = outputPath.replace(/\/$/, '');
const publicPath = webpackConfig.publicPath.replace(/\/$/, '');
/*
* This is a sanity check. If, for example, you are deploying
* to a subdirectory, then you might have something like this:
* outputPath = /var/www/public/build
* publicPath = /subdir/build/
*
* In that case, you probably don't want the keys in the manifest.json
* file to be prefixed with /subdir/build - it makes more sense
* to prefix them with /build, which is the true prefix relative
* to your application (the subdirectory is a deployment detail).
*
* For that reason, we force you to choose your manifestKeyPrefix().
*/
if (!outputPath.includes(publicPath)) {
const suggestion = publicPath.substr(publicPath.lastIndexOf('/') + 1) + '/';
throw new Error(`Cannot determine how to prefix the keys in manifest.json. Call Encore.setManifestKeyPrefix() to choose what path (e.g. ${suggestion}) to use when building your manifest keys. This is caused by setOutputPath() (${outputPath}) and setPublicPath() (${publicPath}) containing paths that don't seem compatible.`);
}
},
/**
* @param {RuntimeConfig} runtimeConfig
* @return {string|null|Object.public|*}
*/
calculateDevServerUrl(runtimeConfig) {
if (runtimeConfig.devServerFinalIsHttps === null) {
logger.warning('The final devServerFinalHttpsConfig was never calculated. This may cause some paths to incorrectly use or not use https and could be a bug.');
}
if (runtimeConfig.devServerPublic) {
if (runtimeConfig.devServerPublic.includes('://')) {
return runtimeConfig.devServerPublic;
}
if (runtimeConfig.devServerFinalIsHttps) {
return `https://${runtimeConfig.devServerPublic}`;
}
return `http://${runtimeConfig.devServerPublic}`;
}
return `http${runtimeConfig.devServerFinalIsHttps ? 's' : ''}://${runtimeConfig.devServerHost}:${runtimeConfig.devServerPort}`;
}
};

View File

@@ -0,0 +1,90 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const pathUtil = require('./path-util');
const logger = require('./../logger');
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
class Validator {
/**
* @param {WebpackConfig} webpackConfig
*/
constructor(webpackConfig) {
this.webpackConfig = webpackConfig;
}
validate() {
this._validateBasic();
this._validatePublicPathAndManifestKeyPrefix();
this._validateDevServer();
this._validateCacheGroupNames();
}
_validateBasic() {
if (this.webpackConfig.outputPath === null) {
throw new Error('Missing output path: Call setOutputPath() to control where the files will be written.');
}
if (this.webpackConfig.publicPath === null) {
throw new Error('Missing public path: Call setPublicPath() to control the public path relative to where the files are written (the output path).');
}
if (this.webpackConfig.entries.size === 0
&& this.webpackConfig.styleEntries.size === 0
&& this.webpackConfig.copyFilesConfigs.length === 0
&& this.webpackConfig.plugins.length === 0
) {
throw new Error('No entries found! You must call addEntry() or addEntries() or addStyleEntry() or copyFiles() or addPlugin() at least once - otherwise... there is nothing to webpack!');
}
}
_validatePublicPathAndManifestKeyPrefix() {
pathUtil.validatePublicPathAndManifestKeyPrefix(this.webpackConfig);
}
_validateDevServer() {
if (!this.webpackConfig.useDevServer()) {
return;
}
if (this.webpackConfig.useVersioning) {
throw new Error('Don\'t enable versioning with the dev-server. A good setting is Encore.enableVersioning(Encore.isProduction()).');
}
/*
* An absolute publicPath is incompatible with webpackDevServer.
* This is because we want to *change* the publicPath to point
* to the webpackDevServer URL (e.g. http://localhost:8080/).
* There are some valid use-cases for not wanting this behavior
* (see #59), but we want to warn the user.
*/
if (this.webpackConfig.publicPath.includes('://')) {
logger.warning(`Passing an absolute URL to setPublicPath() *and* using the dev-server can cause issues. Your assets will load from the publicPath (${this.webpackConfig.publicPath}) instead of from the dev server URL.`);
}
}
_validateCacheGroupNames() {
for (const groupName of Object.keys(this.webpackConfig.cacheGroups)) {
if (['defaultVendors', 'default'].includes(groupName)) {
logger.warning(`Passing "${groupName}" to addCacheGroup() is not recommended, as it will override the built-in cache group by this name.`);
}
}
}
}
module.exports = function(webpackConfig) {
const validator = new Validator(webpackConfig);
validator.validate();
};

View File

@@ -0,0 +1,18 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
/**
* Stores the current RuntimeConfig created by the encore executable.
*/
module.exports = {
runtimeConfig: null
};

View File

@@ -0,0 +1,209 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const packageHelper = require('./package-helper');
/**
* An object that holds internal configuration about different
* "loaders"/"plugins" that can be enabled/used.
*/
const features = {
sass: {
method: 'enableSassLoader()',
packages: [
{ name: 'sass-loader', enforce_version: true },
[{ name: 'sass' }, { name: 'sass-embedded' }, { name: 'node-sass' }]
],
description: 'load Sass files'
},
less: {
method: 'enableLessLoader()',
packages: [
{ name: 'less-loader', enforce_version: true },
],
description: 'load LESS files'
},
stylus: {
method: 'enableStylusLoader()',
packages: [
{ name: 'stylus-loader', enforce_version: true },
{ name: 'stylus' }
],
description: 'load Stylus files'
},
postcss: {
method: 'enablePostCssLoader()',
packages: [
{ name: 'postcss-loader', enforce_version: true }
],
description: 'process through PostCSS'
},
react: {
method: 'enableReactPreset()',
packages: [
{ name: '@babel/preset-react', enforce_version: true }
],
description: 'process React JS files'
},
preact: {
method: 'enablePreactPreset()',
packages: [
{ name: '@babel/plugin-transform-react-jsx', enforce_version: true }
],
description: 'process Preact JS files'
},
typescript: {
method: 'enableTypeScriptLoader()',
packages: [
{ name: 'typescript' },
{ name: 'ts-loader', enforce_version: true },
],
description: 'process TypeScript files'
},
forkedtypecheck: {
method: 'enableForkedTypeScriptTypesChecking()',
packages: [
{ name: 'typescript' },
{ name: 'ts-loader', enforce_version: true },
{ name: 'fork-ts-checker-webpack-plugin', enforce_version: true },
],
description: 'check TypeScript types in a separate process'
},
'typescript-babel': {
method: 'enableBabelTypeScriptPreset',
packages: [
{ name: 'typescript' },
{ name: '@babel/preset-typescript', enforce_version: true },
],
description: 'process TypeScript files with Babel'
},
vue2: {
method: 'enableVueLoader()',
// vue is needed so the end-user can do things
// vue-template-compiler is a peer dep of vue-loader
packages: [
{ name: 'vue', version: '^2.5' },
{ name: 'vue-loader', version: '^15.9.5' },
{ name: 'vue-template-compiler' }
],
description: 'load Vue files'
},
'vue2.7': {
method: 'enableVueLoader()',
// vue is needed so the end-user can do things
packages: [
{ name: 'vue', version: '^2.7' },
{ name: 'vue-loader', version: '^15.10.0' },
],
description: 'load Vue files'
},
vue3: {
method: 'enableVueLoader()',
// vue is needed so the end-user can do things
// @vue/compiler-sfc is an optional peer dep of vue-loader
packages: [
{ name: 'vue', enforce_version: true },
{ name: 'vue-loader', enforce_version: true },
{ name: '@vue/compiler-sfc' }
],
description: 'load Vue files'
},
'vue-jsx': {
method: 'enableVueLoader()',
packages: [
{ name: '@vue/babel-preset-jsx' },
{ name: '@vue/babel-helper-vue-jsx-merge-props' }
],
description: 'use Vue with JSX support'
},
eslint_plugin: {
method: 'enableEslintPlugin()',
// eslint is needed so the end-user can do things
packages: [
{ name: 'eslint' },
{ name: 'eslint-webpack-plugin', enforce_version: true },
],
description: 'Enable ESLint checks'
},
copy_files: {
method: 'copyFiles()',
packages: [
{ name: 'file-loader', enforce_version: true },
],
description: 'Copy files'
},
notifier: {
method: 'enableBuildNotifications()',
packages: [
{ name: 'webpack-notifier', enforce_version: true },
],
description: 'display build notifications'
},
handlebars: {
method: 'enableHandlebarsLoader()',
packages: [
{ name: 'handlebars' },
{ name: 'handlebars-loader', enforce_version: true }
],
description: 'load Handlebars files'
},
stimulus: {
method: 'enableStimulusBridge()',
packages: [
{ name: '@symfony/stimulus-bridge', enforce_version: true }
],
description: 'enable Stimulus bridge'
},
svelte: {
method: 'enableSvelte()',
packages: [
{ name: 'svelte', enforce_version: true },
{ name: 'svelte-loader', enforce_version: true }
],
description: 'process Svelte JS files'
}
};
function getFeatureConfig(featureName) {
if (!features[featureName]) {
throw new Error(`Unknown feature ${featureName}`);
}
return features[featureName];
}
module.exports = {
ensurePackagesExistAndAreCorrectVersion: function(featureName) {
const config = getFeatureConfig(featureName);
packageHelper.ensurePackagesExist(
packageHelper.addPackagesVersionConstraint(config.packages),
config.method
);
},
getMissingPackageRecommendations: function(featureName) {
const config = getFeatureConfig(featureName);
return packageHelper.getMissingPackageRecommendations(
packageHelper.addPackagesVersionConstraint(config.packages),
config.method
);
},
getFeatureMethod: function(featureName) {
return getFeatureConfig(featureName).method;
},
getFeatureDescription: function(featureName) {
return getFeatureConfig(featureName).description;
},
};

View File

@@ -0,0 +1,36 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const chalk = require('chalk');
function AssetOutputDisplayPlugin(outputPath, friendlyErrorsPlugin) {
this.outputPath = outputPath;
this.friendlyErrorsPlugin = friendlyErrorsPlugin;
}
AssetOutputDisplayPlugin.prototype.apply = function(compiler) {
const emit = (compilation, callback) => {
// completely reset messages key to avoid adding more and more messages
// when using watch
this.friendlyErrorsPlugin.compilationSuccessInfo.messages = [
`${chalk.yellow(Object.keys(compilation.assets).length)} files written to ${chalk.yellow(this.outputPath)}`
];
callback();
};
compiler.hooks.emit.tapAsync(
{ name: 'AssetOutputDisplayPlugin' },
emit
);
};
module.exports = AssetOutputDisplayPlugin;

View File

@@ -0,0 +1,39 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const chalk = require('chalk');
function formatErrors(errors) {
if (errors.length === 0) {
return [];
}
let messages = [];
messages.push(
chalk.red('Module build failed: Module not found:')
);
for (let error of errors) {
messages.push(`"${error.file}" contains a reference to the file "${error.ref}".`);
messages.push('This file can not be found, please check it for typos or update it if the file got moved.');
messages.push('');
}
return messages;
}
function format(errors) {
return formatErrors(errors.filter((e) => (
e.type === 'missing-css-file'
)));
}
module.exports = format;

View File

@@ -0,0 +1,73 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const chalk = require('chalk');
const loaderFeatures = require('../../features');
function formatErrors(errors) {
if (errors.length === 0) {
return [];
}
let messages = [];
for (let error of errors) {
const fixes = [];
if (error.loaderName) {
let neededCode = `Encore.${loaderFeatures.getFeatureMethod(error.loaderName)}`;
fixes.push(`Add ${chalk.green(neededCode)} to your webpack.config.js file.`);
const packageRecommendations = loaderFeatures.getMissingPackageRecommendations(error.loaderName);
if (packageRecommendations) {
fixes.push(`${packageRecommendations.message}\n ${packageRecommendations.installCommand}`);
}
} else {
fixes.push('You may need to install and configure a special loader for this file type.');
}
// vue hides their filenames (via a stacktrace) inside error.origin
if (error.isVueLoader) {
messages.push(error.message);
messages.push(error.origin);
messages.push('');
} else {
messages = messages.concat([
chalk.red(`Error loading ${chalk.yellow(error.file)}`),
''
]);
}
if (error.loaderName) {
messages.push(`${chalk.bgGreen.black('', 'FIX', '')} To ${loaderFeatures.getFeatureDescription(error.loaderName)}:`);
} else {
messages.push(`${chalk.bgGreen.black('', 'FIX', '')} To load "${error.file}":`);
}
let index = 0;
for (let fix of fixes) {
messages.push(` ${++index}. ${fix}`);
}
messages.push('');
}
return messages;
}
function format(errors) {
return formatErrors(errors.filter((e) => (
e.type === 'loader-not-enabled'
)));
}
module.exports = format;

View File

@@ -0,0 +1,51 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const chalk = require('chalk');
function formatErrors(errors) {
if (errors.length === 0) {
return [];
}
let messages = [];
// there will be an error for *every* file, but showing
// the error over and over again is not helpful
messages.push(
chalk.red('Module build failed: Error: No PostCSS Config found')
);
messages.push('');
messages.push(`${chalk.bgGreen.black('', 'FIX', '')} Create a ${chalk.yellow('postcss.config.js')} file at the root of your project.`);
messages.push('');
messages.push('Here is an example to get you started!');
messages.push(chalk.yellow(`
// postcss.config.js
module.exports = {
plugins: {
'autoprefixer': {},
}
}
`));
messages.push('');
messages.push('');
return messages;
}
function format(errors) {
return formatErrors(errors.filter((e) => (
e.type === 'missing-postcss-config'
)));
}
module.exports = format;

View File

@@ -0,0 +1,47 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const TYPE = 'missing-css-file';
function isMissingConfigError(e) {
if (e.name !== 'ModuleNotFoundError') {
return false;
}
if (!e.message.includes('Module not found: Error: Can\'t resolve')) {
return false;
}
return true;
}
function getReference(error) {
const index = error.message.indexOf('Can\'t resolve \'') + 15;
const endIndex = error.message.indexOf('\' in \'');
return error.message.substring(index, endIndex);
}
function transform(error) {
if (!isMissingConfigError(error)) {
return error;
}
error = Object.assign({}, error);
error.type = TYPE;
error.ref = getReference(error);
error.severity = 900;
return error;
}
module.exports = transform;

View File

@@ -0,0 +1,114 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const getVueVersion = require('../../utils/get-vue-version');
const TYPE = 'loader-not-enabled';
function isMissingLoaderError(e) {
if (e.name !== 'ModuleParseError') {
return false;
}
if (!/You may need an (appropriate|additional) loader/.test(e.message)) {
return false;
}
return true;
}
function isErrorFromVueLoader(filename) {
// vue2
if (filename.includes('??vue-loader-options')) {
return true;
}
// vue3
if (/vue-loader\/dist(\/index\.js)?\?\?/.test(filename)) {
return true;
}
// later vue3 variant
if (filename.includes('?vue') && filename.includes('lang=')) {
return true;
}
return false;
}
function getFileExtension(filename) {
// ??vue-loader-options
if (isErrorFromVueLoader(filename)) {
// vue is strange, the "filename" is reported as something like
// vue2: /path/to/project/node_modules/vue-loader/lib??vue-loader-options!./vuejs/App.vue?vue&type=style&index=1&lang=scss
// vue3: /path/to/project/node_modules/vue-loader/dist??ref--4-0!./vuejs/App.vue?vue&type=style&index=1&lang=scss
const langPos = filename.indexOf('lang=') + 5;
let endLangPos = filename.indexOf('&', langPos);
if (endLangPos === -1) {
endLangPos = filename.length;
}
return filename.substring(langPos, endLangPos);
}
const str = filename.replace(/\?.*/, '');
const split = str.split('.');
return split.pop();
}
function transform(error, webpackConfig) {
if (!isMissingLoaderError(error)) {
return error;
}
error = Object.assign({}, error);
error.isVueLoader = isErrorFromVueLoader(error.file);
const extension = getFileExtension(error.file);
switch (extension) {
case 'sass':
case 'scss':
error.loaderName = 'sass';
break;
case 'less':
error.loaderName = 'less';
break;
case 'jsx':
error.loaderName = 'react';
break;
case 'vue':
error.loaderName = 'vue' + getVueVersion(webpackConfig);
break;
case 'tsx':
case 'ts':
error.loaderName = 'typescript';
break;
// add more as needed
default:
return error;
}
error.type = TYPE;
error.severity = 900;
error.name = 'Loader not enabled';
return error;
}
/*
* Returns a factory to get the function.
*/
module.exports = function(webpackConfig) {
return function(error) {
return transform(error, webpackConfig);
};
};

View File

@@ -0,0 +1,35 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const TYPE = 'missing-postcss-config';
function isMissingConfigError(e) {
if (!e.message || !e.message.includes('No PostCSS Config found')) {
return false;
}
return true;
}
function transform(error) {
if (!isMissingConfigError(error)) {
return error;
}
error = Object.assign({}, error);
error.type = TYPE;
error.severity = 900;
return error;
}
module.exports = transform;

View File

@@ -0,0 +1,121 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const loaderFeatures = require('../features');
const applyOptionsCallback = require('../utils/apply-options-callback');
module.exports = {
/**
* @param {WebpackConfig} webpackConfig
* @return {Array} of loaders to use for Babel
*/
getLoaders(webpackConfig) {
let babelConfig = {
// improves performance by caching babel compiles
// this option is always added but is set to FALSE in
// production to avoid cache invalidation issues caused
// by some Babel presets/plugins (for instance the ones
// that use browserslist)
// https://github.com/babel/babel-loader#options
cacheDirectory: !webpackConfig.isProduction(),
// let Babel guess which kind of import/export syntax
// it should use based on the content of files
sourceType: 'unambiguous',
};
// configure babel (unless the user is specifying .babelrc)
// todo - add a sanity check for their babelrc contents
if (!webpackConfig.doesBabelRcFileExist()) {
let presetEnvOptions = {
// modules don't need to be transformed - webpack will parse
// the modules for us. This is a performance improvement
// https://babeljs.io/docs/en/babel-preset-env#modules
modules: false,
targets: {},
useBuiltIns: webpackConfig.babelOptions.useBuiltIns,
corejs: webpackConfig.babelOptions.corejs,
};
presetEnvOptions = applyOptionsCallback(
webpackConfig.babelPresetEnvOptionsCallback,
presetEnvOptions
);
Object.assign(babelConfig, {
presets: [
[require.resolve('@babel/preset-env'), presetEnvOptions]
],
plugins: []
});
if (webpackConfig.useBabelTypeScriptPreset) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('typescript-babel');
babelConfig.presets.push([require.resolve('@babel/preset-typescript'), webpackConfig.babelTypeScriptPresetOptions]);
}
if (webpackConfig.useReact) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('react');
babelConfig.presets.push(require.resolve('@babel/preset-react'));
}
if (webpackConfig.usePreact) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('preact');
if (webpackConfig.preactOptions.preactCompat) {
// If preact-compat is enabled tell babel to
// transform JSX into React.createElement calls.
babelConfig.plugins.push([require.resolve('@babel/plugin-transform-react-jsx')]);
} else {
// If preact-compat is disabled tell babel to
// transform JSX into Preact h() calls.
babelConfig.plugins.push([
require.resolve('@babel/plugin-transform-react-jsx'),
{ 'pragma': 'h' }
]);
}
}
if (webpackConfig.useVueLoader && webpackConfig.vueOptions.useJsx) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('vue-jsx');
babelConfig.presets.push(require.resolve('@vue/babel-preset-jsx'));
}
babelConfig = applyOptionsCallback(webpackConfig.babelConfigurationCallback, babelConfig);
}
return [
{
loader: require.resolve('babel-loader'),
options: babelConfig
}
];
},
/**
* @param {WebpackConfig} webpackConfig
* @return {RegExp} to use for eslint-loader `test` rule
*/
getTest(webpackConfig) {
const extensions = [
'm?jsx?', // match .js and .jsx and .mjs
];
if (webpackConfig.useBabelTypeScriptPreset) {
extensions.push('tsx?'); // match .ts and .tsx
}
return new RegExp(`\\.(${extensions.join('|')})$`);
}
};

View File

@@ -0,0 +1,46 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const applyOptionsCallback = require('../utils/apply-options-callback');
module.exports = {
/**
* Prepends loaders with MiniCssExtractPlugin.loader
*
* @param {WebpackConfig} webpackConfig
* @param {Array} loaders An array of some style loaders
* @return {Array}
*/
prependLoaders(webpackConfig, loaders) {
if (!webpackConfig.extractCss) {
const options = {};
// If the CSS extraction is disabled, use the
// style-loader instead.
return [{
loader: require.resolve('style-loader'),
options: applyOptionsCallback(webpackConfig.styleLoaderConfigurationCallback, options)
}, ...loaders];
}
return [{
loader: MiniCssExtractPlugin.loader,
options: applyOptionsCallback(
webpackConfig.miniCssExtractLoaderConfigurationCallback,
{}
),
}, ...loaders];
}
};

View File

@@ -0,0 +1,64 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const loaderFeatures = require('../features');
const applyOptionsCallback = require('../utils/apply-options-callback');
module.exports = {
/**
* @param {WebpackConfig} webpackConfig
* @param {boolean} useCssModules
* @return {Array} of loaders to use for CSS files
*/
getLoaders(webpackConfig, useCssModules = false) {
const usePostCssLoader = webpackConfig.usePostCssLoader;
let modulesConfig = false;
if (useCssModules) {
modulesConfig = {
localIdentName: '[local]_[hash:base64:5]',
};
}
const options = {
sourceMap: webpackConfig.useSourceMaps,
// when using @import, how many loaders *before* css-loader should
// be applied to those imports? This defaults to 0. When postcss-loader
// is used, we set it to 1, so that postcss-loader is applied
// to @import resources.
importLoaders: usePostCssLoader ? 1 : 0,
modules: modulesConfig
};
const cssLoaders = [
{
loader: require.resolve('css-loader'),
options: applyOptionsCallback(webpackConfig.cssLoaderConfigurationCallback, options)
},
];
if (usePostCssLoader) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('postcss');
const postCssLoaderOptions = {
sourceMap: webpackConfig.useSourceMaps
};
cssLoaders.push({
loader: require.resolve('postcss-loader'), //eslint-disable-line node/no-unpublished-require
options: applyOptionsCallback(webpackConfig.postCssLoaderOptionsCallback, postCssLoaderOptions)
});
}
return cssLoaders;
}
};

View File

@@ -0,0 +1,33 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const loaderFeatures = require('../features');
const applyOptionsCallback = require('../utils/apply-options-callback');
module.exports = {
/**
* @param {WebpackConfig} webpackConfig
* @return {Array} of loaders to use for Handlebars
*/
getLoaders(webpackConfig) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('handlebars');
const options = {};
return [
{
loader: require.resolve('handlebars-loader'), //eslint-disable-line node/no-unpublished-require
options: applyOptionsCallback(webpackConfig.handlebarsConfigurationCallback, options)
}
];
}
};

View File

@@ -0,0 +1,38 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const loaderFeatures = require('../features');
const cssLoader = require('./css');
const applyOptionsCallback = require('../utils/apply-options-callback');
module.exports = {
/**
* @param {WebpackConfig} webpackConfig
* @param {boolean} useCssModules
* @return {Array} of loaders to use for Less files
*/
getLoaders(webpackConfig, useCssModules = false) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('less');
const config = {
sourceMap: webpackConfig.useSourceMaps
};
return [
...cssLoader.getLoaders(webpackConfig, useCssModules),
{
loader: require.resolve('less-loader'), //eslint-disable-line node/no-unpublished-require
options: applyOptionsCallback(webpackConfig.lessLoaderOptionsCallback, config)
},
];
}
};

View File

@@ -0,0 +1,58 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const loaderFeatures = require('../features');
const cssLoader = require('./css');
const applyOptionsCallback = require('../utils/apply-options-callback');
module.exports = {
/**
* @param {WebpackConfig} webpackConfig
* @param {boolean} useCssModules
* @return {Array} of loaders to use for Sass files
*/
getLoaders(webpackConfig, useCssModules = false) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('sass');
const sassLoaders = [...cssLoader.getLoaders(webpackConfig, useCssModules)];
if (true === webpackConfig.sassOptions.resolveUrlLoader) {
// responsible for resolving Sass url() paths
// without this, all url() paths must be relative to the
// entry file, not the file that contains the url()
sassLoaders.push({
loader: require.resolve('resolve-url-loader'),
options: Object.assign(
{
sourceMap: webpackConfig.useSourceMaps
},
webpackConfig.sassOptions.resolveUrlLoaderOptions
)
});
}
const config = Object.assign({}, {
// needed by the resolve-url-loader
sourceMap: (true === webpackConfig.sassOptions.resolveUrlLoader) || webpackConfig.useSourceMaps,
sassOptions: {
// CSS minification is handled with mini-css-extract-plugin
outputStyle: 'expanded'
}
});
sassLoaders.push({
loader: require.resolve('sass-loader'), //eslint-disable-line node/no-unpublished-require
options: applyOptionsCallback(webpackConfig.sassLoaderOptionsCallback, config)
});
return sassLoaders;
}
};

View File

@@ -0,0 +1,38 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const loaderFeatures = require('../features');
const cssLoader = require('./css');
const applyOptionsCallback = require('../utils/apply-options-callback');
module.exports = {
/**
* @param {WebpackConfig} webpackConfig
* @param {boolean} useCssModules
* @return {Array} of loaders to use for Stylus files
*/
getLoaders(webpackConfig, useCssModules = false) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('stylus');
const config = {
sourceMap: webpackConfig.useSourceMaps
};
return [
...cssLoader.getLoaders(webpackConfig, useCssModules),
{
loader: require.resolve('stylus-loader'), //eslint-disable-line node/no-unpublished-require
options: applyOptionsCallback(webpackConfig.stylusLoaderOptionsCallback, config)
},
];
}
};

View File

@@ -0,0 +1,60 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const loaderFeatures = require('../features');
const babelLoader = require('./babel');
const applyOptionsCallback = require('../utils/apply-options-callback');
module.exports = {
/**
* @param {WebpackConfig} webpackConfig
* @return {Array} of loaders to use for TypeScript
*/
getLoaders(webpackConfig) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('typescript');
// some defaults
let config = {
silent: true,
};
// allow for ts-loader config to be controlled
config = applyOptionsCallback(webpackConfig.tsConfigurationCallback, config);
// fork-ts-checker-webpack-plugin integration
if (webpackConfig.useForkedTypeScriptTypeChecking) {
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('forkedtypecheck');
// force transpileOnly to speed up
config.transpileOnly = true;
// add forked ts types plugin to the stack
const forkedTypesPluginUtil = require('../plugins/forked-ts-types'); // eslint-disable-line
forkedTypesPluginUtil(webpackConfig);
}
// allow to import .vue files
if (webpackConfig.useVueLoader) {
config.appendTsSuffixTo = [/\.vue$/];
}
// use ts alongside with babel
// @see https://github.com/TypeStrong/ts-loader/blob/master/README.md#babel
let loaders = babelLoader.getLoaders(webpackConfig);
return loaders.concat([
{
loader: require.resolve('ts-loader'), //eslint-disable-line node/no-unpublished-require
// @see https://github.com/TypeStrong/ts-loader/blob/master/README.md#available-options
options: config
}
]);
}
};

View File

@@ -0,0 +1,35 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const loaderFeatures = require('../features');
const applyOptionsCallback = require('../utils/apply-options-callback');
const getVueVersion = require('../utils/get-vue-version');
module.exports = {
/**
* @param {WebpackConfig} webpackConfig
* @return {Array} of loaders to use for Vue files
*/
getLoaders(webpackConfig) {
const vueVersion = getVueVersion(webpackConfig);
loaderFeatures.ensurePackagesExistAndAreCorrectVersion('vue' + vueVersion);
const options = {};
return [
{
loader: require.resolve('vue-loader'), //eslint-disable-line node/no-unpublished-require
options: applyOptionsCallback(webpackConfig.vueLoaderOptionsCallback, options)
}
];
}
};

87
spa/node_modules/@symfony/webpack-encore/lib/logger.js generated vendored Normal file
View File

@@ -0,0 +1,87 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const chalk = require('chalk');
const messagesKeys = [
'debug',
'recommendation',
'warning',
'deprecation',
];
const defaultConfig = {
isVerbose: false,
quiet: false
};
let messages = {};
let config = {};
const reset = function() {
messages = {};
for (let messageKey of messagesKeys) {
messages[messageKey] = [];
}
config = Object.assign({}, defaultConfig);
};
reset();
function log(message) {
if (config.quiet) {
return;
}
console.log(message);
}
module.exports = {
debug(message) {
messages.debug.push(message);
if (config.isVerbose) {
log(`${chalk.bgBlack.white(' DEBUG ')} ${message}`);
}
},
recommendation(message) {
messages.recommendation.push(message);
log(`${chalk.bgBlue.white(' RECOMMEND ')} ${message}`);
},
warning(message) {
messages.warning.push(message);
log(`${chalk.bgYellow.black(' WARNING ')} ${chalk.yellow(message)}`);
},
deprecation(message) {
messages.deprecation.push(message);
log(`${chalk.bgYellow.black(' DEPRECATION ')} ${chalk.yellow(message)}`);
},
getMessages() {
return messages;
},
quiet(setQuiet = true) {
config.quiet = setQuiet;
},
verbose(setVerbose = true) {
config.isVerbose = setVerbose;
},
reset() {
reset();
}
};

View File

@@ -0,0 +1,211 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const chalk = require('chalk');
const fs = require('fs');
const logger = require('./logger');
const semver = require('semver');
function ensurePackagesExist(packagesConfig, requestedFeature) {
const missingPackagesRecommendation = getMissingPackageRecommendations(packagesConfig, requestedFeature);
if (missingPackagesRecommendation) {
throw `
${missingPackagesRecommendation.message}
${missingPackagesRecommendation.installCommand}
`;
}
// check for invalid versions & warn
const invalidVersionRecommendations = getInvalidPackageVersionRecommendations(packagesConfig);
for (let message of invalidVersionRecommendations) {
logger.warning(message);
}
}
function getInstallCommand(packageConfigs) {
const hasYarnLockfile = fs.existsSync('yarn.lock');
const hasNpmLockfile = fs.existsSync('package-lock.json');
const packageInstallStrings = packageConfigs.map((packageConfig) => {
const firstPackage = packageConfig[0];
if (typeof firstPackage.version === 'undefined') {
return firstPackage.name;
}
// e.g. ^4.0||^5.0: use the latest version
let recommendedVersion = firstPackage.version;
if (recommendedVersion.includes('||')) {
recommendedVersion = recommendedVersion.split('|').pop().trim();
}
// recommend the version included in our package.json file
return `${firstPackage.name}@${recommendedVersion}`;
});
if (hasNpmLockfile && !hasYarnLockfile) {
return chalk.yellow(`npm install ${packageInstallStrings.join(' ')} --save-dev`);
}
return chalk.yellow(`yarn add ${packageInstallStrings.join(' ')} --dev`);
}
function isPackageInstalled(packageConfig) {
try {
require.resolve(packageConfig.name);
return true;
} catch (e) {
return false;
}
}
/**
*
* @param {string} packageName
* @returns {null|string}
*/
function getPackageVersion(packageName) {
try {
return require(`${packageName}/package.json`).version;
} catch (e) {
return null;
}
}
function getMissingPackageRecommendations(packagesConfig, requestedFeature = null) {
let missingPackageConfigs = [];
for (let packageConfig of packagesConfig) {
if (!Array.isArray(packageConfig)) {
packageConfig = [packageConfig];
}
if (!packageConfig.some(isPackageInstalled)) {
missingPackageConfigs.push(packageConfig);
}
}
if (missingPackageConfigs.length === 0) {
return;
}
const missingPackageNamesChalked = missingPackageConfigs.map(function(packageConfigs) {
const packageNames = packageConfigs.map(packageConfig => {
return chalk.green(packageConfig.name);
});
let missingPackages = packageNames[0];
if (packageNames.length > 1) {
const alternativePackages = packageNames.slice(1);
missingPackages = `${missingPackages} (or ${alternativePackages.join(' or ')})`;
}
return missingPackages;
});
let message = `Install ${missingPackageNamesChalked.join(' & ')}`;
if (requestedFeature) {
message += ` to use ${chalk.green(requestedFeature)}`;
}
const installCommand = getInstallCommand(missingPackageConfigs);
return {
message,
installCommand
};
}
function getInvalidPackageVersionRecommendations(packagesConfig) {
const processPackagesConfig = (packageConfig) => {
if (Array.isArray(packageConfig)) {
let messages = [];
for (const config of packageConfig) {
messages = messages.concat(processPackagesConfig(config));
}
return messages;
}
if (typeof packageConfig.version === 'undefined') {
return [];
}
const version = getPackageVersion(packageConfig.name);
// If version is null at this point it should be because
// of an optional dependency whose presence has already
// been checked before.
if (version === null) {
return [];
}
if (semver.satisfies(version, packageConfig.version)) {
return [];
}
if (semver.gtr(version, packageConfig.version)) {
return [
`Webpack Encore requires version ${chalk.green(packageConfig.version)} of ${chalk.green(packageConfig.name)}. Your version ${chalk.green(version)} is too new. The related feature *may* still work properly. If you have issues, try downgrading the library, or upgrading Encore.`
];
} else {
return [
`Webpack Encore requires version ${chalk.green(packageConfig.version)} of ${chalk.green(packageConfig.name)}, but your version (${chalk.green(version)}) is too old. The related feature will probably *not* work correctly.`
];
}
};
return processPackagesConfig(packagesConfig);
}
function addPackagesVersionConstraint(packages) {
const packageJsonData = require('../package.json');
const addConstraint = (packageData) => {
if (Array.isArray(packageData)) {
return packageData.map(addConstraint);
}
const newData = Object.assign({}, packageData);
if (packageData.enforce_version) {
if (!packageJsonData.devDependencies) {
logger.warning('Could not find devDependencies key on @symfony/webpack-encore package');
return newData;
}
// this method only supports devDependencies due to how it's used:
// it's mean to inform the user what deps they need to install
// for optional features
if (!packageJsonData.devDependencies[packageData.name]) {
throw new Error(`Could not find package ${packageData.name}`);
}
newData.version = packageJsonData.devDependencies[packageData.name];
delete newData['enforce_version'];
}
return newData;
};
return packages.map(addConstraint);
}
module.exports = {
ensurePackagesExist,
getMissingPackageRecommendations,
getInvalidPackageVersionRecommendations,
addPackagesVersionConstraint,
getInstallCommand,
getPackageVersion,
};

View File

@@ -0,0 +1,36 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const FriendlyErrorsWebpackPlugin = require('@nuxt/friendly-errors-webpack-plugin'); //eslint-disable-line no-unused-vars
const pathUtil = require('../config/path-util');
const AssetOutputDisplayPlugin = require('../friendly-errors/asset-output-display-plugin');
const PluginPriorities = require('./plugin-priorities');
/**
* Updates plugins array passed adding AssetOutputDisplayPlugin instance
*
* @param {Array} plugins
* @param {WebpackConfig} webpackConfig
* @param {FriendlyErrorsWebpackPlugin} friendlyErrorsPlugin
* @return {void}
*/
module.exports = function(plugins, webpackConfig, friendlyErrorsPlugin) {
if (webpackConfig.useDevServer()) {
return;
}
const outputPath = pathUtil.getRelativeOutputPath(webpackConfig);
plugins.push({
plugin: new AssetOutputDisplayPlugin(outputPath, friendlyErrorsPlugin),
priority: PluginPriorities.AssetOutputDisplayPlugin
});
};

View File

@@ -0,0 +1,48 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const PluginPriorities = require('./plugin-priorities');
const applyOptionsCallback = require('../utils/apply-options-callback');
/**
* Updates plugins array passed adding CleanWebpackPlugin instance
*
* @param {Array} plugins to push to
* @param {WebpackConfig} webpackConfig read only variable
* @return {void}
*/
module.exports = function(plugins, webpackConfig) {
if (!webpackConfig.cleanupOutput) {
return;
}
const cleanOnceBeforeBuildPatterns = webpackConfig.cleanWebpackPluginPaths;
// works around a bug where manifest.json is emitted when
// using dev-server... but then CleanWebpackPlugin deletes it
cleanOnceBeforeBuildPatterns.push('!manifest.json');
const cleanWebpackPluginOptions = {
verbose: false,
cleanOnceBeforeBuildPatterns: webpackConfig.cleanWebpackPluginPaths,
// disabled to avoid a bug where some files were incorrectly deleted on watch rebuild
cleanStaleWebpackAssets: false
};
plugins.push({
plugin: new CleanWebpackPlugin(
applyOptionsCallback(webpackConfig.cleanWebpackPluginOptionsCallback, cleanWebpackPluginOptions)
),
priority: PluginPriorities.CleanWebpackPlugin
});
};

View File

@@ -0,0 +1,35 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const webpack = require('webpack');
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const PluginPriorities = require('./plugin-priorities');
const applyOptionsCallback = require('../utils/apply-options-callback');
/**
* @param {Array} plugins
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
module.exports = function(plugins, webpackConfig) {
const definePluginOptions = {
'process.env.NODE_ENV': webpackConfig.isProduction()
? '"production"'
: '"development"',
};
plugins.push({
plugin: new webpack.DefinePlugin(
applyOptionsCallback(webpackConfig.definePluginOptionsCallback, definePluginOptions)
),
priority: PluginPriorities.DefinePlugin
});
};

View File

@@ -0,0 +1,33 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const DeleteUnusedEntriesJSPlugin = require('../webpack/delete-unused-entries-js-plugin');
const PluginPriorities = require('./plugin-priorities');
const copyEntryTmpName = require('../utils/copyEntryTmpName');
/**
* @param {Array} plugins
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
module.exports = function(plugins, webpackConfig) {
const entries = [... webpackConfig.styleEntries.keys()];
if (webpackConfig.copyFilesConfigs.length > 0) {
entries.push(copyEntryTmpName);
}
plugins.push({
plugin: new DeleteUnusedEntriesJSPlugin(entries),
priority: PluginPriorities.DeleteUnusedEntriesJSPlugin
});
};

View File

@@ -0,0 +1,100 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const PluginPriorities = require('./plugin-priorities');
const copyEntryTmpName = require('../utils/copyEntryTmpName');
const AssetsPlugin = require('assets-webpack-plugin');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
function processOutput(webpackConfig) {
return (assets) => {
// Remove temporary entry added by the copyFiles feature
delete assets[copyEntryTmpName];
// with --watch or dev-server, subsequent calls will include
// the original assets (so, assets.entrypoints) + the new
// assets (which will have their original structure). We
// delete the entrypoints key, and then process the new assets
// like normal below. The same reasoning applies to the
// integrity key.
delete assets.entrypoints;
delete assets.integrity;
// This will iterate over all the entry points and convert the
// one file entries into an array of one entry since that was how the entry point file was before this change.
const integrity = {};
const integrityAlgorithms = webpackConfig.integrityAlgorithms;
const publicPath = webpackConfig.getRealPublicPath();
for (const asset in assets) {
for (const fileType in assets[asset]) {
if (!Array.isArray(assets[asset][fileType])) {
assets[asset][fileType] = [assets[asset][fileType]];
}
if (integrityAlgorithms.length) {
for (const file of assets[asset][fileType]) {
if (file in integrity) {
continue;
}
const filePath = path.resolve(
webpackConfig.outputPath,
file.replace(publicPath, '')
);
if (fs.existsSync(filePath)) {
const fileHashes = [];
for (const algorithm of webpackConfig.integrityAlgorithms) {
const hash = crypto.createHash(algorithm);
const fileContent = fs.readFileSync(filePath, 'utf8');
hash.update(fileContent, 'utf8');
fileHashes.push(`${algorithm}-${hash.digest('base64')}`);
}
integrity[file] = fileHashes.join(' ');
}
}
}
}
}
const manifestContent = { entrypoints: assets };
if (integrityAlgorithms.length) {
manifestContent.integrity = integrity;
}
return JSON.stringify(manifestContent, null, 2);
};
}
/**
* @param {Array} plugins
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
module.exports = function(plugins, webpackConfig) {
plugins.push({
plugin: new AssetsPlugin({
path: webpackConfig.outputPath,
filename: 'entrypoints.json',
includeAllFileTypes: true,
entrypoints: true,
processOutput: processOutput(webpackConfig)
}),
priority: PluginPriorities.AssetsPlugin
});
};

View File

@@ -0,0 +1,88 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const forceSync = require('sync-rpc');
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const applyOptionsCallback = require('../utils/apply-options-callback');
const pluginFeatures = require('../features');
const babelLoaderUtil = require('../loaders/babel');
/**
* Support for ESLint.
*
* @param {Array} plugins
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
module.exports = function(plugins, webpackConfig) {
if (webpackConfig.useEslintPlugin) {
const hasEslintConfiguration = forceSync(require.resolve('../utils/has-eslint-configuration'));
pluginFeatures.ensurePackagesExistAndAreCorrectVersion('eslint_plugin');
if (!hasEslintConfiguration(webpackConfig)) {
const chalk = require('chalk');
const packageHelper = require('../package-helper');
let message = `No ESLint configuration has been found.
${chalk.bgGreen.black('', 'FIX', '')} Run command ${chalk.yellow('./node_modules/.bin/eslint --init')} or manually create a ${chalk.yellow('.eslintrc.js')} file at the root of your project.
If you prefer to create a ${chalk.yellow('.eslintrc.js')} file by yourself, here is an example to get you started:
${chalk.yellow(`// .eslintrc.js
module.exports = {
parser: '@babel/eslint-parser',
extends: ['eslint:recommended'],
}
`)}
Install ${chalk.yellow('@babel/eslint-parser')} to prevent potential parsing issues: ${packageHelper.getInstallCommand([[{ name: '@babel/eslint-parser' }]])}
`;
if (!webpackConfig.doesBabelRcFileExist()) {
const babelConfig = babelLoaderUtil.getLoaders(webpackConfig)[0].options;
// cacheDirectory is a custom loader option, not a Babel option
delete babelConfig['cacheDirectory'];
message += `
You will also need to specify your Babel config in a separate file. The current
configuration Encore has been adding for your is:
${chalk.yellow(`// babel.config.js
module.exports = ${JSON.stringify(babelConfig, null, 4)}
`)}`;
if (webpackConfig.babelConfigurationCallback) {
message += `\nAdditionally, remove the ${chalk.yellow('.configureBabel()')} in webpack.config.js: this will no longer be used.`;
}
if (webpackConfig.babelPresetEnvOptionsCallback) {
message += `\nAnd remove the ${chalk.yellow('.configureBabelPresetEnv()')} in webpack.config.js: this will no longer be used.`;
}
}
throw new Error(message);
}
const eslintPluginOptions = {
emitWarning: true,
extensions: ['js', 'jsx'],
};
const EslintPlugin = require('eslint-webpack-plugin'); //eslint-disable-line node/no-unpublished-require
plugins.push({
plugin: new EslintPlugin(
applyOptionsCallback(webpackConfig.eslintPluginOptionsCallback, eslintPluginOptions)
),
});
}
};

View File

@@ -0,0 +1,30 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); // eslint-disable-line
const PluginPriorities = require('./plugin-priorities');
const applyOptionsCallback = require('../utils/apply-options-callback');
/**
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
module.exports = function(webpackConfig) {
const config = {};
webpackConfig.addPlugin(
new ForkTsCheckerWebpackPlugin(
applyOptionsCallback(webpackConfig.forkedTypeScriptTypesCheckOptionsCallback, config)
),
PluginPriorities.ForkTsCheckerWebpackPlugin
);
};

View File

@@ -0,0 +1,47 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const FriendlyErrorsWebpackPlugin = require('@nuxt/friendly-errors-webpack-plugin');
const missingCssFileTransformer = require('../friendly-errors/transformers/missing-css-file');
const missingCssFileFormatter = require('../friendly-errors/formatters/missing-css-file');
const missingLoaderTransformerFactory = require('../friendly-errors/transformers/missing-loader');
const missingLoaderFormatter = require('../friendly-errors/formatters/missing-loader');
const missingPostCssConfigTransformer = require('../friendly-errors/transformers/missing-postcss-config');
const missingPostCssConfigFormatter = require('../friendly-errors/formatters/missing-postcss-config');
const applyOptionsCallback = require('../utils/apply-options-callback');
/**
* @param {WebpackConfig} webpackConfig
* @return {FriendlyErrorsWebpackPlugin}
*/
module.exports = function(webpackConfig) {
const friendlyErrorsPluginOptions = {
clearConsole: false,
additionalTransformers: [
missingCssFileTransformer,
missingLoaderTransformerFactory(webpackConfig),
missingPostCssConfigTransformer
],
additionalFormatters: [
missingCssFileFormatter,
missingLoaderFormatter,
missingPostCssConfigFormatter
],
compilationSuccessInfo: {
messages: []
}
};
return new FriendlyErrorsWebpackPlugin(
applyOptionsCallback(webpackConfig.friendlyErrorsPluginOptionsCallback, friendlyErrorsPluginOptions)
);
};

View File

@@ -0,0 +1,61 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const { WebpackManifestPlugin } = require('../webpack-manifest-plugin');
const PluginPriorities = require('./plugin-priorities');
const applyOptionsCallback = require('../utils/apply-options-callback');
const copyEntryTmpName = require('../utils/copyEntryTmpName');
const manifestKeyPrefixHelper = require('../utils/manifest-key-prefix-helper');
/**
* @param {Array} plugins
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
module.exports = function(plugins, webpackConfig) {
let manifestPluginOptions = {
seed: {},
basePath: manifestKeyPrefixHelper(webpackConfig),
// always write a manifest.json file, even with webpack-dev-server
writeToFileEmit: true,
filter: (file) => {
const isCopyEntry = file.isChunk && copyEntryTmpName === file.chunk.id;
const isStyleEntry = file.isChunk && webpackConfig.styleEntries.has(file.chunk.name);
const isJsOrJsMapFile = /\.js(\.map)?$/.test(file.name);
return !isCopyEntry && !(isStyleEntry && isJsOrJsMapFile);
}
};
manifestPluginOptions = applyOptionsCallback(
webpackConfig.manifestPluginOptionsCallback,
manifestPluginOptions
);
const userMapOption = manifestPluginOptions.map;
manifestPluginOptions.map = (file) => {
const newFile = Object.assign({}, file, {
name: file.name.replace('?copy-files-loader', ''),
});
if (typeof userMapOption === 'function') {
return userMapOption(newFile);
}
return newFile;
};
plugins.push({
plugin: new WebpackManifestPlugin(manifestPluginOptions),
priority: PluginPriorities.WebpackManifestPlugin
});
};

View File

@@ -0,0 +1,62 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PluginPriorities = require('./plugin-priorities');
const applyOptionsCallback = require('../utils/apply-options-callback');
/**
* @param {Array} plugins
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
module.exports = function(plugins, webpackConfig) {
// Don't add the plugin if CSS extraction is disabled
if (!webpackConfig.extractCss) {
return;
}
// Default filename can be overridden using Encore.configureFilenames({ css: '...' })
let filename = webpackConfig.useVersioning ? '[name].[contenthash:8].css' : '[name].css';
// the chunk filename should use [id], not [name]. But, due
// to weird behavior (bug?) that's exposed in a functional test
// (in production mode, code is uglified), in some cases, an entry
// CSS file mysteriously becomes a chunk. In other words, it
// will have a filename like 1.css instead of entry_name.css
// This is related to setting optimization.runtimeChunk = 'single';
// See https://github.com/webpack/webpack/issues/6598
let chunkFilename = webpackConfig.useVersioning ? '[name].[contenthash:8].css' : '[name].css';
if (webpackConfig.configuredFilenames.css) {
filename = webpackConfig.configuredFilenames.css;
// see above: originally we did NOT set this, because this was
// only for split chunks. But now, sometimes the "entry" CSS chunk
// will use chunkFilename. So, we need to always respect the
// user's wishes
chunkFilename = webpackConfig.configuredFilenames.css;
}
const miniCssPluginOptions = {
filename: filename,
chunkFilename: chunkFilename
};
plugins.push({
plugin: new MiniCssExtractPlugin(
applyOptionsCallback(
webpackConfig.miniCssExtractPluginConfigurationCallback,
miniCssPluginOptions
)
),
priority: PluginPriorities.MiniCssExtractPlugin
});
};

View File

@@ -0,0 +1,40 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const pluginFeatures = require('../features');
const PluginPriorities = require('./plugin-priorities');
const applyOptionsCallback = require('../utils/apply-options-callback');
/**
* @param {Array} plugins
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
module.exports = function(plugins, webpackConfig) {
if (!webpackConfig.useWebpackNotifier) {
return;
}
pluginFeatures.ensurePackagesExistAndAreCorrectVersion('notifier');
const notifierPluginOptions = {
title: 'Webpack Encore'
};
const WebpackNotifier = require('webpack-notifier'); // eslint-disable-line
plugins.push({
plugin: new WebpackNotifier(
applyOptionsCallback(webpackConfig.notifierPluginOptionsCallback, notifierPluginOptions)
),
priority: PluginPriorities.WebpackNotifier
});
};

View File

@@ -0,0 +1,26 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const applyOptionsCallback = require('../utils/apply-options-callback');
/**
* @param {WebpackConfig} webpackConfig
* @return {object}
*/
module.exports = function(webpackConfig) {
const minimizerPluginOptions = {};
return new CssMinimizerPlugin(
applyOptionsCallback(webpackConfig.cssMinimizerPluginOptionsCallback, minimizerPluginOptions)
);
};

View File

@@ -0,0 +1,26 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
module.exports = {
MiniCssExtractPlugin: 140,
DeleteUnusedEntriesJSPlugin: 130,
WebpackManifestPlugin: 120,
LoaderOptionsPlugin: 110,
ProvidePlugin: 90,
CleanWebpackPlugin: 80,
DefinePlugin: 70,
WebpackNotifier: 60,
VueLoaderPlugin: 50,
FriendlyErrorsWebpackPlugin: 40,
AssetOutputDisplayPlugin: 30,
ForkTsCheckerWebpackPlugin: 10,
AssetsPlugin: -10,
};

View File

@@ -0,0 +1,28 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const TerserPlugin = require('terser-webpack-plugin');
const applyOptionsCallback = require('../utils/apply-options-callback');
/**
* @param {WebpackConfig} webpackConfig
* @return {object}
*/
module.exports = function(webpackConfig) {
const terserPluginOptions = {
parallel: true,
};
return new TerserPlugin(
applyOptionsCallback(webpackConfig.terserPluginOptionsCallback, terserPluginOptions)
);
};

View File

@@ -0,0 +1,28 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const webpack = require('webpack');
const PluginPriorities = require('./plugin-priorities');
/**
* @param {Array} plugins
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
module.exports = function(plugins, webpackConfig) {
if (Object.keys(webpackConfig.providedVariables).length > 0) {
plugins.push({
plugin: new webpack.ProvidePlugin(webpackConfig.providedVariables),
priority: PluginPriorities.ProvidePlugin
});
}
};

View File

@@ -0,0 +1,31 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const PluginPriorities = require('./plugin-priorities');
/**
* @param {Array} plugins
* @param {WebpackConfig} webpackConfig
* @return {void}
*/
module.exports = function(plugins, webpackConfig) {
if (!webpackConfig.useVueLoader) {
return;
}
const { VueLoaderPlugin } = require('vue-loader'); // eslint-disable-line node/no-unpublished-require
plugins.push({
plugin: new VueLoaderPlugin(),
priority: PluginPriorities.VueLoaderPlugin
});
};

View File

@@ -0,0 +1,20 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
module.exports = function(optionsCallback, options) {
const result = optionsCallback.call(options, options);
if (typeof result === 'object') {
return result;
}
return options;
};

View File

@@ -0,0 +1,12 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
module.exports = '_tmp_copy';

View File

@@ -0,0 +1,19 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const path = require('path');
const url = require('url');
module.exports = function(filename) {
const parsedFilename = new url.URL(filename, 'http://foo');
const extension = path.extname(parsedFilename.pathname);
return extension ? extension.slice(1) : '';
};

View File

@@ -0,0 +1,52 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
const packageHelper = require('../package-helper');
const semver = require('semver');
const logger = require('../logger');
/**
* @param {WebpackConfig} webpackConfig
* @return {int|string|null}
*/
module.exports = function(webpackConfig) {
if (webpackConfig.vueOptions.version !== null) {
return webpackConfig.vueOptions.version;
}
// detect installed version
const vueVersion = packageHelper.getPackageVersion('vue');
if (null === vueVersion) {
// 2 is the current default version to recommend
return 2;
}
if (semver.satisfies(vueVersion, '^2.7')) {
return '2.7';
}
if (semver.satisfies(vueVersion, '^2')) {
return 2;
}
if (semver.satisfies(vueVersion, '^3.0.0-beta.1')) {
return 3;
}
if (semver.satisfies(vueVersion, '^1')) {
throw new Error('vue version 1 is not supported.');
}
logger.warning(`Your version of vue "${vueVersion}" is newer than this version of Encore supports and may or may not function properly.`);
return 3;
};

View File

@@ -0,0 +1,43 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
function isMissingConfigError(e) {
if (!e.message || !e.message.includes('No ESLint configuration found')) {
return false;
}
return true;
}
/**
* @returns {Promise<boolean>}
*/
module.exports = async function() {
/**
* @param {WebpackConfig} webpackConfig
* @returns {Promise<boolean>}
*/
return async function(webpackConfig) {
const { ESLint } = require('eslint'); // eslint-disable-line node/no-unpublished-require
const eslint = new ESLint({
cwd: webpackConfig.runtimeConfig.context,
});
try {
await eslint.calculateConfigForFile('webpack.config.js');
} catch (e) {
return !isMissingConfigError(e);
}
return true;
};
};

View File

@@ -0,0 +1,32 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const WebpackConfig = require('../WebpackConfig'); //eslint-disable-line no-unused-vars
/**
* Helper for determining the manifest.json key prefix.
*
* @param {WebpackConfig} webpackConfig
* @return {string}
*/
module.exports = function(webpackConfig) {
let manifestPrefix = webpackConfig.manifestKeyPrefix;
if (null === manifestPrefix) {
if (null === webpackConfig.publicPath) {
throw new Error('publicPath is not set on WebpackConfig');
}
// by convention, we remove the opening slash on the manifest keys
manifestPrefix = webpackConfig.publicPath.replace(/^\//, '');
}
return manifestPrefix;
};

View File

@@ -0,0 +1,45 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const PrettyError = require('pretty-error');
/**
* Render a pretty version of the given error.
*
* Supported options:
* * {function} skipTrace
* An optional callback that defines whether
* or not each line of the eventual stacktrace
* should be kept. First argument is the content
* of the line, second argument is the line number.
*
* @param {*} error
* @param {object} options
*
* @returns {void}
*/
module.exports = function(error, options = {}) {
const pe = new PrettyError();
// Use the default terminal's color
// for the error message.
pe.appendStyle({
'pretty-error > header > message': { color: 'none' }
});
// Allow to skip some parts of the
// stacktrace if there is one.
if (options.skipTrace) {
pe.skip(options.skipTrace);
}
console.log(pe.render(error));
};

View File

@@ -0,0 +1,22 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
/**
* Function that escapes a string so it can be used in a RegExp.
*
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
*
* @param {string} str
* @return {string}
*/
module.exports = function regexpEscaper(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};

View File

@@ -0,0 +1,24 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
/**
* Function that escapes a string so it can be written into a
* file surrounded by single quotes.
*
* This is imperfect - is used to escape a filename (so, mostly,
* it needs to escape the Window path slashes).
*
* @param {string} str
* @return {string}
*/
module.exports = function stringEscaper(str) {
return str.replace(/\\/g, '\\\\').replace(/\x27/g, '\\\x27');
};

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Dane Thurber <dane.thurber@gmail.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.

View File

@@ -0,0 +1,6 @@
# webpack-manifest-plugin
This is a copy of https://github.com/shellscape/webpack-manifest-plugin
at sha: 9f408f609d9b1af255491036b6fc127777ee6f9a.
It has been modified to fix this bug: https://github.com/shellscape/webpack-manifest-plugin/pull/249

View File

@@ -0,0 +1,115 @@
const { dirname, join, basename } = require('path');
const generateManifest = (compilation, files, { generate, seed = {} }) => {
let result;
if (generate) {
const entrypointsArray = Array.from(compilation.entrypoints.entries());
const entrypoints = entrypointsArray.reduce(
(e, [name, entrypoint]) => Object.assign(e, { [name]: entrypoint.getFiles() }),
{}
);
result = generate(seed, files, entrypoints);
} else {
result = files.reduce(
(manifest, file) => Object.assign(manifest, { [file.name]: file.path }),
seed
);
}
return result;
};
const getFileType = (fileName, { transformExtensions }) => {
const replaced = fileName.replace(/\?.*/, '');
const split = replaced.split('.');
const extension = split.pop();
return transformExtensions.test(extension) ? `${split.pop()}.${extension}` : extension;
};
const reduceAssets = (files, asset, moduleAssets) => {
let name;
if (moduleAssets[asset.name]) {
name = moduleAssets[asset.name];
} else if (asset.info.sourceFilename) {
name = join(dirname(asset.name), basename(asset.info.sourceFilename));
}
if (name) {
return files.concat({
path: asset.name,
name,
isInitial: false,
isChunk: false,
isAsset: true,
isModuleAsset: true
});
}
const isEntryAsset = asset.chunks && asset.chunks.length > 0;
if (isEntryAsset) {
return files;
}
return files.concat({
path: asset.name,
name: asset.name,
isInitial: false,
isChunk: false,
isAsset: true,
isModuleAsset: false
});
};
const reduceChunk = (files, chunk, options, auxiliaryFiles) => {
// auxiliary files contain things like images, fonts AND, most
// importantly, other files like .map sourcemap files
// we modify the auxiliaryFiles so that we can add any of these
// to the manifest that was not added by another method
// (sourcemaps files are not added via any other method)
Array.from(chunk.auxiliaryFiles || []).forEach((auxiliaryFile) => {
auxiliaryFiles[auxiliaryFile] = {
path: auxiliaryFile,
name: basename(auxiliaryFile),
isInitial: false,
isChunk: false,
isAsset: true,
isModuleAsset: true
};
});
return Array.from(chunk.files).reduce((prev, path) => {
let name = chunk.name ? chunk.name : null;
// chunk name, or for nameless chunks, just map the files directly.
name = name
? options.useEntryKeys && !path.endsWith('.map')
? name
: `${name}.${getFileType(path, options)}`
: path;
return prev.concat({
path,
chunk,
name,
isInitial: chunk.isOnlyInitial(),
isChunk: true,
isAsset: false,
isModuleAsset: false
});
}, files);
};
const standardizeFilePaths = (file) => {
const result = Object.assign({}, file);
result.name = file.name.replace(/\\/g, '/');
result.path = file.path.replace(/\\/g, '/');
return result;
};
const transformFiles = (files, options) =>
['filter', 'map', 'sort']
.filter((fname) => !!options[fname])
// TODO: deprecate these
.reduce((prev, fname) => prev[fname](options[fname]), files)
.map(standardizeFilePaths);
module.exports = { generateManifest, reduceAssets, reduceChunk, transformFiles };

View File

@@ -0,0 +1,145 @@
const { mkdirSync, writeFileSync } = require('fs');
const { basename, dirname, join } = require('path');
const { SyncWaterfallHook } = require('tapable');
const webpack = require('webpack');
// eslint-disable-next-line global-require
const { RawSource } = webpack.sources || require('webpack-sources');
const { generateManifest, reduceAssets, reduceChunk, transformFiles } = require('./helpers');
const compilerHookMap = new WeakMap();
const getCompilerHooks = (compiler) => {
let hooks = compilerHookMap.get(compiler);
if (typeof hooks === 'undefined') {
hooks = {
afterEmit: new SyncWaterfallHook(['manifest']),
beforeEmit: new SyncWaterfallHook(['manifest'])
};
compilerHookMap.set(compiler, hooks);
}
return hooks;
};
const beforeRunHook = ({ emitCountMap, manifestFileName }, compiler, callback) => {
const emitCount = emitCountMap.get(manifestFileName) || 0;
emitCountMap.set(manifestFileName, emitCount + 1);
/* istanbul ignore next */
if (callback) {
callback();
}
};
const emitHook = function emit(
{ compiler, emitCountMap, manifestAssetId, manifestFileName, moduleAssets, options },
compilation
) {
const emitCount = emitCountMap.get(manifestFileName) - 1;
// Disable everything we don't use, add asset info, show cached assets
const stats = compilation.getStats().toJson({
all: false,
assets: true,
cachedAssets: true,
ids: true,
publicPath: true
});
const publicPath = options.publicPath !== null ? options.publicPath : stats.publicPath;
const { basePath, removeKeyHash } = options;
emitCountMap.set(manifestFileName, emitCount);
const auxiliaryFiles = {};
let files = Array.from(compilation.chunks).reduce(
(prev, chunk) => reduceChunk(prev, chunk, options, auxiliaryFiles),
[]
);
// module assets don't show up in assetsByChunkName, we're getting them this way
files = stats.assets.reduce((prev, asset) => reduceAssets(prev, asset, moduleAssets), files);
// don't add hot updates and don't add manifests from other instances
files = files.filter(
({ name, path }) =>
!path.includes('hot-update') &&
typeof emitCountMap.get(join(compiler.options.output.path, name)) === 'undefined'
);
// auxiliary files are "extra" files that are probably already included
// in other ways. Loop over files and remove any from auxiliaryFiles
files.forEach((file) => {
delete auxiliaryFiles[file.path];
});
// if there are any auxiliaryFiles left, add them to the files
// this handles, specifically, sourcemaps
Object.keys(auxiliaryFiles).forEach((auxiliaryFile) => {
files = files.concat(auxiliaryFiles[auxiliaryFile]);
});
files = files.map((file) => {
const changes = {
// Append optional basepath onto all references. This allows output path to be reflected in the manifest.
name: basePath ? basePath + file.name : file.name,
// Similar to basePath but only affects the value (e.g. how output.publicPath turns
// require('foo/bar') into '/public/foo/bar', see https://github.com/webpack/docs/wiki/configuration#outputpublicpath
path: publicPath ? publicPath + file.path : file.path
};
// Fixes #210
changes.name = removeKeyHash ? changes.name.replace(removeKeyHash, '') : changes.name;
return Object.assign(file, changes);
});
files = transformFiles(files, options);
let manifest = generateManifest(compilation, files, options);
const isLastEmit = emitCount === 0;
manifest = getCompilerHooks(compiler).beforeEmit.call(manifest);
if (isLastEmit) {
const output = options.serialize(manifest);
//
// Object.assign(compilation.assets, {
// [manifestAssetId]: {
// source() {
// return output;
// },
// size() {
// return output.length;
// }
// }
// });
//
compilation.emitAsset(manifestAssetId, new RawSource(output));
if (options.writeToFileEmit) {
mkdirSync(dirname(manifestFileName), { recursive: true });
writeFileSync(manifestFileName, output);
}
}
getCompilerHooks(compiler).afterEmit.call(manifest);
};
const normalModuleLoaderHook = ({ moduleAssets }, loaderContext, module) => {
const { emitFile } = loaderContext;
// eslint-disable-next-line no-param-reassign
loaderContext.emitFile = (file, content, sourceMap, assetInfo) => {
const info = Object.assign({}, assetInfo);
if (module.userRequest && !moduleAssets[file]) {
info.sourceFilename = join(dirname(file), basename(module.userRequest));
Object.assign(moduleAssets, { [file]: info.sourceFilename });
}
return emitFile.call(module, file, content, sourceMap, info);
};
};
module.exports = { beforeRunHook, emitHook, getCompilerHooks, normalModuleLoaderHook };

View File

@@ -0,0 +1,73 @@
const { relative, resolve } = require('path');
const webpack = require('webpack');
const NormalModule = require('webpack/lib/NormalModule');
const { beforeRunHook, emitHook, getCompilerHooks, normalModuleLoaderHook } = require('./hooks');
const emitCountMap = new Map();
const defaults = {
basePath: '',
fileName: 'manifest.json',
filter: null,
generate: void 0,
map: null,
publicPath: null,
removeKeyHash: /([a-f0-9]{32}\.?)/gi,
// seed must be reset for each compilation. let the code initialize it to {}
seed: void 0,
serialize(manifest) {
return JSON.stringify(manifest, null, 2);
},
sort: null,
transformExtensions: /^(gz|map)$/i,
useEntryKeys: false,
writeToFileEmit: false
};
class WebpackManifestPlugin {
constructor(opts) {
this.options = Object.assign({}, defaults, opts);
}
apply(compiler) {
const moduleAssets = {};
const manifestFileName = resolve(compiler.options.output.path, this.options.fileName);
const manifestAssetId = relative(compiler.options.output.path, manifestFileName);
const beforeRun = beforeRunHook.bind(this, { emitCountMap, manifestFileName });
const emit = emitHook.bind(this, {
compiler,
emitCountMap,
manifestAssetId,
manifestFileName,
moduleAssets,
options: this.options
});
const normalModuleLoader = normalModuleLoaderHook.bind(this, { moduleAssets });
const hookOptions = {
name: 'WebpackManifestPlugin',
stage: Infinity
};
compiler.hooks.compilation.tap(hookOptions, (compilation) => {
const hook = !NormalModule.getCompilationHooks
? compilation.hooks.normalModuleLoader
: NormalModule.getCompilationHooks(compilation).loader;
hook.tap(hookOptions, normalModuleLoader);
});
if (webpack.version.startsWith('4')) {
compiler.hooks.emit.tap(hookOptions, emit);
} else {
compiler.hooks.thisCompilation.tap(hookOptions, (compilation) => {
compilation.hooks.processAssets.tap(hookOptions, () => emit(compilation));
});
}
compiler.hooks.run.tap(hookOptions, beforeRun);
compiler.hooks.watchRun.tap(hookOptions, beforeRun);
}
}
module.exports = { getCompilerHooks, WebpackManifestPlugin };

View File

@@ -0,0 +1,91 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
const LoaderDependency = require('webpack/lib/dependencies/LoaderDependency');
const path = require('path');
module.exports.raw = true; // Needed to avoid corrupted binary files
module.exports.default = function loader(source) {
// This is a hack that allows `Encore.copyFiles()` to support
// JSON files using the file-loader (which is not something
// that is supported in Webpack 4, see https://github.com/symfony/webpack-encore/issues/535)
//
// Since there is no way to change the module's resource type from a loader
// without using private properties yet we have to use "this._module".
//
// By setting its type to 'javascript/auto' Webpack won't try parsing
// the result of the loader as a JSON object.
//
// For more information:
// https://github.com/webpack/webpack/issues/6572#issuecomment-368376326
// https://github.com/webpack/webpack/issues/7057
const requiredType = 'javascript/auto';
if (this._module.type !== requiredType) {
// Try to retrieve the factory used by the LoaderDependency type
// which should be the NormalModuleFactory.
const factory = this._compilation.dependencyFactories.get(LoaderDependency);
if (factory === undefined) {
throw new Error('Could not retrieve module factory for type LoaderDependency');
}
this._module.type = requiredType;
this._module.generator = factory.getGenerator(requiredType);
this._module.parser = factory.getParser(requiredType);
}
const options = this.getOptions();
// Retrieve the real path of the resource, relative
// to the context used by copyFiles(...)
const context = options.context;
const resourcePath = this.resourcePath;
const relativeResourcePath = path.relative(context, resourcePath);
// Retrieve the pattern used in copyFiles(...)
// The "source" part of the regexp is base64 encoded
// in case it contains characters that don't work with
// the "inline loader" syntax
const pattern = options.regExp || new RegExp(
Buffer.from(options.patternSource, 'base64').toString(),
options.patternFlags
);
// If the pattern does not match the ressource's path
// it probably means that the import was resolved using the
// "resolve.extensions" parameter of Webpack (for instance
// if "./test.js" was matched by "./test").
if (!pattern.test(relativeResourcePath)) {
return 'module.exports = "";';
}
// Add the "regExp" option (needed to use the [N] placeholder
// see: https://github.com/webpack-contrib/file-loader#n)
options.regExp = pattern;
// Remove copy-files-loader's custom options (in case the
// file-loader starts looking for thing it doesn't expect)
delete options.patternSource;
delete options.patternFlags;
// Update loader's options.
this.loaders.forEach(loader => {
if (__filename === loader.path) {
loader.options = options;
delete loader.query;
}
});
const fileLoader = require('file-loader'); // eslint-disable-line node/no-unpublished-require
// If everything is OK, let the file-loader do the copy
return fileLoader.bind(this)(source);
};

View File

@@ -0,0 +1,65 @@
/*
* This file is part of the Symfony Webpack Encore package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
function DeleteUnusedEntriesJSPlugin(entriesToDelete = []) {
this.entriesToDelete = entriesToDelete;
}
DeleteUnusedEntriesJSPlugin.prototype.apply = function(compiler) {
const deleteEntries = (compilation) => {
// loop over output chunks
compilation.chunks.forEach((chunk) => {
// see of this chunk is one that needs its .js deleted
if (this.entriesToDelete.includes(chunk.name)) {
const removedFiles = [];
// look for main files to delete first
for (const filename of Array.from(chunk.files)) {
if (/\.js?(\?[^.]*)?$/.test(filename)) {
removedFiles.push(filename);
// remove the output file
compilation.deleteAsset(filename);
// remove the file, so that it does not dump in the manifest
chunk.files.delete(filename);
}
}
// then also look in auxiliary files for source maps
for (const filename of Array.from(chunk.auxiliaryFiles)) {
if (removedFiles.map(name => `${name}.map`).includes(`${filename}`)) {
removedFiles.push(filename);
// remove the output file
compilation.deleteAsset(filename);
// remove the file, so that it does not dump in the manifest
chunk.auxiliaryFiles.delete(filename);
}
}
// sanity check: make sure 1 or 2 files were deleted
// if there's some edge case where more .js files
// or 0 .js files might be deleted, I'd rather error
if (removedFiles.length === 0 || removedFiles.length > 2) {
throw new Error(`Problem deleting JS entry for ${chunk.name}: ${removedFiles.length} files were deleted (${removedFiles.join(', ')})`);
}
}
});
};
compiler.hooks.compilation.tap('DeleteUnusedEntriesJSPlugin', function(compilation) {
compilation.hooks.additionalAssets.tap(
'DeleteUnusedEntriesJsPlugin',
function() {
deleteEntries(compilation);
}
);
});
};
module.exports = DeleteUnusedEntriesJSPlugin;