UNPKG

bananass

Version:

Baekjoon Framework for JavaScript.🍌

287 lines (253 loc) 10.2 kB
/** * @fileoverview Asynchronously build and create bundled files using webpack and babel. */ // -------------------------------------------------------------------------------- // Import // -------------------------------------------------------------------------------- import { resolve } from 'node:path'; import fsPromises from 'node:fs/promises'; // DO NOT USE DESTRUCTURING syntax due to `mock` usage in test. import createLogger from 'bananass-utils-console/logger'; import createSpinner from 'bananass-utils-console/spinner'; import { bananass, success, error } from 'bananass-utils-console/theme'; import webpack from 'webpack'; import { transformArrayPrototypeToReversed, transformArrayPrototypeToSorted, transformObjectHasOwn, } from '../../babel-plugins/index.js'; import { defaultConfigObject as dco } from '../../core/conf/index.js'; import { problems as problemsSchema, configObject as configObjectSchema, } from '../../core/types/index.js'; import { DEFAULT_OUT_FILE_EXTENSION, NODE_VERSION_BAEKJOON, WEBPACK_BANNER, SUPPORTED_SOLUTION_FILE_EXTENSIONS, } from '../../core/constants.js'; // -------------------------------------------------------------------------------- // Typedefs // -------------------------------------------------------------------------------- /** * @import { Configuration as WebpackConfig } from 'webpack'; * @import { PluginItem as BabelPluginItem } from '@babel/core'; * @import { Problems, ConfigObject } from '../../core/types/index.js'; */ // -------------------------------------------------------------------------------- // Helpers // -------------------------------------------------------------------------------- /** * @param {BabelPluginItem[]} [babelPlugins] * @param {'module' | 'unambiguous'} [sourceType] */ function babelLoaderConfig(babelPlugins = [], sourceType = 'module') { return { loader: 'babel-loader', options: { targets: { node: NODE_VERSION_BAEKJOON }, presets: [ // Preset ordering is reversed (last to first). // https://babeljs.io/docs/presets#preset-ordering [ '@babel/preset-env', { targets: { node: NODE_VERSION_BAEKJOON }, }, ], ], plugins: [ // Plugin ordering is first to last. // https://babeljs.io/docs/plugins#plugin-ordering ...babelPlugins, transformArrayPrototypeToReversed, transformArrayPrototypeToSorted, transformObjectHasOwn, ], sourceType, }, }; } // -------------------------------------------------------------------------------- // Export // -------------------------------------------------------------------------------- /** * Asynchronously build and create bundled files using webpack and babel. * @param {Problems} problems * @param {ConfigObject} [configObject = dco] * @async */ export default async function build(problems, configObject = dco) { // ------------------------------------------------------------------------------ // Declarations // ------------------------------------------------------------------------------ const sanitizedProblems = problemsSchema.parse(problems); const sanitizedConfigObject = configObjectSchema.parse(configObject); const { cwd = dco.cwd, entryDir = dco.entryDir, outDir = dco.outDir, console: { debug = dco.console.debug, // (This comment was used for code formatting.) quiet = dco.console.quiet, } = dco.console, build: { clean = dco.build.clean, // (This comment was used for code formatting.) templateType = dco.build.templateType, } = dco.build, } = sanitizedConfigObject; const resolvedEntryDir = resolve(cwd, entryDir); const resolvedOutDir = resolve(cwd, outDir); const resolvedWebpackEntryFile = resolve( import.meta.dirname, `template-${templateType}.cjs`, ); const logger = createLogger({ debug, quiet }); const spinner = createSpinner(); // ------------------------------------------------------------------------------ // CLI Animation // ------------------------------------------------------------------------------ // Ensure correct `this` binding for `spinner.start` using arrow function. (Or use `apply`, `call` or `bind` method.) logger.log(() => spinner.start(bananass('Bananass build is running...', true))); // ------------------------------------------------------------------------------ // Webpack Configs // ------------------------------------------------------------------------------ /** @type {WebpackConfig[]} */ const webpackConfigs = sanitizedProblems.map( problem => /** @type {WebpackConfig} */ ({ /** @see https://webpack.js.org/configuration/target/ */ target: 'node', /** @see https://webpack.js.org/configuration/mode/ */ mode: 'production', /** @see https://webpack.js.org/configuration/resolve/#resolveextensions */ resolve: { extensions: [...SUPPORTED_SOLUTION_FILE_EXTENSIONS], }, /** @see https://webpack.js.org/concepts/#entry */ entry: resolvedWebpackEntryFile, /** @see https://webpack.js.org/concepts/#output */ output: { path: resolvedOutDir, filename: `${problem}${DEFAULT_OUT_FILE_EXTENSION}`, // clean: options.clean, // DO NOT USE THIS OPTION. }, /** @see https://webpack.js.org/concepts/#plugins */ plugins: [ new webpack.BannerPlugin({ banner: WEBPACK_BANNER, raw: true, stage: webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT, }), new webpack.DefinePlugin({ 'globalThis.BAEKJOON_PROBLEM_NUMBER_WITH_PATH': JSON.stringify( resolve(resolvedEntryDir, problem), ), 'globalThis.IS_PROD': JSON.stringify(true), // Same with `process.env.NODE_ENV === 'production'`. }), ], module: { rules: [ { test: /\.(?:js|mjs|cjs)$/i, // JavaScript: `js`, `mjs`, `cjs` use: [babelLoaderConfig()], }, /* * You can find the motivation for these TypeScript setups here: * https://github.com/babel/babel/blob/v7.28.2/packages/babel-preset-typescript/src/index.ts#L44-L102 */ { test: /\.mts$/i, // TypeScript: `mts` use: [ babelLoaderConfig([ [ '@babel/plugin-transform-typescript', { allowDeclareFields: true, }, ], ]), ], }, /* * `.ts` files can include both ESM and CommonJS modules, * so we need to convert them to CommonJS first when dealing with CommonJS modules. * * NOTE: `@babel/preset-typescript` treats `.ts` files as ESM by default, * and this behavior is not configurable. * This can cause issues when using CommonJS modules with `.ts` files. * To avoid this, we use `@babel/plugin-transform-modules-commonjs` and `@babel/plugin-transform-typescript` * directly to transpile `.ts` files to CommonJS format. */ { test: /\.(?:ts|cts)$/i, // TypeScript: `ts`, `cts` use: [ babelLoaderConfig( [ [ '@babel/plugin-transform-modules-commonjs', { allowTopLevelThis: true }, ], [ '@babel/plugin-transform-typescript', { allowDeclareFields: true, }, ], ], 'unambiguous', ), ], }, ], }, }), ); // ------------------------------------------------------------------------------ // Clean Output Directory // ------------------------------------------------------------------------------ try { // Clean the output directory before emitting files. // Note that we must not use the `webpackConfigs.output.clean` option. // Firstly, using the `webpackConfigs.output.clean` option would clean the output directory before each emit, // resulting in only one file in the output directory. // Secondly, even if we use `webpackConfigs.output.clean` only once with the `map()` method's `index` parameter, // it cannot guarantee the build order and may lead to race conditions where files get deleted unpredictably. if (clean) await fsPromises.rm(resolvedOutDir, { recursive: true, force: true }); } catch (err) { logger.log(() => spinner.error(error('Failed to clean output directory'))); const message = err instanceof Error ? err.message : String(err); throw new Error(error(message, true)); } // ------------------------------------------------------------------------------ // Run Webpack // ------------------------------------------------------------------------------ try { await new Promise((res, rej) => { webpack(webpackConfigs, (err, stats) => { if (err || stats?.hasErrors()) { rej(err || new Error(stats?.toString())); } else { res(stats); } }); }); } catch (err) { logger.log(() => spinner.error(error('Failed to run webpack'))); const message = err instanceof Error ? err.message : String(err); throw new Error(error(message, true)); } // ------------------------------------------------------------------------------ // Exit // ------------------------------------------------------------------------------ logger .log(() => spinner.success(success('Bananass build completed successfully'))) .eol() .log('Output Directory:', resolvedOutDir) .log( 'Created:', sanitizedProblems .map(problem => `${problem}${DEFAULT_OUT_FILE_EXTENSION}`) .join(', '), ); }