UNPKG

@ima/cli

Version:

IMA.js CLI tool to build, develop and work with IMA.js applications.

603 lines (602 loc) 27.6 kB
"use strict"; 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, }, }; };