@enact/cli
Version:
Full-featured build environment tool for Enact applications.
676 lines (652 loc) • 24.7 kB
JavaScript
/* eslint-env node, es6 */
// @remove-on-eject-begin
/**
* Portions of this source code file are from create-react-app, used under the
* following MIT license:
*
* Copyright (c) 2013-present, Facebook, Inc.
* https://github.com/facebook/create-react-app
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// @remove-on-eject-end
const fs = require('fs');
const path = require('path');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
const ForkTsCheckerWebpackPlugin =
process.env.TSC_COMPILE_ON_ERROR === 'true'
? require('react-dev-utils/ForkTsCheckerWarningWebpackPlugin')
: require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
const resolve = require('resolve');
const TerserPlugin = require('terser-webpack-plugin');
const {DefinePlugin, EnvironmentPlugin} = require('webpack');
const {
optionParser: app,
cssModuleIdent: getLocalIdent,
GracefulFsPlugin,
ILibPlugin,
WebOSMetaPlugin
} = require('@enact/dev-utils');
const createEnvironmentHash = require('./createEnvironmentHash');
// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
module.exports = function (
env,
contentHash = false,
isomorphic = false,
noAnimation = false,
noSplitCSS = false,
ilibAdditionalResourcesPath
) {
process.chdir(app.context);
// Load applicable .env files into environment variables.
require('./dotenv').load(app.context);
// Sets the browserslist default fallback set of browsers to the Enact default browser support list.
app.setEnactTargetsAsDefault();
// Check if TypeScript is setup
const useTypeScript = fs.existsSync('tsconfig.json');
// Check if Tailwind config exists
const useTailwind = fs.existsSync(path.join(app.context, 'tailwind.config.js'));
process.env.NODE_ENV = env || process.env.NODE_ENV;
const isEnvProduction = process.env.NODE_ENV === 'production';
const publicPath = getPublicUrlOrPath(!isEnvProduction, app.publicUrl, process.env.PUBLIC_URL).replace(/^\/$/, '');
// Source maps are resource heavy and can cause out of memory issue for large source files.
// By default, sourcemaps will be used in development, however it can universally forced
// on or off by setting the GENERATE_SOURCEMAP environment variable.
const GENERATE_SOURCEMAP = process.env.GENERATE_SOURCEMAP || (isEnvProduction ? 'false' : 'true');
const shouldUseSourceMap = GENERATE_SOURCEMAP !== 'false';
// common function to get style loaders
const getStyleLoaders = (cssLoaderOptions = {}, preProcessor) => {
// Multiple styling-support features are used together, bottom-to-top.
// An optonal preprocessor, like "less loader", compiles LESS syntax into CSS.
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// `MiniCssExtractPlugin` takes the resulting CSS and puts it into an
// external file in our build process. If you use code splitting, any async
// bundles will stilluse the "style" loader inside the async code so CSS
// from them won't be in the main CSS file.
// When INLINE_STYLES env var is set, instead of MiniCssExtractPlugin, uses
// `style` loader to dynamically inline CSS in style tags at runtime.
const mergedCssLoaderOptions = {
...cssLoaderOptions,
modules: {
...cssLoaderOptions.modules,
// Options to restore 6.x behavior:
// https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md#700-2024-04-04
namedExport: false,
exportLocalsConvention: 'as-is'
}
};
const loaders = [
process.env.INLINE_STYLES ? require.resolve('style-loader') : MiniCssExtractPlugin.loader,
{
loader: require.resolve('css-loader'),
options: Object.assign({sourceMap: shouldUseSourceMap}, mergedCssLoaderOptions, {
url: {
filter: url => {
// Don't handle absolute path urls
if (url.startsWith('/')) {
return false;
}
return true;
}
}
})
},
{
// Options for PostCSS as we reference these options twice
// Adds vendor prefixing based on your specified browser support in
// package.json
loader: require.resolve('postcss-loader'),
options: {
postcssOptions: {
// Necessary for external CSS imports to work
// https://github.com/facebook/create-react-app/issues/2677
ident: 'postcss',
plugins: [
useTailwind && 'tailwindcss',
// Fix and adjust for known flexbox issues
// See https://github.com/philipwalton/flexbugs
'postcss-flexbugs-fixes',
// Transpile stage-3 CSS standards based on browserslist targets.
// See https://preset-env.cssdb.org/features for supported features.
// Includes support for targetted auto-prefixing.
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009',
remove: false
},
stage: 3,
features: {'custom-properties': false}
}
],
// Adds PostCSS Normalize to standardize browser quirks based on
// the browserslist targets.
!useTailwind && require('postcss-normalize'),
// Resolution indepedence support
app.ri !== false && require('postcss-resolution-independence')(app.ri),
// Support importing JSON files with ~ alias - custom plugin (must run first)
{
postcssPlugin: 'postcss-import-json-tilde',
Once(root) {
// Process all @import-json rules with ~ prefix first, before other plugins
root.walkAtRules('import-json', atRule => {
let src = atRule.params.slice(1, -1); // Remove quotes
// Only handle ~ alias paths
if (src.startsWith('~')) {
const packagePath = src.substring(1); // Remove ~
try {
// Use Node.js standard module resolution
// This mimics webpack's ~ alias behavior
const currentFileDir = path.dirname(atRule.source.input.file || '');
// Try to resolve the module using require.resolve
// This follows standard Node.js module resolution algorithm
let resolvedPath;
try {
// First try from current file's directory
resolvedPath = require.resolve(packagePath, {
paths: [currentFileDir]
});
} catch (e) {
// Fallback to current working directory
resolvedPath = require.resolve(packagePath, {
paths: [process.cwd()]
});
}
// Convert to relative path for the original plugin
const relativePath = path.relative(currentFileDir, resolvedPath);
atRule.params = `"${relativePath}"`;
} catch (error) {
// If resolution fails, try manual node_modules lookup
try {
let currentDir = path.dirname(
atRule.source.input.file || process.cwd()
);
let found = false;
// Walk up directories to find node_modules
while (currentDir !== path.parse(currentDir).root && !found) {
const moduleDir = path.join(
currentDir,
'node_modules',
packagePath
);
if (fs.existsSync(moduleDir)) {
const relativePath = path.relative(
path.dirname(atRule.source.input.file || ''),
moduleDir
);
atRule.params = `"${relativePath}"`;
found = true;
break;
}
currentDir = path.dirname(currentDir);
}
if (!found) {
console.warn(`Could not resolve module path: ${packagePath}`);
}
} catch (fallbackError) {
console.warn(
`Failed to resolve ${packagePath}:`,
fallbackError.message
);
}
}
}
});
}
},
// Support importing JSON files in CSS - original plugin (for non-~ paths)
[
'@daltontan/postcss-import-json',
{
map: (selector, value) => {
if (typeof value === 'object' && value !== null && value.$ref) {
const tokenPath = value.$ref.split('#/')[1];
const cssVariableName = '--' + tokenPath.replace(/\//g, '-');
return `var(${cssVariableName})`;
}
return value;
}
}
]
].filter(Boolean)
},
sourceMap: shouldUseSourceMap
}
}
];
if (preProcessor) {
loaders.push(preProcessor);
}
return loaders;
};
const getLessStyleLoaders = cssLoaderOptions =>
getStyleLoaders(cssLoaderOptions, {
loader: require.resolve('less-loader'),
options: {
lessOptions: {
modifyVars: Object.assign({__DEV__: !isEnvProduction}, app.accent)
},
sourceMap: shouldUseSourceMap
}
});
const getScssStyleLoaders = cssLoaderOptions =>
getStyleLoaders(cssLoaderOptions, {
loader: require.resolve('sass-loader'),
options: {
sourceMap: shouldUseSourceMap
}
});
const getAdditionalModulePaths = paths => {
if (!paths) return [];
return Array.isArray(paths) ? paths : [paths];
};
return {
mode: isEnvProduction ? 'production' : 'development',
// Don't attempt to continue if there are any errors.
bail: true,
// Webpack noise constrained to errors and warnings
stats: 'errors-warnings',
// Use source maps during development builds or when specified by GENERATE_SOURCEMAP
devtool: shouldUseSourceMap && (isEnvProduction ? 'source-map' : 'cheap-module-source-map'),
// These are the "entry points" to our application.
entry: {
main: [
// Include any polyfills needed for the target browsers.
require.resolve('./polyfills'),
// This is your app's code
app.context
]
},
output: {
// The build output directory.
path: path.resolve('./dist'),
// Generated JS file names (with nested folders).
// There will be one main bundle, and one file per asynchronous chunk.
// We don't currently advertise code splitting but Webpack supports it.
filename: contentHash ? '[name].[contenthash].js' : '[name].js',
// There are also additional JS chunk files if you use code splitting.
chunkFilename: contentHash ? 'chunk.[name].[contenthash].js' : 'chunk.[name].js',
assetModuleFilename: contentHash ? '[path][name][contenthash][ext]' : '[path][name][ext]',
// Add /* filename */ comments to generated require()s in the output.
pathinfo: !isEnvProduction,
publicPath,
// Improved sourcemap path name mapping for system filepaths
devtoolModuleFilenameTemplate: info => {
let file = isEnvProduction
? path.relative(app.context, info.absoluteResourcePath)
: path.resolve(info.absoluteResourcePath);
file = file.replace(/\\/g, '/').replace(/\.\./g, '_');
const loader = info.allLoaders.match(/[^\\/]+-loader/);
if (info.resource.includes('.less') && loader) {
// Temporary special handling for LESS files. The css-loader will
// output absolute-path mapped LESS sourcemaps, unaffected by this
// function, while both css-loader and style-loader pseudo modules
// will get their own sourcemaps. Good to differentiate.
return file + '?' + loader[0];
} else {
return file;
}
}
},
cache: {
type: 'filesystem',
version: createEnvironmentHash(Object.keys(process.env)),
cacheDirectory: path.resolve('./node_modules/.cache'),
store: 'pack',
buildDependencies: {
defaultWebpack: ['webpack/lib/'],
config: [__filename],
tsconfig: useTypeScript ? ['tsconfig.json'] : []
}
},
infrastructureLogging: {
level: 'none'
},
ignoreWarnings: [
// We ignore 'Module not found' warnings from SnapshotPlugin
{
module: /SnapshotPlugin/,
message: /Module not found/
}
],
resolve: {
// These are the reasonable defaults supported by the React/ES6 ecosystem.
extensions: ['.js', '.mjs', '.jsx', '.ts', '.tsx', '.json'].filter(
ext => useTypeScript || !ext.includes('ts')
),
// Allows us to specify paths to check for module resolving.
modules: [
path.resolve('./node_modules'),
'node_modules',
...getAdditionalModulePaths(app.additionalModulePaths)
],
// Don't resolve symlinks to their underlying paths
symlinks: false,
// Backward compatibility for apps using new ilib references with old Enact
// and old apps referencing old iLib location with new Enact
alias: fs.existsSync(path.join(app.context, 'node_modules', '@enact', 'i18n', 'ilib'))
? Object.assign({ilib: '@enact/i18n/ilib'}, app.alias)
: Object.assign({'@enact/i18n/ilib': 'ilib'}, app.alias),
// Optional configuration for redirecting module requests.
fallback: app.resolveFallback
},
// @remove-on-eject-begin
// Resolve loaders (webpack plugins for CSS, images, transpilation) from the
// directory of `@enact/cli` itself rather than the project directory.
resolveLoader: {
modules: [path.resolve(__dirname, '../node_modules'), path.resolve('./node_modules')]
},
// @remove-on-eject-end
module: {
rules: [
shouldUseSourceMap && {
enforce: 'pre',
exclude: /@babel(?:\/|\\{1,2})runtime/,
test: /\.(js|mjs|jsx|ts|tsx|css)$/,
loader: require.resolve('source-map-loader')
},
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
// back to the "file" loader at the end of the loader list.
oneOf: [
// Process JS with Babel.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
exclude: /node_modules.(?!@enact)/,
loader: require.resolve('babel-loader'),
options: {
configFile: path.join(__dirname, 'babel.config.js'),
babelrc: false,
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: !isEnvProduction,
cacheCompression: false,
compact: isEnvProduction
}
},
// Style-based rules support both LESS and CSS format, with *.module.* extension format
// to designate CSS modular support.
// See comments within `getStyleLoaders` for details on the stylesheet loader chains and
// options used at each level of processing.
{
test: /\.module\.css$/,
use: getStyleLoaders({
importLoaders: 1,
modules: {
...(isEnvProduction ? {} : {getLocalIdent})
}
})
},
{
test: /\.css$/,
// The `forceCSSModules` Enact build option can be set true to universally apply
// modular CSS support.
use: getStyleLoaders({
importLoaders: 1,
modules: {
...(app.forceCSSModules ? {} : {mode: 'icss'}),
...(!app.forceCSSModules && isEnvProduction ? {} : {getLocalIdent})
}
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true
},
{
test: /\.module\.less$/,
use: getLessStyleLoaders({
importLoaders: 2,
modules: {
...(isEnvProduction ? {} : {getLocalIdent})
}
})
},
{
test: /\.less$/,
use: getLessStyleLoaders({
importLoaders: 2,
modules: {
...(app.forceCSSModules ? {} : {mode: 'icss'}),
...(!app.forceCSSModules && isEnvProduction ? {} : {getLocalIdent})
}
}),
sideEffects: true
},
// Opt-in support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: /\.module\.(scss|sass)$/,
use: getScssStyleLoaders({
importLoaders: 3,
modules: {
...(isEnvProduction ? {} : {getLocalIdent})
}
})
},
// Opt-in support for SASS (using .scss or .sass extensions)
{
test: /\.(scss|sass)$/,
use: getScssStyleLoaders({
importLoaders: 3,
modules: {
...(app.forceCSSModules ? {} : {mode: 'icss'}),
...(!app.forceCSSModules && isEnvProduction ? {} : {getLocalIdent})
}
})
},
// "file" loader handles on all files not caught by the above loaders.
// When you `import` an asset, you get its output filename and the file
// is copied during the build process.
{
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
// Exclude `ejs` HTML templating language as that's handled by
// the HtmlWebpackPlugin.
exclude: [/^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.ejs$/, /\.json$/],
type: 'asset/resource'
}
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
]
}
].filter(Boolean)
},
// Target app to build for a specific environment (default 'browserslist')
target: app.environment,
// Optional configuration for polyfilling NodeJS built-ins.
node: app.nodeBuiltins,
performance: false,
optimization: {
minimize: isEnvProduction,
// These are only used in production mode
minimizer: [
new TerserPlugin({
terserOptions: {
parse: {
// we want uglify-js to parse ecma 8 code. However, we don't want it
// to apply any minfication steps that turns valid ecma 5 code
// into invalid ecma 5 code. This is why the 'compress' and 'output'
// sections only apply transformations that are ecma 5 safe
// https://github.com/facebook/create-react-app/pull/4234
ecma: 8
},
compress: {
ecma: 5,
warnings: false,
// Disabled because of an issue with Uglify breaking seemingly valid code:
// https://github.com/facebook/create-react-app/issues/2376
// Pending further investigation:
// https://github.com/mishoo/UglifyJS2/issues/2011
comparisons: false,
// Disabled because of an issue with Terser breaking valid code:
// https://github.com/facebook/create-react-app/issues/5250
// Pending futher investigation:
// https://github.com/terser-js/terser/issues/120
inline: 2
},
mangle: {
safari10: true
},
output: {
ecma: 5,
comments: false,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebook/create-react-app/issues/2488
ascii_only: true
}
},
// Use multi-process parallel running to improve the build speed
// Default number of concurrent runs: os.cpus().length - 1
parallel: true
}),
new CssMinimizerPlugin()
],
splitChunks: noSplitCSS && {
cacheGroups: {
styles: {
name: 'main',
type: 'css/mini-extract',
chunks: 'all',
enforce: true
}
}
}
},
plugins: [
// Generates an `index.html` file with the js and css tags injected.
new HtmlWebpackPlugin({
// Title can be specified in the package.json enact options or will
// be determined automatically from any appinfo.json files discovered.
title: app.title || '',
inject: 'body',
template: app.template || path.join(__dirname, 'html-template.ejs'),
xhtml: true,
minify: isEnvProduction && {
removeComments: true,
collapseWhitespace: false,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
}
}),
// Make NODE_ENV environment variable available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }.
// It is absolutely essential that NODE_ENV was set to production here.
// Otherwise React will be compiled in the very slow development mode.
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(isEnvProduction ? 'production' : 'development'),
'process.env.PUBLIC_URL': JSON.stringify(publicPath),
// Define ENACT_PACK_ISOMORPHIC global variable to determine to use
// `hydrateRoot` for isomorphic build and `createRoot` for non-isomorphic build by app.
ENACT_PACK_ISOMORPHIC: isomorphic,
// Define ENACT_PACK_NO_ANIMATION global variable to determine
// whether to build including effects such as animation or shadow or not.
ENACT_PACK_NO_ANIMATION: noAnimation
}),
// Inject prefixed environment variables within code, when used
new EnvironmentPlugin(Object.keys(process.env).filter(key => /^(REACT_APP|WDS_SOCKET)/.test(key))),
// Note: this won't work without MiniCssExtractPlugin.loader in `loaders`.
!process.env.INLINE_STYLES &&
new MiniCssExtractPlugin({
filename: contentHash ? '[name].[contenthash].css' : '[name].css',
chunkFilename: contentHash ? 'chunk.[name].[contenthash].css' : 'chunk.[name].css',
ignoreOrder: noSplitCSS
}),
// Webpack5 removed node polyfills but we need this to run screenshot tests
new NodePolyfillPlugin({
additionalAliases: ['console', 'domain', 'process', 'stream']
}),
// Provide meaningful information when modules are not found
new ModuleNotFoundPlugin(app.context),
// Ensure correct casing in module filepathes
new CaseSensitivePathsPlugin(),
// Switch the internal NodeOutputFilesystem to use graceful-fs to avoid
// EMFILE errors when hanndling mass amounts of files at once, such as
// what happens when using ilib bundles/resources.
new GracefulFsPlugin(),
// Automatically configure iLib library within @enact/i18n. Additionally,
// ensure the locale data files and the resource files are copied during
// the build to the output directory.
new ILibPlugin({publicPath, symlinks: false, ilibAdditionalResourcesPath}),
// Automatically detect ./appinfo.json and ./webos-meta/appinfo.json files,
// and parses any to copy over any webOS meta assets at build time.
new WebOSMetaPlugin({htmlPlugin: HtmlWebpackPlugin}),
// TypeScript type checking
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
async: !isEnvProduction,
typescript: {
typescriptPath: resolve.sync('typescript', {
basedir: 'node_modules'
}),
configOverwrite: {
compilerOptions: {
sourceMap: shouldUseSourceMap,
skipLibCheck: true,
inlineSourceMap: false,
declarationMap: false,
noEmit: true,
incremental: true,
tsBuildInfoFile: 'node_modules/.cache/tsconfig.tsbuildinfo'
}
},
context: app.context,
diagnosticOptions: {
syntactic: true
},
mode: 'write-references'
// profile: true,
},
issue: {
// prettier-ignore
include: [
{file: '../**/src/**/*.{ts,tsx}'},
{file: '**/src/**/*.{ts,tsx}'}
],
exclude: [
{file: '**/src/**/__tests__/**'},
{file: '**/src/**/?(*.){spec|test}.*'},
{file: '**/src/setupProxy.*'},
{file: '**/src/setupTests.*'}
]
},
logger: {
infrastructure: 'silent'
}
}),
new ESLintPlugin({
// Plugin options
configType: 'flat',
extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
formatter: require.resolve('react-dev-utils/eslintFormatter'),
eslintPath: require.resolve('eslint'),
// @remove-on-eject-begin
overrideConfigFile: require.resolve('./eslintWebpackPluginConfig'),
// @remove-on-eject-end
cache: true
})
].filter(Boolean)
};
};