UNPKG

@storm-stack/core

Version:

A build toolkit and runtime used by Storm Software in TypeScript applications

329 lines (323 loc) 13.3 kB
import { finalize } from './chunk-QOGDZ7VT.js'; import { init } from './chunk-OVTQGVVL.js'; import { lint } from './chunk-CGQHYHSA.js'; import { _new } from './chunk-HZ263YUL.js'; import { prepare, getPersistedMeta, getChecksum, createContext } from './chunk-3MGEBIVG.js'; import { build } from './chunk-4LKKF5CO.js'; import { clean } from './chunk-YLHFO6NT.js'; import { docs } from './chunk-OPZLKH33.js'; import { __STORM_STACK_IS_PLUGIN__ } from './chunk-S6IVF7HS.js'; import { init_esm_shims, __name } from './chunk-QH7NXH7H.js'; import { LogLevelLabel } from '@storm-software/config-tools/types'; import { install } from '@stryke/fs/install'; import { isPackageExists } from '@stryke/fs/package-fns'; import { joinPaths } from '@stryke/path/join-paths'; import { camelCase } from '@stryke/string-format/camel-case'; import { isError } from '@stryke/type-checks/is-error'; import { isNumber } from '@stryke/type-checks/is-number'; import { isSetObject } from '@stryke/type-checks/is-set-object'; import { isSetString } from '@stryke/type-checks/is-set-string'; import chalk from 'chalk'; import defu from 'defu'; import { createHooks } from 'hookable'; // src/base/engine.ts init_esm_shims(); // src/lib/utilities/plugin-helpers.ts init_esm_shims(); function isPluginInstance(plugin) { return isSetObject(plugin) && __STORM_STACK_IS_PLUGIN__ in plugin && plugin[__STORM_STACK_IS_PLUGIN__] === true; } __name(isPluginInstance, "isPluginInstance"); function isPluginConfigObject(plugin) { return isSetObject(plugin) && "plugin" in plugin && isSetString(plugin.plugin); } __name(isPluginConfigObject, "isPluginConfigObject"); // src/base/engine.ts var Engine = class _Engine { static { __name(this, "Engine"); } inlineConfig; /** * The Storm Stack context */ #context; /** * The engine hooks - these allow the plugins to hook into the engines processing */ #hooks; /** * The plugins provided in the options */ #plugins = []; /** * The Storm Stack context */ get context() { return this.#context; } /** * The Storm Stack engine hooks */ get hooks() { return this.#hooks; } /** * Create a new Storm Stack Engine instance * * @param inlineConfig - The inline configuration for the Storm Stack engine */ constructor(inlineConfig) { this.inlineConfig = inlineConfig; } /** * Initialize the engine */ static async create(inlineConfig) { const engine = new _Engine(inlineConfig); await engine.init(); return engine; } /** * Create a new Storm Stack project * * @remarks * This method will create a new Storm Stack project in the current directory. * * @param inlineConfig - The inline configuration for the new command * @returns A promise that resolves when the project has been created */ async new(_inlineConfig = { command: "new" }) { this.#context.log(LogLevelLabel.INFO, "\u{1F195} Creating a new Storm Stack project"); await _new(this.#context, this.#hooks); this.#context.log(LogLevelLabel.TRACE, "Storm Stack - New command completed"); } /** * Clean any previously prepared artifacts * * @remarks * This method will remove the previous Storm Stack artifacts from the project. * * @param inlineConfig - The inline configuration for the clean command * @returns A promise that resolves when the clean command has completed */ async clean(_inlineConfig = { command: "clean" }) { this.#context.log(LogLevelLabel.INFO, "\u{1F9F9} Cleaning the previous Storm Stack artifacts"); await clean(this.#context, this.#hooks); this.#context.log(LogLevelLabel.TRACE, "Storm Stack - Clean command completed"); } /** * Prepare the Storm Stack project prior to building * * @remarks * This method will create the necessary directories, and write the artifacts files to the project. * * @param inlineConfig - The inline configuration for the prepare command * @returns A promise that resolves when the prepare command has completed */ async prepare(_inlineConfig = { command: "prepare" }) { this.#context.log(LogLevelLabel.INFO, "\u{1F3D7}\uFE0F Preparing the Storm Stack project"); await prepare(this.#context, this.#hooks); this.#context.log(LogLevelLabel.TRACE, "Storm Stack preparation completed"); } /** * Lint the project * * @param inlineConfig - The inline configuration for the lint command * @returns A promise that resolves when the lint command has completed */ async lint(inlineConfig = { command: "lint" }) { if (this.#context.persistedMeta?.checksum !== this.#context.meta.checksum) { this.#context.log(LogLevelLabel.INFO, "The Storm Stack project has been modified since the last time `prepare` was ran. Re-preparing the project."); await this.prepare(inlineConfig); } this.#context.log(LogLevelLabel.INFO, "\u{1F4CB} Linting the Storm Stack project"); await lint(this.#context, this.#hooks); this.#context.log(LogLevelLabel.TRACE, "Storm Stack linting completed"); } /** * Build the project * * @remarks * This method will build the Storm Stack project, generating the necessary artifacts. * * @param inlineConfig - The inline configuration for the build command * @returns A promise that resolves when the build command has completed */ async build(inlineConfig = { command: "build" }) { const persistedMeta = await getPersistedMeta(this.#context); const checksum = await getChecksum(this.#context.options.projectRoot); if (persistedMeta?.checksum !== checksum) { this.#context.log(LogLevelLabel.INFO, "The Storm Stack project has been modified since the last time `prepare` was ran. Re-preparing the project."); await this.prepare(inlineConfig); } this.#context.log(LogLevelLabel.INFO, "\u{1F4E6} Building the Storm Stack project"); await build(this.#context, this.#hooks); this.#context.log(LogLevelLabel.TRACE, "Storm Stack build completed"); } /** * Generate the documentation for the project * * @param inlineConfig - The inline configuration for the docs command * @returns A promise that resolves when the documentation generation has completed */ async docs(inlineConfig = { command: "docs" }) { if (this.#context.persistedMeta?.checksum !== this.#context.meta.checksum) { this.#context.log(LogLevelLabel.INFO, "The Storm Stack project has been modified since the last time `prepare` was ran. Re-preparing the project."); await this.prepare(inlineConfig); } this.#context.log(LogLevelLabel.INFO, "Generating documentation for the Storm Stack project"); await docs(this.#context, this.#hooks); this.#context.log(LogLevelLabel.TRACE, "Storm Stack documentation generation completed"); } /** * Finalization process * * @remarks * This step includes any final processes or clean up required by Storm Stack. It will be run after each Storm Stack command. * * @returns A promise that resolves when the finalization process has completed */ async finalize() { this.#context.log(LogLevelLabel.TRACE, "Storm Stack finalize execution started"); await finalize(this.#context, this.#hooks); this.#context.log(LogLevelLabel.TRACE, "Storm Stack finalize execution completed"); } /** * Initialize the engine */ async init() { this.#hooks = createHooks(); this.#context = await createContext(this.inlineConfig); this.#context.log(LogLevelLabel.TRACE, "\u2699\uFE0F Initializing Storm Stack engine"); for (const plugin of this.#context.options.userConfig.plugins ?? []) { await this.addPlugin(plugin); } if (this.#plugins.length === 0) { this.#context.log(LogLevelLabel.WARN, "No Storm Stack plugins or presets were specified in the options. Please ensure this is correct, as it is generally not recommended."); } else { for (const plugin of this.#plugins) { plugin.addHooks(this.#hooks); } } await init(this.#context, this.#hooks); this.#context.log(LogLevelLabel.INFO, "Storm Stack engine has been initialized"); } /** * Add a Storm Stack plugin used in the build process * * @param config - The import path of the plugin to add */ async addPlugin(config) { if (config) { const instance = await this.initPlugin(config); if (!instance) { return; } if (instance.dependencies) { for (const dependency of instance.dependencies) { await this.addPlugin(dependency); } } this.#context.log(LogLevelLabel.DEBUG, `Successfully initialized the ${chalk.bold.cyanBright(instance.name)} plugin`); this.#plugins.push(instance); } } /** * Initialize a Storm Stack plugin * * @param plugin - The import path of the plugin to add */ async initPlugin(plugin) { if (!plugin || !isSetString(plugin) && !Array.isArray(plugin) && !isPluginConfigObject(plugin) && !isPluginInstance(plugin)) { throw new Error(`Invalid plugin specified in the configuration - ${JSON.stringify(plugin)}. Please ensure the value is a plugin name, an object with the \`plugin\` and \`props\` properties, or an instance of \`Plugin\`.`); } let pluginInstance; if (isPluginInstance(plugin)) { pluginInstance = plugin; pluginInstance.options ??= {}; pluginInstance.options.log ??= this.#context.log; } else { const pluginConfig = isSetString(plugin) ? [ plugin, {} ] : Array.isArray(plugin) ? plugin : [ plugin.plugin, plugin.props ]; let installPath = pluginConfig[0]; if (installPath.startsWith("@") && installPath.split("/").filter(Boolean).length > 2) { const splits = installPath.split("/").filter(Boolean); installPath = `${splits[0]}/${splits[1]}`; } const isInstalled = isPackageExists(installPath, { paths: [ this.#context.options.workspaceConfig.workspaceRoot, this.#context.options.projectRoot ] }); if (!isInstalled && this.#context.options.skipInstalls !== true) { this.#context.log(LogLevelLabel.WARN, `The plugin package "${installPath}" is not installed. It will be installed automatically.`); const result = await install(installPath, { cwd: this.#context.options.projectRoot }); if (isNumber(result.exitCode) && result.exitCode > 0) { this.#context.log(LogLevelLabel.ERROR, result.stderr); throw new Error(`An error occurred while installing the build plugin package "${installPath}" `); } } try { const module = await this.#context.resolver.import(this.#context.resolver.esmResolve(joinPaths(pluginConfig[0], "plugin"))); const PluginConstructor = module.plugin ?? module.default; pluginInstance = new PluginConstructor({ ...pluginConfig[1] ?? {}, log: this.#context.log }); } catch (error) { try { const module = await this.#context.resolver.import(this.#context.resolver.esmResolve(pluginConfig[0])); const PluginConstructor = module.plugin ?? module.default; pluginInstance = new PluginConstructor({ ...pluginConfig[1] ?? {}, log: this.#context.log }); } catch { if (!isInstalled) { throw new Error(`The plugin package "${pluginConfig[0]}" is not installed. Please install the package using the command: "npm install ${pluginConfig[0]} --save-dev"`); } else { throw new Error(`An error occurred while importing the build plugin package "${pluginConfig[0]}": ${isError(error) ? error.message : String(error)} Note: Please ensure the plugin package's default export is a class that extends \`Plugin\` with a constructor that excepts a single arguments of type \`PluginOptions\`.`); } } } } if (!isPluginInstance(pluginInstance)) { throw new Error(`The plugin option ${JSON.stringify(plugin)} does not export a valid module.`); } this.#context.options.plugins[pluginInstance.identifier] = defu(pluginInstance.options ?? {}, isSetObject(this.#context.options.plugins[pluginInstance.identifier]) ? this.#context.options.plugins[pluginInstance.identifier] : {}, camelCase(pluginInstance.name) !== camelCase(pluginInstance.identifier) && isSetObject(this.#context.options.plugins[pluginInstance.name]) ? this.#context.options.plugins[pluginInstance.name] : {}); pluginInstance.options = this.#context.options.plugins[pluginInstance.identifier]; const duplicatePlugin = this.#plugins.find((plugin2) => plugin2.isSame(pluginInstance)); if (duplicatePlugin) { this.#context.log(LogLevelLabel.TRACE, `Duplicate ${chalk.bold.cyanBright(duplicatePlugin.identifier)} plugin dependency detected - Skipping initialization.`); duplicatePlugin.options = defu(duplicatePlugin.options ?? {}, this.#context.options.plugins[pluginInstance.identifier]); this.#context.options.plugins[duplicatePlugin.identifier] = duplicatePlugin.options; return null; } this.#context.log(LogLevelLabel.TRACE, `Initializing the ${chalk.bold.cyanBright(pluginInstance.name)} plugin...`); return pluginInstance; } }; export { Engine };