@ima/cli
Version:
IMA.js CLI tool to build, develop and work with IMA.js applications.
603 lines (602 loc) • 27.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const cliUtils_1 = require("@ima/dev-utils/cliUtils");
const logger_1 = require("@ima/dev-utils/logger");
const react_refresh_webpack_plugin_1 = __importDefault(require("@pmmmwh/react-refresh-webpack-plugin"));
const compression_webpack_plugin_1 = __importDefault(require("compression-webpack-plugin"));
// eslint-disable-next-line import/default
const copy_webpack_plugin_1 = __importDefault(require("copy-webpack-plugin"));
const css_minimizer_webpack_plugin_1 = __importDefault(require("css-minimizer-webpack-plugin"));
const fork_ts_checker_webpack_plugin_1 = __importDefault(require("fork-ts-checker-webpack-plugin"));
const less_plugin_glob_1 = __importDefault(require("less-plugin-glob"));
const mini_css_extract_plugin_1 = __importDefault(require("mini-css-extract-plugin"));
const terser_webpack_plugin_1 = __importDefault(require("terser-webpack-plugin"));
const webpack_1 = require("webpack");
const languages_1 = require("./languages");
const GenerateRunnerPlugin_1 = require("./plugins/GenerateRunnerPlugin");
const ManifestPlugin_1 = require("./plugins/ManifestPlugin");
const ProgressPlugin_1 = require("./plugins/ProgressPlugin");
const utils_1 = require("./utils");
/**
* Creates Webpack configuration object based on input ConfigurationContext
* and ImaConfig objects.
*
* @returns {Promise<Configuration>} Webpack configuration object.
*/
exports.default = async (ctx, imaConfig) => {
const { rootDir, isServer, isClientES, isClient, name, processCss, outputFolders, typescript, imaEnvironment, appDir, useHMR, mode, lessGlobalsPath, useSourceMaps, isDevEnv, devtool, targets, } = ctx;
// Define helper variables derived from context
const isDebug = imaEnvironment.$Debug;
const devServerConfig = (0, utils_1.createDevServerConfig)({ imaConfig, ctx });
// Bundle entries
const publicPathEntry = path_1.default.join(__dirname, './entries/publicPathEntry');
const appMainEntry = path_1.default.join(rootDir, 'app/main.js');
// Define browserslist targets for current context
const coreJsVersion = await (0, utils_1.getCurrentCoreJsVersion)();
/**
* Generates SWC loader for js and ts files
*/
const getSwcLoader = async (syntax) => {
return imaConfig.swc({
// We use core-js only for lower ES version build
...(isClient && {
env: {
targets,
mode: 'usage',
coreJs: coreJsVersion,
bugfixes: true,
dynamicImport: true,
},
}),
isModule: true,
module: {
type: 'es6',
},
jsc: {
...(isClient ? {} : { target: 'es2022' }),
parser: {
syntax: syntax ?? 'ecmascript',
decorators: false,
dynamicImport: true,
[syntax === 'typescript' ? 'tsx' : 'jsx']: true,
},
transform: {
react: {
runtime: imaConfig.jsxRuntime ?? 'automatic',
refresh: useHMR && ctx.reactRefresh,
useBuiltins: true,
},
},
},
sourceMaps: useSourceMaps,
inlineSourcesContent: useSourceMaps,
}, ctx);
};
/**
* CSS loaders function generator. Contains postcss-loader
* and optional less loaders.
*/
const getStyleLoaders = async (useCssModules = false) => {
/**
* Return null-loader in contexts that don't process styles while
* not using css-modules, since we don't need to compile the styles at all.
* This improves build performance significantly in applications with
* large amounts of style files.
*/
if (!useCssModules && !processCss) {
return [{ loader: 'null-loader' }];
}
return [
...(!imaConfig.experiments?.css
? [
processCss && {
loader: mini_css_extract_plugin_1.default.loader,
},
{
loader: require.resolve('css-loader'),
options: {
...(useCssModules && {
modules: {
exportOnlyLocals: !processCss,
localIdentName: isDevEnv
? '[path][name]__[local]--[hash:base64:5]'
: '[hash:base64]',
},
}),
sourceMap: useSourceMaps,
},
},
]
: []),
{
loader: require.resolve('postcss-loader'),
options: await imaConfig.postcss({
postcssOptions: {
config: false,
plugins: [
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
browsers: imaConfig.cssBrowsersTarget,
autoprefixer: {
flexbox: 'no-2009',
grid: 'autoplace',
},
stage: 1,
},
],
],
},
implementation: require('postcss'),
sourceMap: useSourceMaps,
}, ctx),
},
{
loader: require.resolve('less-loader'),
options: {
webpackImporter: false,
sourceMap: useSourceMaps,
implementation: require('less'),
additionalData: fs_1.default.existsSync(lessGlobalsPath)
? `@import "${lessGlobalsPath}";\n\n`
: '',
lessOptions: {
plugins: [less_plugin_glob_1.default],
paths: [
path_1.default.resolve(rootDir),
path_1.default.resolve(rootDir, 'node_modules'),
],
},
},
},
].filter(Boolean);
};
return {
name,
dependencies: [],
target: isServer
? 'node18'
: isClientES
? ['web', 'es2022']
: ['web', 'es2018'],
mode,
devtool: useHMR
? 'cheap-module-source-map' // Needed for proper source maps parsing in error-overlay
: devtool,
entry: {
...(isServer
? {
server: [publicPathEntry, appMainEntry],
}
: {
[name]: [
publicPathEntry,
useHMR &&
isDebug &&
`${require.resolve('@ima/hmr-client')}?${new URLSearchParams({
name,
noInfo: 'false',
reload: 'true',
timeout: '3000',
reactRefresh: ctx.reactRefresh ? 'true' : 'false',
port: devServerConfig.port.toString(),
hostname: devServerConfig.hostname,
publicUrl: devServerConfig.publicUrl,
}).toString()}`,
appMainEntry,
].filter(Boolean),
...(0, utils_1.createPolyfillEntry)(ctx),
}),
...(0, languages_1.getLanguageEntryPoints)(imaConfig.languages, rootDir, useHMR),
},
output: {
path: path_1.default.join(rootDir, 'build'),
pathinfo: isDevEnv,
hashFunction: 'xxhash64',
assetModuleFilename: `${outputFolders.media}/[name].[hash][ext]`,
filename: ({ chunk }) => {
const fileNameParts = [
chunk?.name === name
? isServer
? 'app.server'
: isDevEnv
? 'app.client'
: 'app.bundle'
: '[name]',
'[contenthash]',
'js',
].filter(Boolean);
return `${outputFolders.js}/${fileNameParts.join('.')}`;
},
chunkFilename: () => `${outputFolders.js}/chunk.[id].[contenthash].js`,
cssFilename: ({ chunk }) => `${outputFolders.css}/${chunk?.name === name ? 'app' : '[name]'}${ctx.command === 'dev' ? '' : '.[contenthash]'}.css`,
cssChunkFilename: `${outputFolders.css}/chunk.[id]${ctx.command === 'dev' ? '' : '.[contenthash]'}.css`,
publicPath: imaConfig.publicPath,
/**
* We put hot updates into it's own folder
* otherwise it clutters the build folder.
*/
hotUpdateChunkFilename: `${outputFolders.hot}/[id].[fullhash].hot-update.js`,
hotUpdateMainFilename: `${outputFolders.hot}/[runtime].[fullhash].hot-update.json`,
...(isServer && { library: { type: 'commonjs2' } }),
},
cache: {
type: 'filesystem',
name: `${name}-${ctx.command}-${mode}`,
version: (0, utils_1.createCacheKey)(ctx, imaConfig, {
...devServerConfig,
$Debug: isDebug,
coreJsVersion: 'core-js',
devtool,
}),
store: 'pack',
hashAlgorithm: 'xxhash64',
memoryCacheUnaffected: true,
buildDependencies: {
imaCli: [require.resolve('@ima/cli')],
imaConfig: [path_1.default.join(rootDir, utils_1.IMA_CONF_FILENAME)],
defaultConfig: [__filename],
},
},
optimization: {
minimize: ctx.command === 'build' && !isServer,
minimizer: [
new terser_webpack_plugin_1.default({
minify: terser_webpack_plugin_1.default.swcMinify,
terserOptions: {
ecma: isServer || isClientES ? 2020 : 2018,
module: true,
mangle: {
// Added for profiling in devtools
keep_classnames: ctx.profile || isDevEnv,
keep_fnames: ctx.profile || isDevEnv,
},
},
}),
new css_minimizer_webpack_plugin_1.default(),
],
moduleIds: 'named',
chunkIds: 'named',
...(!isServer && { runtimeChunk: 'single' }),
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/](.(?!.*\.(less|css)$))*$/,
name: 'vendors',
enforce: isDevEnv,
chunks: isDevEnv ? 'initial' : 'async',
reuseExistingChunk: true,
},
default: {
chunks: 'async',
minChunks: 2,
reuseExistingChunk: true,
},
},
},
},
resolve: {
extensions: ['.mjs', '.ts', '.tsx', '.js', '.jsx', '.json'],
mainFields: isServer ? ['module', 'main'] : ['browser', 'module', 'main'],
alias: {
// App specific aliases
app: path_1.default.join(rootDir, 'app'),
// Enable better profiling in react devtools
...(ctx.profile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
// Ima config overrides
...(imaConfig.webpackAliases ?? {}),
},
},
resolveLoader: {
modules: [path_1.default.resolve(__dirname, 'loaders'), 'node_modules'],
},
module: {
rules: [
{
/**
* This will traverse following loaders until a match is found.
* If no matches are found, it falls back to resource loader
* (much like create-react-app does this).
*/
oneOf: [
/**
* Image loaders, which either load explicitly image as inline
* or external, or choose which mode to use automatically based
* on the resource size
*/
{
test: [
/\.bmp$/,
/\.gif$/,
/\.jpe?g$/,
/\.png$/,
/\.ico$/,
/\.avif$/,
/\.webp$/,
/\.svg$/,
],
oneOf: [
{
resourceQuery: /inline/, // foo.png?inline
type: 'asset/inline',
},
{
resourceQuery: /external/, // foo.png?external
type: 'asset/resource',
},
{
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: imaConfig.imageInlineSizeLimit,
},
},
},
],
},
/**
* Raw loaders, by default it loads file source into the bundle,
* optionally by postfixing the import with '?external' we can
* force it to return path to the source.
*/
{
test: [/\.csv$/, /\.txt$/, /\.html/],
oneOf: [
{
resourceQuery: /external/, // foo.png?external
type: 'asset/resource',
},
{
type: 'asset/source',
},
],
},
/**
* Handle app JS files
*/
{
test: /\.(mjs|js|jsx)$/,
include: appDir,
loader: require.resolve('swc-loader'),
options: await getSwcLoader('ecmascript'),
},
/**
* Handle app Typescript files
*/
typescript.enabled && {
test: /\.(ts|tsx)$/,
include: appDir,
loader: require.resolve('swc-loader'),
options: await getSwcLoader('typescript'),
},
/**
* Run vendor paths through swc for lower client versions
*/
isClient && {
test: /\.(js|mjs|cjs)$/,
include: [
/@ima/,
/@esmj/,
...(imaConfig.transformVendorPaths?.include ?? []),
],
exclude: [
appDir,
...(imaConfig.transformVendorPaths?.exclude ?? []),
],
loader: require.resolve('swc-loader'),
options: await imaConfig.swcVendor({
env: {
targets,
mode: 'usage',
coreJs: coreJsVersion,
bugfixes: true,
dynamicImport: true,
},
module: {
type: 'es6',
},
jsc: {
parser: {
syntax: 'ecmascript',
decorators: false,
dynamicImport: true,
},
},
sourceMaps: useSourceMaps,
inlineSourcesContent: useSourceMaps,
}, ctx),
},
/**
* CSS & LESS loaders, both have the exact same capabilities
*/
{
test: /\.module\.(c|le)ss$/,
sideEffects: true,
use: await getStyleLoaders(true),
...(imaConfig.experiments?.css && { type: 'css' }),
},
{
test: /\.(c|le)ss$/,
sideEffects: true,
use: await getStyleLoaders(),
...(imaConfig.experiments?.css && { type: 'css' }),
},
/**
* Fallback loader for all modules, that don't match any
* of the above defined rules. This should be defined last.
*/
{
exclude: [/^$/, /\.(js|mjs|jsx|ts|tsx|cjs)$/, /\.json$/],
type: 'asset/resource',
},
].filter(Boolean),
},
{
/**
* Allows the use of // @if | @else | @elseif | @endif directives
* on client server, ctx === 'client'|'client.es'|'server' variables
* to conditionally exclude parts of the source code for concrete bundles.
*/
test: /\.(js|mjs|jsx|cjs|ts|tsx)$/,
loader: 'preprocess-loader',
include: appDir,
options: {
context: {
server: isServer,
client: !isServer,
ctx: ctx.name,
},
},
},
{
/**
* Allow interop import of .mjs modules.
*/
test: /\.m?js$/,
type: 'javascript/auto',
resolve: {
fullySpecified: false,
},
},
/**
* Extracts source maps from existing source files (from their sourceMappingURL),
* this is usefull mainly for node_modules.
*/
useSourceMaps && {
enforce: 'pre',
test: /\.(js|mjs|jsx|ts|tsx|css)$/,
loader: require.resolve('source-map-loader'),
},
].filter(Boolean),
},
plugins: [
/**
* Initialize webpack.ProgressPlugin to track and report compilation
* progress across all configuration contexts. For verbose mode, we are using
* the default implementation.
*/
!ctx.verbose && (0, ProgressPlugin_1.createProgress)(name),
// Server/client specific plugins are defined below
...(isServer
? // Server-specific plugins
[]
: // Client-specific plugins
[
// This needs to run for both client bundles
new GenerateRunnerPlugin_1.GenerateRunnerPlugin({
context: ctx,
imaConfig,
}),
processCss &&
new mini_css_extract_plugin_1.default({
filename: ({ chunk }) => `${outputFolders.css}/${chunk?.name === name ? 'app' : '[name]'}${ctx.command === 'dev' ? '' : '.[contenthash]'}.css`,
ignoreOrder: true,
chunkFilename: `${outputFolders.css}/[id]${ctx.command === 'dev' ? '' : '.[contenthash]'}.css`,
}),
// Copies essential assets to static directory
isClientES &&
new copy_webpack_plugin_1.default({
patterns: [
{
from: 'app/public',
to: outputFolders.public,
noErrorOnMissing: true,
},
],
}),
/**
* TS type checking plugin (since swc doesn't do type checking, we want
* to show errors at least during build so it fails before going to production.
*/
isClientES &&
typescript.enabled &&
new fork_ts_checker_webpack_plugin_1.default({
typescript: {
configFile: typescript.tsconfigPath,
},
async: ctx.command === 'dev', // be async only in watch mode,
devServer: false,
// Custom formatter for async mode
...(ctx.command === 'dev' && {
formatter: issue => {
return JSON.stringify({
fileUri: issue.file,
line: issue.location?.start.line,
column: issue.location?.start.column,
name: issue.code,
message: issue.message,
});
},
logger: {
error: async (message) => {
try {
logger_1.logger.error(await (0, cliUtils_1.formatError)(JSON.parse(message.split('\n')[1]), ctx.rootDir));
}
catch {
// Fallback to original message
console.error(message);
}
},
log: () => { },
},
}),
}),
// Enables compression for assets in production build
...(ctx.command === 'build' && imaConfig.compress
? ['brotliCompress', 'gzip'].map(algorithm => new compression_webpack_plugin_1.default({
algorithm,
filename: `[path][base].${algorithm === 'brotliCompress' ? 'br' : 'gz'}`,
test: /\.(js|css|svg)$/,
minRatio: 0.95,
}))
: []),
// Following plugins enable react refresh and hmr in watch mode
useHMR && new webpack_1.HotModuleReplacementPlugin(),
useHMR &&
ctx.reactRefresh &&
new react_refresh_webpack_plugin_1.default({
esModule: true,
overlay: false,
include: [/\.(jsx|tsx)$/],
exclude: [/node_modules/],
}),
]),
// Generate assets manifest from all compilation instances
new ManifestPlugin_1.ManifestPlugin({ context: ctx, imaConfig }),
].filter(Boolean),
// Enable node preset for externals on server
externalsPresets: {
node: isServer,
},
// Server will use externals from node modules
...(isServer && {
externals: {
react: 'react',
'react-dom': 'react-dom',
'react-dom/client': 'react-dom/client',
'react-dom/server': 'react-dom/server',
},
}),
// Turn webpack performance reports off since we print reports ourselves
performance: false,
// Disable infrastructure logging in normal mode
infrastructureLogging: {
colors: true,
appendOnly: true,
level: ctx.verbose ? 'log' : 'error',
},
// Enable native css support (this replaces mini-css-extract-plugin and css-loader)
experiments: {
css: !!imaConfig.experiments?.css,
},
};
};