From 5b500b07dbbc77fed769661608a6c79f4d81d2e6 Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Mon, 22 Apr 2024 13:51:23 -0700 Subject: [PATCH] wip --- .eslintrc.js | 2 +- package-lock.json | 20 +- .../.config/tsconfig.paths.json | 56 +- packages/midnight-smoker/package.json | 4 +- .../src/component/reporter/reporter.ts | 5 +- .../src/component/rule/context.ts | 4 + .../src/component/schema/lint-manifest.ts | 1 + .../component/schema/pkg-install-manifest.ts | 1 + .../src/component/schema/rule-static.ts | 1 + .../src/error/reporter-error.ts | 2 +- .../controller/control-machine-events.ts | 44 +- .../src/machine/controller/control-machine.ts | 797 +++++++++--------- packages/midnight-smoker/src/machine/index.ts | 2 +- .../src/machine/installer/install-machine.ts | 13 +- .../machine/installer/installer-machine.ts | 16 +- .../src/machine/linter/linter-machine.ts | 18 +- .../src/machine/linter/rule-machine.ts | 2 +- .../src/machine/machine-util.ts | 89 -- .../src/machine/packer/pack-machine.ts | 5 +- .../src/machine/packer/packer-machine.ts | 36 +- .../src/machine/plugin-loader/index.ts | 1 + .../plugin-loader/plugin-loader-machine.ts | 520 ++++++++++++ .../machine/reifier/reifier-machine-actors.ts | 190 +++++ .../src/machine/reifier/reifier-machine.ts | 247 ++---- .../reporter/reporter-machine-actors.ts | 24 +- .../reporter/reporter-machine-events.ts | 2 +- .../src/machine/reporter/reporter-machine.ts | 213 ++--- .../src/machine/runner/run-machine.ts | 36 +- .../src/machine/runner/runner-machine.ts | 28 +- .../midnight-smoker/src/machine/util/index.ts | 160 ++++ .../src/plugin/plugin-metadata.ts | 11 + packages/midnight-smoker/src/smoker.ts | 2 +- .../test/unit/component/rule/context.spec.ts | 1 + .../test/unit/component/rule/issue.spec.ts | 1 + .../plugin-default/data/pnpm-dist-tags.json | 6 +- .../plugin-default/data/pnpm-versions.json | 3 +- .../test/unit/package-manager/npm7.spec.ts | 3 + .../test/unit/package-manager/npm9.spec.ts | 1 + 38 files changed, 1605 insertions(+), 962 deletions(-) delete mode 100644 packages/midnight-smoker/src/machine/machine-util.ts create mode 100644 packages/midnight-smoker/src/machine/plugin-loader/index.ts create mode 100644 packages/midnight-smoker/src/machine/plugin-loader/plugin-loader-machine.ts create mode 100644 packages/midnight-smoker/src/machine/reifier/reifier-machine-actors.ts create mode 100644 packages/midnight-smoker/src/machine/util/index.ts diff --git a/.eslintrc.js b/.eslintrc.js index 420c352e..f5409b4f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -78,7 +78,7 @@ module.exports = { '@typescript-eslint/require-await': 'off', // unfortunately required when using Sets and Maps - '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-non-null-assertion': 'warn', // this rule seems broken '@typescript-eslint/no-invalid-void-type': 'off', diff --git a/package-lock.json b/package-lock.json index 4c807720..b7f18bb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13484,15 +13484,6 @@ "version": "1.0.2", "license": "ISC" }, - "node_modules/xstate": { - "version": "5.9.1", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.9.1.tgz", - "integrity": "sha512-85edx7iMqRJSRlEPevDwc98EWDYUlT5zEQ54AXuRVR+G76gFbcVTAUdtAeqOVxy8zYnUr9FBB5114iK6enljjw==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/xstate" - } - }, "node_modules/xtend": { "version": "4.0.2", "dev": true, @@ -14776,7 +14767,7 @@ "terminal-link": "2.1.1", "type-fest": "4.8.3", "which": "4.0.0", - "xstate": "5.9.1", + "xstate": "5.11.0", "yargs": "17.7.2", "zod": "3.22.4", "zod-validation-error": "2.1.0" @@ -14824,6 +14815,15 @@ "node": "^16.13.0 || >=18.0.0" } }, + "packages/midnight-smoker/node_modules/xstate": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.11.0.tgz", + "integrity": "sha512-0MqTLpc7dr/hXFHY25oN4sdnO3Ey6MYy9WkWxOgiwjPV0S6rWwLb5nZlRlPDSku2GEV4/y6AR8bX+GNCOxnEwA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + }, "packages/midnight-smoker/node_modules/zod-to-json-schema": { "version": "3.22.3", "dev": true, diff --git a/packages/midnight-smoker/.config/tsconfig.paths.json b/packages/midnight-smoker/.config/tsconfig.paths.json index f1d01661..9405c89f 100644 --- a/packages/midnight-smoker/.config/tsconfig.paths.json +++ b/packages/midnight-smoker/.config/tsconfig.paths.json @@ -2,34 +2,34 @@ "compilerOptions": { "paths": { "@midnight-smoker/plugin-default": ["../../plugin-default"], - "@midnight-smoker/test-util": ["../../test-util"] - // "#cli": ["../src/cli/index.js"], - // "#cli/*": ["../src/cli/*.js"], - // "#config-file": ["../src/config-file.js"], - // "#component": ["../src/component/component/index.js"], - // "#constants": ["../src/constants.js"], - // "#controller": ["../src/controller/index.js"], - // "#controller/*": ["../src/controller/*.js"], - // "#error": ["../src/error/index.js"], - // "#error/*": ["../src/error/*.js"], - // "#event": ["../src/event/index.js"], - // "#event/*": ["../src/event/*.js"], - // "#executor": ["../src/component/executor/index.js"], - // "#options": ["../src/options/index.js"], - // "#options/*": ["../src/options/*.js"], - // "#pkg-manager": ["../src/component/pkg-manager/index.js"], - // "#pkg-manager/*": ["../src/component/pkg-manager/*.js"], - // "#plugin": ["../src/plugin/index.js"], - // "#plugin/*": ["../src/plugin/*.js"], - // "#reporter": ["../src/component/reporter/index.js"], - // "#reporter/*": ["../src/component/reporter/*.js"], - // "#rule": ["../src/component/rule/index.js"], - // "#rule/*": ["../src/component/rule/*.js"], - // "#schema": ["../src/component/schema/index.js"], - // "#schema/*": ["../src/component/schema/*.js"], - // "#smoker": ["../src/smoker.js"], - // "#util": ["../src/util/index.js"], - // "#util/*": ["../src/util/*.js"] + "@midnight-smoker/test-util": ["../../test-util"], + "#cli": ["../src/cli/index.js"], + "#cli/*": ["../src/cli/*.js"], + "#component": ["../src/component/component/index.js"], + "#config-file": ["../src/config-file.js"], + "#constants": ["../src/constants.js"], + "#error": ["../src/error/index.js"], + "#error/*": ["../src/error/*.js"], + "#event": ["../src/event/index.js"], + "#event/*": ["../src/event/*.js"], + "#executor": ["../src/component/executor/index.js"], + "#machine": ["../src/machine/index.js"], + "#machine/*": ["../src/machine/*/index.js"], + "#options": ["../src/options/index.js"], + "#options/*": ["../src/options/*.js"], + "#pkg-manager": ["../src/component/pkg-manager/index.js"], + "#pkg-manager/*": ["../src/component/pkg-manager/*.js"], + "#plugin": ["../src/plugin/index.js"], + "#plugin/*": ["../src/plugin/*.js"], + "#reporter": ["../src/component/reporter/index.js"], + "#reporter/*": ["../src/component/reporter/*.js"], + "#rule": ["../src/component/rule/index.js"], + "#rule/*": ["../src/component/rule/*.js"], + "#schema": ["../src/component/schema/index.js"], + "#schema/*": ["../src/component/schema/*.js"], + "#smoker": ["../src/smoker.js"], + "#util": ["../src/util/index.js"], + "#util/*": ["../src/util/*.js"] } } } diff --git a/packages/midnight-smoker/package.json b/packages/midnight-smoker/package.json index 65413087..ffff8f8e 100644 --- a/packages/midnight-smoker/package.json +++ b/packages/midnight-smoker/package.json @@ -28,7 +28,7 @@ "#config-file": "./dist/config-file.js", "#constants": "./dist/constants.js", "#machine": "./dist/machine/index.js", - "#machine/*": "./dist/machine/*.js", + "#machine/*": "./dist/machine/*/index.js", "#error": "./dist/error/index.js", "#error/*": "./dist/error/*.js", "#event": "./dist/event/index.js", @@ -196,7 +196,7 @@ "terminal-link": "2.1.1", "type-fest": "4.8.3", "which": "4.0.0", - "xstate": "5.9.1", + "xstate": "5.11.0", "yargs": "17.7.2", "zod": "3.22.4", "zod-validation-error": "2.1.0" diff --git a/packages/midnight-smoker/src/component/reporter/reporter.ts b/packages/midnight-smoker/src/component/reporter/reporter.ts index 3286554d..8100c852 100644 --- a/packages/midnight-smoker/src/component/reporter/reporter.ts +++ b/packages/midnight-smoker/src/component/reporter/reporter.ts @@ -46,9 +46,10 @@ export class Reporter extends ReifiedComponent< // await Promise.resolve(); // XXX don't like these casts if (this.hasListener(data.type)) { - const listenerName = `on${data.type}` as keyof ReporterListeners; - const listener = this.def[listenerName] as ReporterListener; try { + const listenerName = `on${data.type}` as keyof ReporterListeners; + const listener = this.def[listenerName] as ReporterListener; + debug('%s - invoking %s', this, listenerName); await listener(this.ctx, data); debug('%s - invoked %s', this, listenerName); } catch (err) { diff --git a/packages/midnight-smoker/src/component/rule/context.ts b/packages/midnight-smoker/src/component/rule/context.ts index 1cdb9be1..8513199b 100644 --- a/packages/midnight-smoker/src/component/rule/context.ts +++ b/packages/midnight-smoker/src/component/rule/context.ts @@ -66,6 +66,10 @@ export class RuleContext implements StaticRuleContext { return [...this.#issues]; } + public get localPath(): string { + return this.staticCtx.localPath; + } + /** * The absolute path to this context's package's `package.json`. */ diff --git a/packages/midnight-smoker/src/component/schema/lint-manifest.ts b/packages/midnight-smoker/src/component/schema/lint-manifest.ts index 98288f02..c55478fa 100644 --- a/packages/midnight-smoker/src/component/schema/lint-manifest.ts +++ b/packages/midnight-smoker/src/component/schema/lint-manifest.ts @@ -10,6 +10,7 @@ export const LintManifestSchema = z.object({ installPath: NonEmptyStringSchema.describe( 'Install path of package being checked', ), + localPath: NonEmptyStringSchema, }); export const LintManifestsSchema = z diff --git a/packages/midnight-smoker/src/component/schema/pkg-install-manifest.ts b/packages/midnight-smoker/src/component/schema/pkg-install-manifest.ts index 5ff4c929..7fc9bb2a 100644 --- a/packages/midnight-smoker/src/component/schema/pkg-install-manifest.ts +++ b/packages/midnight-smoker/src/component/schema/pkg-install-manifest.ts @@ -5,6 +5,7 @@ import {InstallManifestSchema} from './install-manifest'; export const PkgInstallManifest = InstallManifestSchema.extend({ isAdditional: z.literal(false).optional(), installPath: NonEmptyStringSchema, + localPath: NonEmptyStringSchema, }); export type PkgInstallManifest = z.infer; diff --git a/packages/midnight-smoker/src/component/schema/rule-static.ts b/packages/midnight-smoker/src/component/schema/rule-static.ts index 31bf9ece..08794637 100644 --- a/packages/midnight-smoker/src/component/schema/rule-static.ts +++ b/packages/midnight-smoker/src/component/schema/rule-static.ts @@ -5,6 +5,7 @@ import {z} from 'zod'; export const StaticRuleContextSchema = z .object({ pkgName: NonEmptyStringSchema, + localPath: NonEmptyStringSchema, pkgJson: PackageJsonSchema, pkgJsonPath: NonEmptyStringSchema, installPath: NonEmptyStringSchema, diff --git a/packages/midnight-smoker/src/error/reporter-error.ts b/packages/midnight-smoker/src/error/reporter-error.ts index bb1ce4fa..6be0d3ac 100644 --- a/packages/midnight-smoker/src/error/reporter-error.ts +++ b/packages/midnight-smoker/src/error/reporter-error.ts @@ -15,7 +15,7 @@ export class ReporterError extends BaseSmokerError< constructor(error: Error, reporter: ReporterDef) { super( - `Reporter ${reporter.name} threw while initializing: ${error.message}`, + `Reporter ${reporter.name} threw: ${error.message}`, {reporter}, error, ); diff --git a/packages/midnight-smoker/src/machine/controller/control-machine-events.ts b/packages/midnight-smoker/src/machine/controller/control-machine-events.ts index 271b4cb0..cf290083 100644 --- a/packages/midnight-smoker/src/machine/controller/control-machine-events.ts +++ b/packages/midnight-smoker/src/machine/controller/control-machine-events.ts @@ -19,10 +19,11 @@ import { type SmokerEventData, } from '#schema'; import {type Simplify, type ValueOf} from 'type-fest'; +import {type SomeReporter, type SomeRule} from '../../component'; import {type InstallerMachineOutput} from '../installer/installer-machine'; import {type LinterMachineOutput} from '../linter/linter-machine'; import {type PackerMachineOutput} from '../packer/packer-machine'; -import {type ReifierOutput} from '../reifier/reifier-machine'; +import {type PluginLoaderMachineOutput} from '../plugin-loader/plugin-loader-machine'; import {type ReporterMachineOutput} from '../reporter/reporter-machine'; import {type RunnerMachineOutput} from '../runner/runner-machine'; @@ -41,7 +42,6 @@ export type CtrlEvents = | CtrlPkgManagerPackOkEvent | CtrlPkgManagerInstallBeginEvent | CtrlPkgManagerPackBeginEvent - | CtrlReifierDoneEvent | CtrlRunScriptsEvent | CtrlRunScriptBeginEvent | CtrlReporterDoneEvent @@ -65,6 +65,12 @@ export type CtrlEvents = | CtrlPkgManagerLintBeginEvent | CtrlPkgManagerLintOkEvent | CtrlPkgManagerLintFailedEvent + | CtrlComponentsEvent + | CtrlPluginLoaderDoneEvent + | CtrlPackFailedEvent + | CtrlPackOkEvent + | CtrlInstallOkEvent + | CtrlInstallFailedEvent | CtrlRunnerMachineDoneEvent; type SourceEvents = InstallEventData & @@ -217,9 +223,29 @@ export interface CtrlPkgManagerPackBeginEvent extends PackOptions { type: 'PKG_MANAGER_PACK_BEGIN'; } -export interface CtrlReifierDoneEvent { - output: ReifierOutput; - type: 'xstate.done.actor.Reifier.*'; +export interface CtrlPluginLoaderDoneEvent { + output: PluginLoaderMachineOutput; + type: 'xstate.done.actor.PluginLoaderMachine'; +} + +export interface CtrlPackFailedEvent { + type: 'PACK_FAILED'; + error: PackError | PackParseError; +} + +export interface CtrlPackOkEvent { + type: 'PACK_OK'; + manifests: InstallManifest[]; +} + +export interface CtrlInstallOkEvent { + type: 'INSTALL_OK'; + manifests: InstallManifest[]; +} + +export interface CtrlInstallFailedEvent { + type: 'INSTALL_FAILED'; + error: InstallError; } export interface CtrlInstallerMachineDoneEvent { @@ -319,3 +345,11 @@ export interface CtrlLintBeginEvent { export interface CtrlRuleErrorEvent { type: 'RULE_ERROR'; } + +export interface CtrlComponentsEvent { + type: 'COMPONENTS'; + sender: string; + pkgManagers: PkgManager[]; + reporters: SomeReporter[]; + rules: SomeRule[]; +} diff --git a/packages/midnight-smoker/src/machine/controller/control-machine.ts b/packages/midnight-smoker/src/machine/controller/control-machine.ts index 05fd5c95..3dd54489 100644 --- a/packages/midnight-smoker/src/machine/controller/control-machine.ts +++ b/packages/midnight-smoker/src/machine/controller/control-machine.ts @@ -1,10 +1,24 @@ import {DEFAULT_EXECUTOR_ID, SYSTEM_EXECUTOR_ID} from '#constants'; import {fromUnknownError} from '#error'; import {SmokerEvent} from '#event'; +import { + InstallerMachine, + type InstallerMachineInput, + type InstallerMachineInstallEvent, +} from '#machine/installer'; +import {LinterMachine, type LinterMachineOutput} from '#machine/linter'; +import {PackerMachine, type PackerMachineInput} from '#machine/packer'; +import { + PluginLoaderMachine, + type PluginLoaderMachineInput, +} from '#machine/plugin-loader'; +import {ReporterMachine, type ReporterMachineOutput} from '#machine/reporter'; +import {RunnerMachine, type RunnerMachineOutput} from '#machine/runner'; +import * as MachineUtil from '#machine/util'; import {type SmokerOptions} from '#options/options'; import {type PkgManager} from '#pkg-manager'; import {type PluginRegistry} from '#plugin'; -import {type Reporter} from '#reporter/reporter'; +import {type SomeReporter} from '#reporter/reporter'; import { type Executor, type InstallEventBaseData, @@ -17,42 +31,18 @@ import { } from '#schema'; import {type FileManager} from '#util/filemanager'; import {filter, isEmpty, map, memoize, partition, sumBy, uniq} from 'lodash'; +import assert from 'node:assert'; import { and, assign, enqueueActions, - fromPromise, log, not, + raise, sendTo, setup, type ActorRefFrom, } from 'xstate'; -import { - InstallerMachine, - type InstallerMachineInput, -} from '../installer/installer-machine'; -import {type InstallerMachineInstallEvent} from '../installer/installer-machine-events'; -import { - LinterMachine, - type LinterMachineOutput, -} from '../linter/linter-machine'; -import * as MachineUtil from '../machine-util'; -import {uniquePkgNames} from '../machine-util'; -import {PackerMachine, type PackerMachineInput} from '../packer/packer-machine'; -import { - LoadableComponents, - ReifierMachine, - type ReifierOutput, -} from '../reifier/reifier-machine'; -import { - ReporterMachine, - type ReporterMachineOutput, -} from '../reporter/reporter-machine'; -import { - RunnerMachine, - type RunnerMachineOutput, -} from '../runner/runner-machine'; import type * as Event from './control-machine-events'; export interface CtrlMachineInput { @@ -74,13 +64,12 @@ export type CtrlMachineContext = Omit< pkgManagers: PkgManager[]; defaultExecutor: Executor; systemExecutor: Executor; - reifierMachineRefs: Record>; reporterMachineRefs: Record>; runnerMachineRefs: Record>; linterMachineRefs: Record>; runScriptManifests: WeakMap; lintManifests: WeakMap; - reporters: Reporter[]; + reporters: SomeReporter[]; rules: SomeRule[]; shouldLint: boolean; scripts: string[]; @@ -92,6 +81,8 @@ export type CtrlMachineContext = Omit< shouldHalt: boolean; packerMachineRef?: ActorRefFrom; installerMachineRef?: ActorRefFrom; + pluginLoaderRef?: ActorRefFrom; + startTime: number; }; export type CtrlOutputOk = MachineUtil.MachineOutputOk<{ @@ -112,21 +103,9 @@ export const ControlMachine = setup({ output: {} as CtrlMachineOutput, }, actors: { - setupPkgManagers: fromPromise( - async ({input: pkgManagers}): Promise => { - await Promise.all(pkgManagers.map((pkgManager) => pkgManager.setup())); - }, - ), - teardownPkgManagers: fromPromise( - async ({input: pkgManagers}): Promise => { - await Promise.all( - pkgManagers.map((pkgManager) => pkgManager.teardown()), - ); - }, - ), ReporterMachine, + PluginLoaderMachine, RunnerMachine, - ReifierMachine, PackerMachine, InstallerMachine, LinterMachine, @@ -236,6 +215,12 @@ export const ControlMachine = setup({ */ isNotLinting: ({context: {linterMachineRefs}}) => isEmpty(linterMachineRefs), + + hasPluginLoaderRef: ({context: {pluginLoaderRef}}) => + Boolean(pluginLoaderRef), + + hasReporterRefs: ({context: {reporterMachineRefs}}) => + !isEmpty(reporterMachineRefs), }, actions: { assignLintResult: assign({ @@ -249,13 +234,20 @@ export const ControlMachine = setup({ }; }, }), - cleanup: enqueueActions( - ({enqueue, context: {reporterMachineRefs: reporterMachines}}) => { - Object.values(reporterMachines).forEach((reporterMachine) => { + flushReporters: enqueueActions( + ({enqueue, context: {reporterMachineRefs}}) => { + Object.values(reporterMachineRefs).forEach((reporterMachine) => { enqueue.sendTo(reporterMachine, {type: 'HALT'}); }); }, ), + teardown: sendTo( + ({context: {pluginLoaderRef}}) => { + assert.ok(pluginLoaderRef); + return pluginLoaderRef; + }, + {type: 'TEARDOWN'}, + ), assignError: assign({ // TODO: aggregate for multiple @@ -263,13 +255,6 @@ export const ControlMachine = setup({ error ? fromUnknownError(error) : context.error, }), - stopReifiers: enqueueActions(({enqueue, context: {reifierMachineRefs}}) => { - for (const machine of Object.values(reifierMachineRefs)) { - enqueue.stopChild(machine); - } - enqueue.assign({reifierMachineRefs: {}}); - }), - assignRunScriptManifests: assign({ runScriptManifests: ({context: {pkgManagers, scripts}}) => new WeakMap( @@ -297,10 +282,13 @@ export const ControlMachine = setup({ return new WeakMap( pkgManagers.map((pkgManager) => [ pkgManager, - pkgManager.pkgInstallManifests.map(({installPath, pkgName}) => ({ - installPath, - pkgName, - })), + pkgManager.pkgInstallManifests.map( + ({installPath, pkgName, localPath}) => ({ + installPath, + pkgName, + localPath, + }), + ), ]), ); }, @@ -385,69 +373,48 @@ export const ControlMachine = setup({ } }, ), + stopPluginLoaderMachine: enqueueActions( + ({enqueue, context: {pluginLoaderRef}}) => { + if (pluginLoaderRef) { + enqueue.stopChild(pluginLoaderRef.id); + enqueue.assign({pluginLoaderRef: undefined}); + } + }, + ), spawnReporterMachines: assign({ reporterMachineRefs: ({spawn, context: {reporters}, self}) => Object.fromEntries( reporters.map((reporter) => { const id = `ReporterMachine.${MachineUtil.makeId()}`; + // @ts-expect-error https://github.com/statelyai/xstate/blob/main/packages/core/src/types.ts#L114 -- no TEmitted const actor = spawn('ReporterMachine', { id, - // @ts-expect-error https://github.com/statelyai/xstate/blob/main/packages/core/src/types.ts#L114 -- no TEmitted input: {emitter: self, reporter}, }); return [id, MachineUtil.monkeypatchActorLogger(actor, id)]; }), ), }), - assignReifierResults: assign({ - pkgManagers: ({context: {pkgManagers}}, output: ReifierOutput) => { - MachineUtil.assertMachineOutputOk(output); - return [...pkgManagers, ...output.pkgManagers]; - }, - reporters: ({context: {reporters}}, output: ReifierOutput) => { - MachineUtil.assertMachineOutputOk(output); - return [...reporters, ...output.reporters]; - }, - rules: ({context: {rules}}, output: ReifierOutput) => { - MachineUtil.assertMachineOutputOk(output); - return [...rules, ...output.rules]; - }, - }), - spawnReifiers: assign({ - reifierMachineRefs: ({ - context: { - pluginRegistry, - fileManager: fm, - systemExecutor, - defaultExecutor, - smokerOptions: smokerOpts, + assignComponents: enqueueActions( + ( + {enqueue, context}, + { + pkgManagers, + reporters, + rules, + }: { + pkgManagers: PkgManager[]; + reporters: SomeReporter[]; + rules: SomeRule[]; }, - spawn, - }) => - Object.fromEntries( - pluginRegistry.plugins.map((plugin) => { - const id = `Reifier.${MachineUtil.makeId()}`; - const actor = spawn('ReifierMachine', { - id, - input: { - plugin, - pluginRegistry, - pkgManager: { - fm, - cwd: smokerOpts.cwd, - systemExecutor, - defaultExecutor, - }, - smokerOpts, - component: LoadableComponents.All, - }, - }); - - return [id, MachineUtil.monkeypatchActorLogger(actor, id)]; - }), - ), - }), - + ) => { + enqueue.assign({ + pkgManagers: [...(context.pkgManagers ?? []), ...pkgManagers], + reporters: [...(context.reporters ?? []), ...reporters], + rules: [...(context.rules ?? []), ...rules], + }); + }, + ), spawnLinterMachines: assign({ linterMachineRefs: ({ context: { @@ -464,12 +431,14 @@ export const ControlMachine = setup({ pkgManagers.map((pkgManager, index) => { const id = `LinterMachine.${MachineUtil.makeId()}`; + const manifests = lintManifests.get(pkgManager); + assert.ok(manifests); const actorRef = spawn('LinterMachine', { id, input: { pkgManager, ruleConfigs, - lintManifests: lintManifests.get(pkgManager)!, + lintManifests: manifests, fileManager, rules, parentRef: self, @@ -553,11 +522,17 @@ export const ControlMachine = setup({ }, }), sendPackingComplete: sendTo( - ({context: {installerMachineRef}}) => installerMachineRef!, + ({context: {installerMachineRef}}) => { + assert.ok(installerMachineRef); + return installerMachineRef; + }, {type: 'PACKING_COMPLETE'}, ), beginInstallation: sendTo( - ({context: {installerMachineRef}}) => installerMachineRef!, + ({context: {installerMachineRef}}) => { + assert.ok(installerMachineRef); + return installerMachineRef; + }, ( _, { @@ -590,7 +565,6 @@ export const ControlMachine = setup({ fileManager, shouldLint: false, pkgManagers: [], - reifierMachineRefs: {}, runnerMachineRefs: {}, reporterMachineRefs: {}, reporters: [], @@ -601,6 +575,7 @@ export const ControlMachine = setup({ totalChecks: 0, linterMachineRefs: {}, shouldHalt: false, + startTime: performance.now(), }), initial: 'loading', on: { @@ -646,6 +621,7 @@ export const ControlMachine = setup({ }; }, }, + {type: 'stopReporterMachine', params: ({event}) => event}, ], target: '#ControlMachine.done', }, @@ -660,85 +636,94 @@ export const ControlMachine = setup({ event: { output: {id}, }, - }) => `pkg manager machine ${id} exited gracefully`, + }) => `${id} exited gracefully`, ), + {type: 'stopReporterMachine', params: ({event}) => event}, + ], + }, + ], + + 'xstate.done.actor.PluginLoaderMachine': [ + { + guard: { + type: 'isMachineOutputOk', + params: ({event: {output}}) => output, + }, + actions: [ + {type: 'stopPluginLoaderMachine'}, + log('unloading plugin loader'), + ], + }, + { + guard: { + type: 'isMachineOutputNotOk', + params: ({event: {output}}) => output, + }, + actions: [ + { + type: 'assignError', + params: ({event: {output}}) => { + MachineUtil.assertMachineOutputNotOk(output); + return {error: output.error}; + }, + }, + {type: 'stopPluginLoaderMachine'}, + log('unloading plugin loader'), ], }, - {actions: [{type: 'stopReporterMachine', params: ({event}) => event}]}, ], }, states: { loading: { - entry: [log('loading plugin components...')], - initial: 'loadingPlugins', - states: { - loadingPlugins: { - entry: [{type: 'spawnReifiers'}], - on: { - 'xstate.done.actor.Reifier.*': [ - { - guard: { - type: 'isMachineOutputNotOk', - params: ({event: {output}}) => output, - }, - actions: [ - { - type: 'assignError', - params: ({event: {output}}) => { - MachineUtil.assertMachineOutputNotOk(output); - return {error: output.error}; - }, - }, - ], - target: '#ControlMachine.done', - }, - { - guard: { - type: 'isMachineOutputOk', - params: ({event: {output}}) => output, - }, - actions: [ - { - type: 'assignReifierResults', - params: ({event: {output}}) => output, - }, - ], - target: '#ControlMachine.loading.loadedPlugins', - }, - ], - }, - }, - loadedPlugins: { - invoke: { - src: 'setupPkgManagers', - input: ({context: {pkgManagers}}) => pkgManagers, - onDone: { - target: '#ControlMachine.loading.done', - }, - onError: { - actions: [ - { - type: 'assignError', - params: ({event: {error}}) => ({error}), - }, - ], - target: '#ControlMachine.done', + description: + 'Spawns a PluginLoaderMachine, which provides reified components', + entry: [ + log('loading plugin components...'), + + assign({ + pluginLoaderRef: ({ + self, + spawn, + context: { + pluginRegistry, + smokerOptions, + fileManager, + systemExecutor, + defaultExecutor, }, + }) => { + const input: PluginLoaderMachineInput = { + pluginRegistry, + smokerOptions, + fileManager, + systemExecutor, + defaultExecutor, + parentRef: self, + }; + const id = 'PluginLoaderMachine'; + const actorRef = spawn('PluginLoaderMachine', { + id, + input, + }); + return MachineUtil.monkeypatchActorLogger(actorRef, id); }, - entry: [ + }), + ], + on: { + COMPONENTS: { + actions: [ + { + type: 'assignComponents', + params: ({event}) => event, + }, + log('components loaded'), { type: 'spawnReporterMachines', }, ], - }, - done: { - type: 'final', + target: 'setup', }, }, - onDone: { - target: 'setup', - actions: [log('done loading components')], - }, }, setup: { entry: [ @@ -752,9 +737,75 @@ export const ControlMachine = setup({ opts: smokerOptions, }), }, - {type: 'assignSetupActors'}, + { + type: 'assignSetupActors', + }, ], on: { + PACK_OK: { + actions: [ + { + type: 'report', + params: ({ + context, + event: {manifests}, + }): Event.CtrlExternalEvent => { + return { + uniquePkgs: MachineUtil.uniquePkgNames(manifests), + type: SmokerEvent.PackOk, + pkgManagers: map(context.pkgManagers, 'staticSpec'), + manifests, + totalPkgs: manifests.length, + }; + }, + }, + { + type: 'sendPackingComplete', + }, + { + type: 'stopPackerMachine', + }, + ], + }, + PACK_FAILED: { + actions: [ + { + type: 'report', + params: ({ + context: { + pkgManagers, + smokerOptions: { + cwd, + all: allWorkspaces, + includeRoot: includeWorkspaceRoot, + workspace: workspaces, + }, + }, + event: {error}, + }): Event.CtrlExternalEvent => { + return { + error, + type: SmokerEvent.PackFailed, + packOptions: { + cwd, + allWorkspaces, + includeWorkspaceRoot, + workspaces, + }, + pkgManagers: map(pkgManagers, 'staticSpec'), + }; + }, + }, + { + type: 'assignError', + params: ({event: {error}}) => ({error}), + }, + { + type: 'stopPackerMachine', + }, + ], + target: '#ControlMachine.done', + }, 'xstate.done.actor.PackerMachine': [ { guard: { @@ -762,28 +813,13 @@ export const ControlMachine = setup({ params: ({event: {output}}) => output, }, actions: [ - { - type: 'report', - params: ({ - context, - event: {output}, - }): Event.CtrlExternalEvent => { - MachineUtil.assertMachineOutputOk(output); - return { - uniquePkgs: uniquePkgNames(output.manifests), - type: SmokerEvent.PackOk, - pkgManagers: map(context.pkgManagers, 'staticSpec'), - manifests: output.manifests, - totalPkgs: output.manifests.length, - }; - }, - }, - { - type: 'sendPackingComplete', - }, - { - type: 'stopPackerMachine', - }, + raise(({event: {output}}) => { + MachineUtil.assertMachineOutputOk(output); + return { + type: 'PACK_OK', + manifests: output.manifests, + }; + }), ], }, { @@ -792,117 +828,100 @@ export const ControlMachine = setup({ params: ({event: {output}}) => output, }, actions: [ - { - type: 'report', - params: ({ - context: { - pkgManagers, - smokerOptions: { - cwd, - all: allWorkspaces, - includeRoot: includeWorkspaceRoot, - workspace: workspaces, - }, - }, - event: {output}, - }): Event.CtrlExternalEvent => { - MachineUtil.assertMachineOutputNotOk(output); - return { - error: output.error, - type: SmokerEvent.PackFailed, - packOptions: { - cwd, - allWorkspaces, - includeWorkspaceRoot, - workspaces, - }, - pkgManagers: map(pkgManagers, 'staticSpec'), - }; - }, - }, - { - type: 'assignError', - params: ({event: {output}}) => { - MachineUtil.assertMachineOutputNotOk(output); - return {error: output.error}; - }, - }, - { - type: 'stopPackerMachine', - }, + raise(({event: {output}}) => { + MachineUtil.assertMachineOutputNotOk(output); + return { + type: 'PACK_FAILED', + error: output.error, + }; + }), ], - target: '#ControlMachine.done', }, ], + INSTALL_OK: { + actions: [ + { + type: 'report', + params: ({ + context: {pkgManagers}, + event: {manifests}, + }): Event.CtrlExternalEvent => { + const [additionalDeps, pkgs] = partition( + manifests, + 'isAdditional', + ); + const uniquePkgs: string[] = MachineUtil.uniquePkgNames(pkgs); + const uniqueAdditionalDeps = + MachineUtil.uniquePkgNames(additionalDeps); + return { + type: SmokerEvent.InstallOk, + uniquePkgs, + pkgManagers: map(pkgManagers, 'staticSpec'), + manifests, + totalPkgs: manifests.length, + additionalDeps: uniqueAdditionalDeps, + }; + }, + }, + { + type: 'stopInstallerMachine', + }, + ], + }, + INSTALL_FAILED: { + actions: [ + { + type: 'report', + params: ({ + context: {pkgManagers}, + event: {error}, + }): Event.CtrlExternalEvent => { + return { + error, + type: SmokerEvent.InstallFailed, + ...buildInstallEventData(pkgManagers), + }; + }, + }, + { + type: 'assignError', + params: ({event: {error}}) => ({error}), + }, + { + type: 'stopInstallerMachine', + }, + ], + target: '#ControlMachine.done', + }, 'xstate.done.actor.InstallerMachine': [ { guard: { - type: 'isMachineOutputNotOk', + type: 'isMachineOutputOk', params: ({event: {output}}) => output, }, actions: [ - { - type: 'report', - params: ({ - context: {pkgManagers}, - event: {output}, - }): Event.CtrlExternalEvent< - typeof SmokerEvent.InstallFailed - > => { - MachineUtil.assertMachineOutputNotOk(output); - return { - error: output.error, - type: SmokerEvent.InstallFailed, - ...buildInstallEventData(pkgManagers), - }; - }, - }, - { - type: 'assignError', - params: ({event: {output}}) => { - MachineUtil.assertMachineOutputNotOk(output); - return {error: output.error}; - }, - }, - { - type: 'stopInstallerMachine', - }, + raise(({event: {output}}) => { + MachineUtil.assertMachineOutputOk(output); + return { + type: 'INSTALL_OK', + manifests: output.manifests, + }; + }), ], - target: '#ControlMachine.done', }, { guard: { - type: 'isMachineOutputOk', + type: 'isMachineOutputNotOk', params: ({event: {output}}) => output, }, actions: [ - { - type: 'report', - params: ({ - context: {pkgManagers}, - event: {output}, - }): Event.CtrlExternalEvent => { - MachineUtil.assertMachineOutputOk(output); - const {manifests} = output; - const [additionalDeps, pkgs] = partition( - manifests, - 'isAdditional', - ); - const uniquePkgs: string[] = uniquePkgNames(pkgs); - const uniqueAdditionalDeps = uniquePkgNames(additionalDeps); - return { - type: SmokerEvent.InstallOk, - uniquePkgs, - pkgManagers: map(pkgManagers, 'staticSpec'), - manifests, - totalPkgs: manifests.length, - additionalDeps: uniqueAdditionalDeps, - }; - }, - }, - { - type: 'stopInstallerMachine', - }, + raise(({event: {output}}) => { + MachineUtil.assertMachineOutputNotOk(output); + return { + type: 'INSTALL_FAILED', + error: output.error, + }; + }), ], }, ], @@ -964,20 +983,18 @@ export const ControlMachine = setup({ event: {index, pkgManager}, }): Event.CtrlExternalEvent< typeof SmokerEvent.PkgManagerPackBegin - > => { - return { - type: SmokerEvent.PkgManagerPackBegin, - currentPkgManager: index, - pkgManager: pkgManager.staticSpec, - packOptions: { - cwd, - allWorkspaces, - includeWorkspaceRoot, - workspaces, - }, - totalPkgManagers: pkgManagers.length, - }; - }, + > => ({ + type: SmokerEvent.PkgManagerPackBegin, + currentPkgManager: index, + pkgManager: pkgManager.staticSpec, + packOptions: { + cwd, + allWorkspaces, + includeWorkspaceRoot, + workspaces, + }, + totalPkgManagers: pkgManagers.length, + }), }, ], }, @@ -1000,50 +1017,46 @@ export const ControlMachine = setup({ }, ], }, - PKG_MANAGER_PACK_OK: [ - { - guard: ({context: {installerMachineRef}}) => - Boolean(installerMachineRef), - actions: [ - { - type: 'report', - params: ({ - context: { - pkgManagers, - smokerOptions: { - cwd, - all: allWorkspaces, - includeRoot: includeWorkspaceRoot, - workspace: workspaces, - }, - }, - event: {index, pkgManager, installManifests}, - }): Event.CtrlExternalEvent< - typeof SmokerEvent.PkgManagerPackOk - > => ({ - type: SmokerEvent.PkgManagerPackOk, - currentPkgManager: index, - pkgManager: pkgManager.staticSpec, - packOptions: { - allWorkspaces, + PKG_MANAGER_PACK_OK: { + actions: [ + { + type: 'report', + params: ({ + context: { + pkgManagers, + smokerOptions: { cwd, - includeWorkspaceRoot, - workspaces, + all: allWorkspaces, + includeRoot: includeWorkspaceRoot, + workspace: workspaces, }, - manifests: installManifests, - totalPkgManagers: pkgManagers.length, - }), - }, - { - type: 'beginInstallation', - params: ({event: {pkgManager, installManifests}}) => ({ - pkgManager, - installManifests, - }), - }, - ], - }, - ], + }, + event: {index, pkgManager, installManifests}, + }): Event.CtrlExternalEvent< + typeof SmokerEvent.PkgManagerPackOk + > => ({ + type: SmokerEvent.PkgManagerPackOk, + currentPkgManager: index, + pkgManager: pkgManager.staticSpec, + packOptions: { + allWorkspaces, + cwd, + includeWorkspaceRoot, + workspaces, + }, + manifests: installManifests, + totalPkgManagers: pkgManagers.length, + }), + }, + { + type: 'beginInstallation', + params: ({event: {pkgManager, installManifests}}) => ({ + pkgManager, + installManifests, + }), + }, + ], + }, PKG_MANAGER_PACK_FAILED: { actions: [ { @@ -1082,27 +1095,25 @@ export const ControlMachine = setup({ ], target: '#ControlMachine.done', }, - PKG_MANAGER_INSTALL_OK: [ - { - actions: [ - { - type: 'report', - params: ({ - context: {pkgManagers}, - event: {index, pkgManager, installManifests}, - }): Event.CtrlExternalEvent< - typeof SmokerEvent.PkgManagerInstallOk - > => ({ - type: SmokerEvent.PkgManagerInstallOk, - manifests: installManifests, - currentPkgManager: index, - pkgManager: pkgManager.staticSpec, - totalPkgManagers: pkgManagers.length, - }), - }, - ], - }, - ], + PKG_MANAGER_INSTALL_OK: { + actions: [ + { + type: 'report', + params: ({ + context: {pkgManagers}, + event: {index, pkgManager, installManifests}, + }): Event.CtrlExternalEvent< + typeof SmokerEvent.PkgManagerInstallOk + > => ({ + type: SmokerEvent.PkgManagerInstallOk, + manifests: installManifests, + currentPkgManager: index, + pkgManager: pkgManager.staticSpec, + totalPkgManagers: pkgManagers.length, + }), + }, + ], + }, PKG_MANAGER_INSTALL_FAILED: { actions: [ { @@ -1344,7 +1355,8 @@ export const ControlMachine = setup({ currentPkgManager, totalPkgManagers: pkgManagers.length, totalUniqueScripts: scripts.length, - totalUniquePkgs: uniquePkgNames(manifests).length, + totalUniquePkgs: + MachineUtil.uniquePkgNames(manifests).length, }; }, }, @@ -1375,7 +1387,8 @@ export const ControlMachine = setup({ currentPkgManager: pkgManagerIndex, totalPkgManagers: pkgManagers.length, totalUniqueScripts: scripts.length, - totalUniquePkgs: uniquePkgNames(manifests).length, + totalUniquePkgs: + MachineUtil.uniquePkgNames(manifests).length, }; }, }, @@ -1403,6 +1416,7 @@ export const ControlMachine = setup({ | typeof SmokerEvent.RunScriptsOk | typeof SmokerEvent.RunScriptsFailed > => { + assert.ok(runScriptResults); const [failedResults, otherResults] = partition( runScriptResults, 'error', @@ -1445,7 +1459,7 @@ export const ControlMachine = setup({ totalUniqueScripts: scripts.length, totalUniquePkgs: pkgNames.size, totalPkgManagers: pkgManagers.length, - results: runScriptResults!, + results: runScriptResults, }; }, }, @@ -1656,13 +1670,14 @@ export const ControlMachine = setup({ }): Event.CtrlExternalEvent< typeof SmokerEvent.LintOk | typeof SmokerEvent.LintFailed > => { - const type = isEmpty(lintResult!.issues) + assert.ok(lintResult); + const type = isEmpty(lintResult.issues) ? SmokerEvent.LintOk : SmokerEvent.LintFailed; return { type, - result: lintResult!, + result: lintResult, totalPkgManagers: pkgManagers.length, config: smokerOptions.rules, totalRules, @@ -1684,9 +1699,9 @@ export const ControlMachine = setup({ }, }, done: { - initial: 'cleanup', + initial: 'flushReporters', states: { - cleanup: { + flushReporters: { entry: [ log('cleaning up...'), { @@ -1694,16 +1709,33 @@ export const ControlMachine = setup({ params: {type: SmokerEvent.BeforeExit}, }, { - type: 'cleanup', + type: 'flushReporters', }, ], always: [ { - guard: {type: 'notHasError'}, + guard: and([ + 'notHasError', + 'hasPluginLoaderRef', + not('hasReporterRefs'), + ]), + target: '#ControlMachine.done.teardown', + }, + { + guard: and(['hasError', not('hasPluginLoaderRef')]), + target: '#ControlMachine.done.errored', + }, + ], + }, + teardown: { + entry: [{type: 'teardown'}], + always: [ + { + guard: and(['notHasError', not('hasPluginLoaderRef')]), target: '#ControlMachine.done.complete', }, { - guard: {type: 'hasError'}, + guard: and(['hasError', not('hasPluginLoaderRef')]), target: '#ControlMachine.done.errored', }, ], @@ -1713,7 +1745,12 @@ export const ControlMachine = setup({ type: 'final', }, complete: { - entry: [log('complete')], + entry: [ + log(({context: {startTime}}) => { + const sec = ((performance.now() - startTime) / 1000).toFixed(2); + return `complete in ${sec}s`; + }), + ], type: 'final', }, }, @@ -1738,7 +1775,7 @@ const buildInstallEventData = memoize( const additionalDeps = uniq( map(filter(manifests, {isAdditional: true}), 'pkgName'), ); - const uniquePkgs = uniquePkgNames(manifests); + const uniquePkgs = MachineUtil.uniquePkgNames(manifests); const specs = map(pkgManagers, 'staticSpec'); return Object.freeze({ diff --git a/packages/midnight-smoker/src/machine/index.ts b/packages/midnight-smoker/src/machine/index.ts index f238f53f..a1a9d0cd 100644 --- a/packages/midnight-smoker/src/machine/index.ts +++ b/packages/midnight-smoker/src/machine/index.ts @@ -1,4 +1,4 @@ -export * from './machine-util'; +export * from './util'; export * from './runner'; diff --git a/packages/midnight-smoker/src/machine/installer/install-machine.ts b/packages/midnight-smoker/src/machine/installer/install-machine.ts index 88a8dccd..4a777ff5 100644 --- a/packages/midnight-smoker/src/machine/installer/install-machine.ts +++ b/packages/midnight-smoker/src/machine/installer/install-machine.ts @@ -1,3 +1,4 @@ +import {type MachineOutputError, type MachineOutputOk} from '#machine/util'; import {type InstallError, type PkgManager} from '#pkg-manager'; import { type ExecResult, @@ -12,7 +13,6 @@ import { setup, type AnyActorRef, } from 'xstate'; -import {type MachineOutputError, type MachineOutputOk} from '../machine-util'; import {type InstallerMachinePkgManagerInstallBeginEvent} from './installer-machine-events'; export interface InstallMachineInput { @@ -47,14 +47,10 @@ export type InstallActorParams = Pick< export type InstallMachineOutput = InstallMachineOk | InstallMachineError; -const installActor = fromPromise( +export const installActor = fromPromise( async ({ input: {signal, pkgManager, installManifests}, - }): Promise => { - const result = await pkgManager.install(installManifests, signal); - - return result; - }, + }): Promise => pkgManager.install(installManifests, signal), ); export const InstallMachine = setup({ @@ -67,9 +63,6 @@ export const InstallMachine = setup({ installOk: assign({ rawResult: (_, rawResult: ExecResult) => rawResult, }), - // installFailed: assign({ - // error: (_, error: unknown) => error as InstallError, - // }), sendInstallBegin: sendTo( ({context: {parentRef}}) => parentRef, ({context}): InstallerMachinePkgManagerInstallBeginEvent => { diff --git a/packages/midnight-smoker/src/machine/installer/installer-machine.ts b/packages/midnight-smoker/src/machine/installer/installer-machine.ts index ee3a82d3..158826f7 100644 --- a/packages/midnight-smoker/src/machine/installer/installer-machine.ts +++ b/packages/midnight-smoker/src/machine/installer/installer-machine.ts @@ -1,3 +1,9 @@ +import { + type CtrlInstallBeginEvent, + type CtrlPkgManagerInstallBeginEvent, + type CtrlPkgManagerInstallFailedEvent, + type CtrlPkgManagerInstallOkEvent, +} from '#machine/controller'; import { type InstallError, type InstallManifest, @@ -15,12 +21,7 @@ import { type ActorRefFrom, type AnyActorRef, } from 'xstate'; -import { - type CtrlInstallBeginEvent, - type CtrlPkgManagerInstallBeginEvent, - type CtrlPkgManagerInstallFailedEvent, - type CtrlPkgManagerInstallOkEvent, -} from '../controller/control-machine-events'; +import {type PackResult} from '../packer/packer-machine'; import { assertMachineOutputNotOk, assertMachineOutputOk, @@ -31,8 +32,7 @@ import { type MachineOutputError, type MachineOutputLike, type MachineOutputOk, -} from '../machine-util'; -import {type PackResult} from '../packer/packer-machine'; +} from '../util'; import { InstallMachine, type InstallMachineError, diff --git a/packages/midnight-smoker/src/machine/linter/linter-machine.ts b/packages/midnight-smoker/src/machine/linter/linter-machine.ts index 22c5a499..63f9a688 100644 --- a/packages/midnight-smoker/src/machine/linter/linter-machine.ts +++ b/packages/midnight-smoker/src/machine/linter/linter-machine.ts @@ -33,7 +33,7 @@ import { monkeypatchActorLogger, type MachineOutputError, type MachineOutputOk, -} from '../machine-util'; +} from '../util'; import {RuleMachine, type RuleMachineOutput} from './rule-machine'; export interface LinterMachineInput { @@ -140,10 +140,11 @@ export const LinterMachine = setup({ context: {rules, ruleConfigs, lintManifestsWithPkgs, pkgManager}, }): Readonly[] => lintManifestsWithPkgs.flatMap( - ({pkgName, installPath, pkgJson, pkgJsonPath}) => + ({pkgName, installPath, pkgJson, pkgJsonPath, localPath}) => rules.map((rule) => { const {severity} = ruleConfigs[rule.name]; return RuleContext.create(rule, { + localPath, pkgName, severity, installPath, @@ -184,13 +185,14 @@ export const LinterMachine = setup({ {self, context: {pkgManager, ruleConfigs}}, { issues, - ctx: {ruleName, pkgName, installPath}, + ctx: {ruleName, pkgName, installPath, localPath}, index: currentRule, }: RuleMachineOutput, ): CtrlRuleFailedEvent => ({ pkgManager: pkgManager.staticSpec, pkgName, installPath, + localPath, config: ruleConfigs[ruleName], rule: ruleName, currentRule, @@ -204,7 +206,7 @@ export const LinterMachine = setup({ ( {self, context: {pkgManager, ruleConfigs}}, { - ctx: {ruleName, pkgName, installPath}, + ctx: {ruleName, pkgName, installPath, localPath}, index: currentRule, }: RuleMachineOutput, ): CtrlRuleOkEvent => ({ @@ -216,6 +218,7 @@ export const LinterMachine = setup({ currentRule, type: 'RULE_OK', sender: self.id, + localPath, }), ), sendRuleBegin: sendTo( @@ -224,7 +227,7 @@ export const LinterMachine = setup({ {self, context: {pkgManager, ruleConfigs}}, { currentRule, - ctx: {pkgName, installPath, ruleName}, + ctx: {pkgName, installPath, ruleName, localPath}, }: {currentRule: number; ctx: StaticRuleContext}, ): CtrlRuleBeginEvent => ({ pkgName, @@ -235,6 +238,7 @@ export const LinterMachine = setup({ currentRule, type: 'RULE_BEGIN', sender: self.id, + localPath, }), ), sendPkgManagerLintBegin: sendTo( @@ -273,10 +277,10 @@ export const LinterMachine = setup({ readPkgJsons: fromPromise( async ({input: {fileManager, lintManifests}}) => Promise.all( - lintManifests.map(async ({installPath, pkgName}) => { + lintManifests.map(async ({installPath, pkgName, localPath}) => { const {packageJson: pkgJson, path: pkgJsonPath} = await fileManager.findPkgUp(installPath, {strict: true}); - return {installPath, pkgName, pkgJson, pkgJsonPath}; + return {installPath, pkgName, pkgJson, pkgJsonPath, localPath}; }), ), ), diff --git a/packages/midnight-smoker/src/machine/linter/rule-machine.ts b/packages/midnight-smoker/src/machine/linter/rule-machine.ts index 09ed24cb..11993c77 100644 --- a/packages/midnight-smoker/src/machine/linter/rule-machine.ts +++ b/packages/midnight-smoker/src/machine/linter/rule-machine.ts @@ -15,7 +15,7 @@ import { type AnyActorRef, } from 'xstate'; import {type LinterMachineRuleBeginEvent} from '.'; -import {type MachineOutputOk} from '../machine-util'; +import {type MachineOutputOk} from '../util'; export interface RuleMachineInput { ctx: Readonly; diff --git a/packages/midnight-smoker/src/machine/machine-util.ts b/packages/midnight-smoker/src/machine/machine-util.ts deleted file mode 100644 index cea89787..00000000 --- a/packages/midnight-smoker/src/machine/machine-util.ts +++ /dev/null @@ -1,89 +0,0 @@ -import Debug from 'debug'; -import {map, memoize, uniqBy} from 'lodash'; -import {AssertionError} from 'node:assert'; -import {type AnyActorRef} from 'xstate'; -import {MIDNIGHT_SMOKER} from '../constants'; - -export function makeId() { - return Math.random().toString(36).substring(7); -} - -export function monkeypatchActorLogger( - actor: T, - namespace: string, -): T { - // https://github.com/statelyai/xstate/issues/4634 - // @ts-expect-error private - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - actor.logger = actor._actorScope.logger = Debug( - `${MIDNIGHT_SMOKER}:${namespace}`, - ); - return actor; -} - -export interface MachineOutputLike { - id: string; - type: string; -} - -export type MachineOutputOk = { - type: 'OK'; - id: string; -} & Ctx; - -export type MachineOutputError< - Err extends Error = Error, - Ctx extends object = object, -> = { - id: string; - type: 'ERROR'; - error: Err; -} & Ctx; - -export type MachineOutput< - Ok extends object = object, - Err extends Error = Error, -> = MachineOutputOk | MachineOutputError; - -export function isMachineOutputOk< - Ok extends object = object, - Err extends Error = Error, ->(output: MachineOutput): output is MachineOutputOk { - return output.type === 'OK'; -} - -export function isMachineOutputNotOk< - Ok extends object = object, - Err extends Error = Error, ->(output: MachineOutput): output is MachineOutputError { - return output.type === 'ERROR'; -} - -export function assertMachineOutputOk< - Ok extends object = object, - Err extends Error = Error, ->(output: MachineOutput): asserts output is MachineOutputOk { - if (isMachineOutputNotOk(output)) { - throw new AssertionError({ - message: 'Unexpected error in machine output', - actual: output, - }); - } -} - -export function assertMachineOutputNotOk< - Ok extends object = object, - Err extends Error = Error, ->(output: MachineOutput): asserts output is MachineOutputError { - if (isMachineOutputOk(output)) { - throw new AssertionError({ - message: 'Expected an error in machine output', - actual: output, - }); - } -} - -export const uniquePkgNames = memoize( - (manifests: {pkgName: string}[]): string[] => - map(uniqBy(manifests, 'pkgName'), 'pkgName'), -); diff --git a/packages/midnight-smoker/src/machine/packer/pack-machine.ts b/packages/midnight-smoker/src/machine/packer/pack-machine.ts index 622754c3..b1b50dd9 100644 --- a/packages/midnight-smoker/src/machine/packer/pack-machine.ts +++ b/packages/midnight-smoker/src/machine/packer/pack-machine.ts @@ -1,7 +1,4 @@ -import { - type MachineOutputError, - type MachineOutputOk, -} from '#machine/machine-util'; +import {type MachineOutputError, type MachineOutputOk} from '#machine/util'; import { type PackError, type PackOptions, diff --git a/packages/midnight-smoker/src/machine/packer/packer-machine.ts b/packages/midnight-smoker/src/machine/packer/packer-machine.ts index 11a5d044..da8d198d 100644 --- a/packages/midnight-smoker/src/machine/packer/packer-machine.ts +++ b/packages/midnight-smoker/src/machine/packer/packer-machine.ts @@ -1,3 +1,21 @@ +import { + type CtrlInstallBeginEvent, + type CtrlPackBeginEvent, + type CtrlPkgManagerPackBeginEvent, + type CtrlPkgManagerPackFailedEvent, + type CtrlPkgManagerPackOkEvent, +} from '#machine/controller'; +import { + assertMachineOutputNotOk, + assertMachineOutputOk, + isMachineOutputNotOk, + isMachineOutputOk, + makeId, + monkeypatchActorLogger, + type MachineOutputError, + type MachineOutputLike, + type MachineOutputOk, +} from '#machine/util'; import { type PackError, type PackOptions, @@ -15,24 +33,6 @@ import { type ActorRefFrom, type AnyActorRef, } from 'xstate'; -import { - type CtrlInstallBeginEvent, - type CtrlPackBeginEvent, - type CtrlPkgManagerPackBeginEvent, - type CtrlPkgManagerPackFailedEvent, - type CtrlPkgManagerPackOkEvent, -} from '../controller/control-machine-events'; -import { - assertMachineOutputNotOk, - assertMachineOutputOk, - isMachineOutputNotOk, - isMachineOutputOk, - makeId, - monkeypatchActorLogger, - type MachineOutputError, - type MachineOutputLike, - type MachineOutputOk, -} from '../machine-util'; import { PackMachine, type PackMachineOutputError, diff --git a/packages/midnight-smoker/src/machine/plugin-loader/index.ts b/packages/midnight-smoker/src/machine/plugin-loader/index.ts new file mode 100644 index 00000000..39830703 --- /dev/null +++ b/packages/midnight-smoker/src/machine/plugin-loader/index.ts @@ -0,0 +1 @@ +export * from './plugin-loader-machine'; diff --git a/packages/midnight-smoker/src/machine/plugin-loader/plugin-loader-machine.ts b/packages/midnight-smoker/src/machine/plugin-loader/plugin-loader-machine.ts new file mode 100644 index 00000000..05bcd627 --- /dev/null +++ b/packages/midnight-smoker/src/machine/plugin-loader/plugin-loader-machine.ts @@ -0,0 +1,520 @@ +import {fromUnknownError} from '#error'; +import {type CtrlComponentsEvent} from '#machine/controller'; +import { + LoadableComponents, + ReifierMachine, + type ReifierOutput, +} from '#machine/reifier'; +import { + assertMachineOutputNotOk, + assertMachineOutputOk, + isMachineOutputNotOk, + isMachineOutputOk, + makeId, + monkeypatchActorLogger, + type MachineOutput, + type MachineOutputError, + type MachineOutputOk, +} from '#machine/util'; +import {type SmokerOptions} from '#options'; +import {type PkgManager} from '#pkg-manager'; +import {type PluginRegistry} from '#plugin'; +import {type SomeReporter} from '#reporter'; +import {type Executor, type SomeRule} from '#schema'; +import {type FileManager} from '#util'; +import { + assign, + enqueueActions, + fromPromise, + log, + not, + sendTo, + setup, + type ActorRefFrom, + type AnyActorRef, +} from 'xstate'; + +export interface PluginLoaderMachineInput { + pluginRegistry: PluginRegistry; + fileManager: FileManager; + systemExecutor: Executor; + defaultExecutor: Executor; + smokerOptions: SmokerOptions; + parentRef: AnyActorRef; +} + +export type PluginLoaderMachineOutputOk = MachineOutputOk; + +export type PluginLoaderMachineOutputError = MachineOutputError; + +export type PluginLoaderMachineOutput = + | PluginLoaderMachineOutputOk + | PluginLoaderMachineOutputError; + +export interface PluginLoaderMachineContext extends PluginLoaderMachineInput { + error?: Error; + reifierMachineRefs: Record>; + pkgManagers?: PkgManager[]; + reporters?: SomeReporter[]; + rules?: SomeRule[]; +} + +export interface SendComponentsParams { + pkgManagers?: PkgManager[]; + reporters?: SomeReporter[]; + rules?: SomeRule[]; +} + +export interface PluginLoaderReifierDoneEvent { + output: ReifierOutput; + type: 'xstate.done.actor.ReifierMachine.*'; +} + +export interface PluginLoaderTeardownEvent { + type: 'TEARDOWN'; +} + +export type PluginLoaderEvents = + | PluginLoaderReifierDoneEvent + | PluginLoaderTeardownEvent; + +export interface AssignReifiedComponentsParams { + pkgManagers: PkgManager[]; + reporters: SomeReporter[]; + rules: SomeRule[]; +} + +/** + * Executes the {@link PkgManager.teardown} method on all package managers. + */ +const teardownPkgManagers = fromPromise( + async ({input: pkgManagers}): Promise => { + await Promise.all(pkgManagers.map((pkgManager) => pkgManager.teardown())); + }, +); + +/** + * Executes the {@link PkgManager.setup} method on all package managers. + */ +const setupPkgManagers = fromPromise( + async ({input: pkgManagers}): Promise => { + await Promise.all(pkgManagers.map((pkgManager) => pkgManager.setup())); + }, +); + +/** + * Executes the {@link Reporter.setup} method on all reporters. + */ +const setupReporters = fromPromise( + async ({input: reporters}): Promise => { + await Promise.all(reporters.map((reporter) => reporter.setup())); + }, +); + +/** + * Executes the {@link Reporter.teardown} method on all reporters. + */ +const teardownReporters = fromPromise( + async ({input: reporters}): Promise => { + await Promise.all(reporters.map((reporter) => reporter.teardown())); + }, +); + +/** + * This is mainly here to make `ControlMachine` smaller. + * + * When loading, `ControlMachine` will spawn one of these. It reifies (via _n_ + * {@link ReifierMachine ReifierMachines}) all package managers, enabled rules, + * and enabled reporters. It will then emit a {@link CtrlComponentsEvent} back to + * `ControlMachine` with the reified components. + * + * It stays alive until `ControlMachine` sends a `TEARDOWN` event, which is + * relayed to the components. + * + * TODO: Currently it only tears down the package managers; it should also tear + * down reporters. Rules do not not have a lifecycle + */ +export const PluginLoaderMachine = setup({ + types: { + input: {} as PluginLoaderMachineInput, + context: {} as PluginLoaderMachineContext, + events: {} as PluginLoaderEvents, + output: {} as PluginLoaderMachineOutput, + }, + actors: { + ReifierMachine, + setupPkgManagers, + teardownPkgManagers, + setupReporters, + teardownReporters, + }, + actions: { + /** + * Assigns reified components to the context for setup/teardown lifecycle + * events. + * + * This action intentionally omits rules, since they do not have a + * lifecycle. + */ + assignReifiedComponents: assign({ + pkgManagers: ( + {context: {pkgManagers = []}}, + {pkgManagers: newPkgManagers}: AssignReifiedComponentsParams, + ) => [...pkgManagers, ...newPkgManagers], + reporters: ( + {context: {reporters = []}}, + {reporters: newReporters}: AssignReifiedComponentsParams, + ) => [...reporters, ...newReporters], + rules: ( + {context: {rules = []}}, + {rules: newRules}: AssignReifiedComponentsParams, + ) => [...rules, ...newRules], + }), + + /** + * Spawns a {@link ReifierMachine} for each plugin + */ + spawnReifiers: assign({ + reifierMachineRefs: ({ + context: { + pluginRegistry, + fileManager: fm, + systemExecutor, + defaultExecutor, + smokerOptions: smokerOpts, + }, + spawn, + }) => + Object.fromEntries( + pluginRegistry.plugins.map((plugin) => { + const id = `ReifierMachine.${makeId()}`; + const actor = spawn('ReifierMachine', { + id, + input: { + plugin, + pluginRegistry, + pkgManager: { + fm, + cwd: smokerOpts.cwd, + systemExecutor, + defaultExecutor, + }, + smokerOpts, + component: LoadableComponents.All, + }, + }); + + return [id, monkeypatchActorLogger(actor, id)]; + }), + ), + }), + + /** + * Stops a given {@link ReifierMachine} + */ + stopReifier: enqueueActions( + ({enqueue, context: {reifierMachineRefs}}, id: string) => { + enqueue.stopChild(id); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {[id]: _, ...rest} = reifierMachineRefs; + enqueue.assign({ + reifierMachineRefs: rest, + }); + }, + ), + + /** + * Assigns an error to the context + */ + assignError: assign({ + // TODO: aggregate for multiple + error: ({context}, {error}: {error?: unknown}): Error | undefined => + error ? fromUnknownError(error) : context.error, + }), + + /** + * Sends component data to the parent actor + */ + sendComponents: sendTo( + ({context: {parentRef}}) => parentRef, + ( + {self}, + {pkgManagers = [], reporters = [], rules = []}: SendComponentsParams, + ): CtrlComponentsEvent => ({ + type: 'COMPONENTS', + pkgManagers, + reporters, + rules, + sender: self.id, + }), + ), + }, + guards: { + hasError: ({context: {error}}) => Boolean(error), + notHasError: not('hasError'), + isMachineOutputOk: (_, output: MachineOutput) => isMachineOutputOk(output), + isMachineOutputNotOk: (_, output: MachineOutput) => + isMachineOutputNotOk(output), + }, +}).createMachine({ + /** + * @xstate-layout N4IgpgJg5mDOIC5QAUA2BXKBLAdgGQHsBDCMAJwFkiBjAC1zADpViJcoBiAD1gBcjeTCARxMavAmUYAlMFgBmWclToNGAKgDaABgC6iUAAcCsLLywiDILogBsAJgA0IAJ6IArAE4AjI2+eHAGZA+08AFjDA7wB2AF9Y5zRMXEISZRp6UWZWdm4+ASERMWoJKVkFJUoMtS1vfSQQY1NzSwabBDD7AA5GWzDtW21orqj7KLDnNwR7MNtGd0DPJfdohy6uz0DbeMSMbHxWdNUs2DBedEMOYRP+QUYk-dTSKuOmU-PDHXqjEzMLHCs7W82n6jE8XUitlsSy69m0nmik0QnlCjAh0XstkCQ1s3gh9h2IAeKUOL0ybzOFyuRUY+TuxIOaTJaneF00dSsTT+rVAQJB2jBEK20PBcIRSIQ3kxvjC4OxCIi3S6hIZTyO5NplMMjDIYGMZEEZFgmo+sn1htg1KyuAAbgQANZMVWklQa1na3Xm8jG91myQWhC2gjUAT-L5fTm-FoAtqIQLRXwDGbhBPabH2bwS4EJ+baLoY4JjQLuZUJIl7ElM10srU6vX+70mi5+g3ejjkMiSRiGVACeSSAC29wrjOe1ZOtc9DaNTcMLYDQZD0fDekjzX+gLjOaTsrCqfTmdcyPcvi67hmDmiQ28fTiZedVeqE4+3ftUCoOCIMBn7uQb4-X5ttcTBBo6w7JKO6o1i+hj-kQn7fj6Wp-u+8GAUagY4HaS5hnoEYNFy0abggiyBGCSwIkMKwYl0thZpEPQzFEaaeNowKse4KojmqzLPhcr6oQhja-nBQlGu2ZCdlIPZ9oO4GPC6T4UjBonoUhHwoQBiGYdhoYiCu3yNFGG6xiRmzkUsV7RNR3R0UekpRD0Ja2EK3RhBs0RhFxEE8eOTC6iQLgcAAKgAogAgtIAAiADyADqABy+E-OuPLWIgMR4n4-S2F4uJ9LY0SIvZATRH4WzBJsGzots97cYpryMIIRBkMIADuOBWm8txOvVj6Nc1rUEB1yVGalMa8ogcLRJ4jCjOeIIRO5h5TJs7i5vmYzuCW3jOd5Cn9Rqg3tZ1wG0j18mVmOSlNWALUneyhmESZk3TEMs3zXCS3LVmbHrfi2grLRWKrJ48RljgBCkPADQPtdrxrtyE3pQgAC09jFVMGJoi5SaBLC2Jpt4+1XVBWQsCQ7CI0RpmdBK9gzHNuM7WEJ4bLlJOQbxykXNTL0oxmEouWVZ4zFesJeJ5nO+Td7p1l6Rp82l7TQhKXT+Gi56zIVULuNCXl1T5DVupO9atj+Wrzt6SvI0CMQSp4JbzN4pEOOCV5RNLxvQfxU7m8awE28RKxkWemzWf4ybeBMJXDM7izAi76zZl7h0+9qsGCWpQemar9neHiczOeigQROmqfwybKlZ4hs6aWhiE569BeYxlITrTMIw3vmfTFrVuxG2nfEZ6pteBwRxnK3YGvRKX8ZsXmAwrFmpcd+5oTXrlkQGwPB2V2oAUQFMKVI8R0cZoKDGO3rN7uQ7Ll+NZq8Zo7wScYbe9k0wx3Dcjz1T5KCEs1WJYjTGEaOnRwhqxzDEBY6xFh60KhXL+t17q-wElpa2E9xpnw8mCAY2JS4QNCDHKYwI0yP3cJ0BMes5R3l3qTbmqChodXltOGGJ8abNx8GRROgw0znnCBEX6uJehPyhOAma4xkFMI7JISATcBZbHWsMKU+UZqeVnlmMYZVdbaH0WMaOeYd7lkHvvLI49OH83aGMcEjAPYFxcnrWULkHYFzEYI8EiD1ilniEAA + */ + context: ({input}) => ({...input, reifierMachineRefs: {}}), + initial: 'loading', + id: 'PluginLoaderMachine', + description: + 'This gets reified components from each plugin and hands them to the parent machine', + states: { + loading: { + description: 'Spawns a reifier for each plugin and waits for completion', + entry: [{type: 'spawnReifiers'}], + on: { + 'xstate.done.actor.ReifierMachine.*': [ + { + guard: { + type: 'isMachineOutputNotOk', + params: ({event: {output}}) => output, + }, + actions: [ + { + type: 'assignError', + params: ({event: {output}}) => { + assertMachineOutputNotOk(output); + return {error: output.error}; + }, + }, + { + type: 'stopReifier', + params: ({event: {output}}) => output.id, + }, + ], + target: 'done', + }, + { + guard: { + type: 'isMachineOutputOk', + params: ({event: {output}}) => output, + }, + actions: [ + { + type: 'assignReifiedComponents', + params: ({event: {output}}) => { + assertMachineOutputOk(output); + return output; + }, + }, + { + type: 'stopReifier', + params: ({event: {output}}) => output.id, + }, + ], + target: '#PluginLoaderMachine.setup', + }, + ], + }, + }, + setup: { + description: 'Runs the "setup" lifecycle event for reified components', + type: 'parallel', + states: { + reporters: { + description: 'Runs the "setup" lifecyle event for reified Reporters', + initial: 'setupReporters', + states: { + setupReporters: { + description: 'Executes the setupReporters actor', + entry: [log('lifecycle: setup reporters')], + invoke: { + src: 'setupReporters', + input: ({context: {reporters = []}}) => reporters, + onDone: { + actions: [log('lifecycle: setup reporters done')], + target: '#PluginLoaderMachine.setup.reporters.done', + }, + onError: { + actions: [ + log(({event: {error}}) => error), + { + type: 'assignError', + params: ({event: {error}}) => ({error}), + }, + ], + target: '#PluginLoaderMachine.setup.reporters.done', + }, + }, + }, + done: { + description: 'Reporters done setting up (with or without error)', + type: 'final', + }, + }, + }, + pkgManagers: { + initial: 'setupPkgManagers', + description: + 'Runs the "setup" lifecyle event for reified PkgManagers', + states: { + setupPkgManagers: { + description: 'Executes the setupPkgManagers actor', + entry: [log('lifecycle: setup pkg managers')], + invoke: { + src: 'setupPkgManagers', + input: ({context: {pkgManagers = []}}) => pkgManagers, + onDone: { + actions: [log('lifecycle: setup pkg managers done')], + target: '#PluginLoaderMachine.setup.pkgManagers.done', + }, + onError: { + actions: [ + log(({event: {error}}) => error), + { + type: 'assignError', + params: ({event: {error}}) => ({error}), + }, + ], + target: '#PluginLoaderMachine.setup.pkgManagers.done', + }, + }, + }, + done: { + description: + 'PkgManagers done setting up (with or without error)', + type: 'final', + }, + }, + }, + }, + onDone: [ + { + description: + 'Setup lifecycle completed successfully; send all components to parent machine', + guard: {type: 'notHasError'}, + actions: [ + log( + ({context: {rules = [], pkgManagers = [], reporters = []}}) => + `sending ${rules.length} rules, ${pkgManagers.length} pkg managers, and ${reporters.length} reporters`, + ), + { + type: 'sendComponents', + params: ({context: {rules, pkgManagers, reporters}}) => { + return {rules, pkgManagers, reporters}; + }, + }, + ], + target: '#PluginLoaderMachine.ready', + }, + { + description: 'Setup lifecycle completed with error', + guard: {type: 'hasError'}, + target: '#PluginLoaderMachine.errored', + }, + ], + }, + ready: { + description: 'Idles until a TEARDOWN event is received', + on: { + TEARDOWN: { + actions: [log('TEARDOWN received')], + target: 'teardown', + }, + }, + }, + teardown: { + type: 'parallel', + description: 'Runs the "teardown" lifecycle for reified components', + states: { + pkgManagers: { + initial: 'teardownPkgManagers', + states: { + teardownPkgManagers: { + description: + 'Runs the "teardown" lifecycle for reified PkgManagers', + entry: [log('tearing down pkg managers...')], + invoke: { + src: 'teardownPkgManagers', + input: ({context: {pkgManagers = []}}) => pkgManagers, + onError: { + actions: [ + { + type: 'assignError', + params: ({event: {error}}) => ({error}), + }, + ], + target: '#PluginLoaderMachine.teardown.pkgManagers.done', + }, + onDone: { + target: '#PluginLoaderMachine.teardown.pkgManagers.done', + }, + }, + }, + done: { + type: 'final', + }, + }, + }, + reporters: { + initial: 'teardownReporters', + states: { + teardownReporters: { + description: + 'Runs the "teardown" lifecycle for reified Reporters', + entry: [log('tearing down reporters...')], + invoke: { + src: 'teardownReporters', + input: ({context: {reporters = []}}) => reporters, + onDone: { + target: '#PluginLoaderMachine.teardown.reporters.done', + }, + onError: { + actions: [ + { + type: 'assignError', + params: ({event: {error}}) => ({error}), + }, + ], + target: '#PluginLoaderMachine.teardown.reporters.done', + }, + }, + }, + done: { + type: 'final', + }, + }, + }, + }, + onDone: [ + { + description: 'Teardown lifecycle completed successfully', + guard: {type: 'notHasError'}, + target: '#PluginLoaderMachine.done', + }, + { + description: 'Setup teardown completed with error', + guard: {type: 'hasError'}, + target: '#PluginLoaderMachine.errored', + }, + ], + }, + errored: { + description: 'An error occurred', + type: 'final', + }, + done: { + description: 'Complete without error', + type: 'final', + }, + }, + output: ({self, context: {error}}) => + error + ? { + type: 'ERROR', + error, + id: self.id, + } + : { + type: 'OK', + id: self.id, + }, +}); diff --git a/packages/midnight-smoker/src/machine/reifier/reifier-machine-actors.ts b/packages/midnight-smoker/src/machine/reifier/reifier-machine-actors.ts new file mode 100644 index 00000000..d547bb7c --- /dev/null +++ b/packages/midnight-smoker/src/machine/reifier/reifier-machine-actors.ts @@ -0,0 +1,190 @@ +import {PACKAGE_JSON} from '#constants'; +import {type SmokerOptions} from '#options'; +import {type PluginMetadata} from '#plugin'; +import { + PkgManagerContextSchema, + WorkspacesConfigSchema, + type Executor, + type PkgManagerContext, + type PkgManagerDefSpec, + type PkgManagerOpts, + type ReporterContext, + type ReporterDef, +} from '#schema'; +import {readSmokerPkgJson, type FileManager} from '#util'; +import {Console} from 'console'; +import {glob} from 'glob'; +import {isFunction} from 'lodash'; +import path from 'node:path'; +import {type PackageJson} from 'type-fest'; +import {fromPromise} from 'xstate'; + +export interface LoadPkgManagersInput { + cwd?: string; + plugin: Readonly; + smokerOpts: SmokerOptions; +} + +export type PkgManagerDefSpecsWithCtx = PkgManagerDefSpec & { + ctx: PkgManagerContext; +}; + +export interface CreatePkgManagerContextsInput { + fm: FileManager; + systemExecutor: Executor; + defaultExecutor: Executor; + pkgManagerOpts?: PkgManagerOpts; + pkgManagerDefSpecs: PkgManagerDefSpec[]; + workspaces: Record; +} + +export interface ReporterStreams { + stderr: NodeJS.WritableStream; + stdout: NodeJS.WritableStream; +} + +export interface CreateReporterContextsInput { + pkgJson: PackageJson; + reporterDefs: ReporterDef[]; + smokerOpts: SmokerOptions; +} + +export interface ReporterDefWithCtx { + ctx: ReporterContext; + def: ReporterDef; +} + +export async function getStreams( + def: ReporterDef, +): Promise { + let stdout: NodeJS.WritableStream = process.stdout; + let stderr: NodeJS.WritableStream = process.stderr; + if (def.stdout) { + stdout = isFunction(def.stdout) ? await def.stdout() : def.stdout; + } + if (def.stderr) { + stderr = isFunction(def.stderr) ? await def.stderr() : def.stderr; + } + return {stdout, stderr}; +} + +export const queryWorkspaces = fromPromise< + Record, + {cwd: string; fm: FileManager; includeRoot?: boolean} +>( + async ({ + input: {cwd, fm, includeRoot = false}, + }): Promise> => { + const {packageJson: rootPkgJson} = await fm.findPkgUp(cwd, { + strict: true, + normalize: true, + }); + + const getWorkspaceInfo = async ( + paths: string[], + ): Promise> => { + const workspaces = await glob(paths, { + cwd, + withFileTypes: true, + }); + const entries = await Promise.all( + workspaces + .filter((workspace) => workspace.isDirectory()) + .map(async (workspace) => { + const fullpath = workspace.fullpath(); + const workspacePkgJson = await fm.readPkgJson( + path.join(fullpath, PACKAGE_JSON), + ); + return [workspacePkgJson.name ?? '(unknown)', fullpath] as [ + pkgName: string, + path: string, + ]; + }), + ); + return Object.fromEntries(entries); + }; + + const result = WorkspacesConfigSchema.safeParse(rootPkgJson.workspaces); + let workspaces: string[] = []; + if (result.success) { + workspaces = result.data; + if (includeRoot) { + workspaces = [cwd, ...workspaces]; + } + } else { + workspaces = [cwd]; + } + return getWorkspaceInfo(workspaces); + }, +); + +export const readSmokerPackageJson = fromPromise( + readSmokerPkgJson, +); + +export const loadPkgManagers = fromPromise< + PkgManagerDefSpec[], + LoadPkgManagersInput +>(async ({input: {plugin, cwd, smokerOpts}}) => { + return plugin.loadPkgManagers({ + cwd, + desiredPkgManagers: smokerOpts.pkgManager, + }); +}); + +export const createReporterContexts = fromPromise< + ReporterDefWithCtx[], + CreateReporterContextsInput +>(async ({input: {smokerOpts, reporterDefs, pkgJson}}) => { + return Promise.all( + reporterDefs.map(async (def) => { + const {stdout, stderr} = await getStreams(def); + const console = new Console({stdout, stderr}); + + const ctx: ReporterContext = { + opts: smokerOpts, + pkgJson, + console, + stdout, + stderr, + }; + return {def, ctx}; + }), + ); +}); + +export const createPkgManagerContexts = fromPromise< + PkgManagerDefSpecsWithCtx[], + CreatePkgManagerContextsInput +>( + async ({ + input: { + pkgManagerDefSpecs, + fm, + systemExecutor, + defaultExecutor, + pkgManagerOpts: opts, + workspaces, + }, + }) => { + if (pkgManagerDefSpecs?.length) { + return Promise.all( + pkgManagerDefSpecs.map(async ({spec, def}) => { + const tmpdir = await fm.createTempDir( + `${spec.pkgManager}-${spec.version}`, + ); + const executor = spec.isSystem ? systemExecutor : defaultExecutor; + const ctx = PkgManagerContextSchema.parse({ + spec, + tmpdir, + executor, + workspaceInfo: workspaces, + ...opts, + }); + return {spec, def, ctx}; + }), + ); + } + throw new Error('No pkgManagerDefSpecs'); + }, +); diff --git a/packages/midnight-smoker/src/machine/reifier/reifier-machine.ts b/packages/midnight-smoker/src/machine/reifier/reifier-machine.ts index 31a8500c..38a318a6 100644 --- a/packages/midnight-smoker/src/machine/reifier/reifier-machine.ts +++ b/packages/midnight-smoker/src/machine/reifier/reifier-machine.ts @@ -1,33 +1,26 @@ import { ComponentKinds, DEFAULT_EXECUTOR_ID, - PACKAGE_JSON, RuleSeverities, SYSTEM_EXECUTOR_ID, } from '#constants'; import {fromUnknownError} from '#error'; +import {type MachineOutputError, type MachineOutputOk} from '#machine/util'; import {type SmokerOptions} from '#options'; import {PkgManager} from '#pkg-manager'; import {type PluginMetadata, type PluginRegistry} from '#plugin'; import {Reporter, type SomeReporter} from '#reporter'; import {Rule, type RuleContext} from '#rule'; import { - PkgManagerContextSchema, - WorkspacesConfigSchema, type Executor, - type PkgManagerContext, type PkgManagerDefSpec, type PkgManagerOpts, - type ReporterContext, type ReporterDef, type SomeRule, type SomeRuleDef, } from '#schema'; -import {readSmokerPkgJson, type FileManager} from '#util'; -import {glob} from 'glob'; -import {isFunction} from 'lodash'; -import {Console} from 'node:console'; -import path from 'node:path'; +import {type FileManager} from '#util'; +import assert from 'node:assert'; import { type PackageJson, type SetFieldType, @@ -35,8 +28,19 @@ import { type SetRequired, type Simplify, } from 'type-fest'; -import {and, assign, fromPromise, log, not, setup} from 'xstate'; -import {type MachineOutputError, type MachineOutputOk} from '../machine-util'; +import {and, assign, log, not, setup} from 'xstate'; +import { + createPkgManagerContexts, + createReporterContexts, + loadPkgManagers, + queryWorkspaces, + readSmokerPackageJson, + type CreatePkgManagerContextsInput, + type CreateReporterContextsInput, + type LoadPkgManagersInput, + type PkgManagerDefSpecsWithCtx, + type ReporterDefWithCtx, +} from './reifier-machine-actors'; export const LoadableComponents = { All: 'all', @@ -105,62 +109,16 @@ export type ReifierOutputError = MachineOutputError; export type ReifierOutput = ReifierOutputOk | ReifierOutputError; -export type CreatePkgManagerContextsInput = SetRequired< - Pick< - ReifierPkgManagerParams, - 'fm' | 'systemExecutor' | 'defaultExecutor' | 'pkgManagerOpts' - > & - Pick, - 'systemExecutor' | 'defaultExecutor' ->; - export interface LoadReportersInput { opts: SmokerOptions; pluginRegistry: PluginRegistry; } -export type PkgManagerDefSpecsWithCtx = PkgManagerDefSpec & { - ctx: PkgManagerContext; -}; - -export type LoadPkgManagersInput = Pick & - Pick; - -export interface CreateReporterContextsInput { - reporterDefs: ReporterDef[]; - pkgJson: PackageJson; - smokerOpts: SmokerOptions; -} - -export interface ReporterDefWithCtx { - def: ReporterDef; - ctx: ReporterContext; -} - export interface RuleDefWithCtx { def: SomeRuleDef; ctx: RuleContext; } -export type ReporterStreams = { - stderr: NodeJS.WritableStream; - stdout: NodeJS.WritableStream; -}; - -async function getStreams( - def: ReporterDef, -): Promise { - let stdout: NodeJS.WritableStream = process.stdout; - let stderr: NodeJS.WritableStream = process.stderr; - if (def.stdout) { - stdout = isFunction(def.stdout) ? await def.stdout() : def.stdout; - } - if (def.stderr) { - stderr = isFunction(def.stderr) ? await def.stderr() : def.stderr; - } - return {stdout, stderr}; -} - export const ReifierMachine = setup({ types: { input: {} as ReifierInput, @@ -282,120 +240,11 @@ export const ReifierMachine = setup({ }), }, actors: { - // TODO this is gonna need a test or two - queryWorkspaces: fromPromise< - Record, - {cwd: string; fm: FileManager; includeRoot?: boolean} - >( - async ({ - input: {cwd, fm, includeRoot = false}, - }): Promise> => { - const {packageJson: rootPkgJson} = await fm.findPkgUp(cwd, { - strict: true, - normalize: true, - }); - - const getWorkspaceInfo = async ( - paths: string[], - ): Promise> => { - const workspaces = await glob(paths, { - cwd, - withFileTypes: true, - }); - const entries = await Promise.all( - workspaces - .filter((workspace) => workspace.isDirectory()) - .map(async (workspace) => { - const fullpath = workspace.fullpath(); - const workspacePkgJson = await fm.readPkgJson( - path.join(fullpath, PACKAGE_JSON), - ); - return [workspacePkgJson.name ?? '(unknown)', fullpath] as [ - pkgName: string, - path: string, - ]; - }), - ); - return Object.fromEntries(entries); - }; - - const result = WorkspacesConfigSchema.safeParse(rootPkgJson.workspaces); - let workspaces: string[] = []; - if (result.success) { - workspaces = result.data; - if (includeRoot) { - workspaces = [cwd, ...workspaces]; - } - } else { - workspaces = [cwd]; - } - return getWorkspaceInfo(workspaces); - }, - ), - readSmokerPkgJson: fromPromise(readSmokerPkgJson), - loadPkgManagers: fromPromise( - async ({input: {plugin, cwd, smokerOpts}}) => { - return plugin.loadPkgManagers({ - cwd, - desiredPkgManagers: smokerOpts.pkgManager, - }); - }, - ), - createReporterContexts: fromPromise< - ReporterDefWithCtx[], - CreateReporterContextsInput - >(async ({input: {smokerOpts, reporterDefs, pkgJson}}) => { - return Promise.all( - reporterDefs.map(async (def) => { - const {stdout, stderr} = await getStreams(def); - const console = new Console({stdout, stderr}); - - const ctx: ReporterContext = { - opts: smokerOpts, - pkgJson, - console, - stdout, - stderr, - }; - return {def, ctx}; - }), - ); - }), - createPkgManagerContexts: fromPromise< - PkgManagerDefSpecsWithCtx[], - CreatePkgManagerContextsInput - >( - async ({ - input: { - pkgManagerDefSpecs, - fm, - systemExecutor, - defaultExecutor, - pkgManagerOpts: opts, - workspaces, - }, - }) => { - if (pkgManagerDefSpecs?.length) { - return Promise.all( - pkgManagerDefSpecs.map(async ({spec, def}) => { - const tmpdir = await fm.createTempDir( - `${spec.pkgManager}-${spec.version}`, - ); - const executor = spec.isSystem ? systemExecutor : defaultExecutor; - const ctx = PkgManagerContextSchema.parse({ - spec, - tmpdir, - executor, - workspaceInfo: workspaces, - ...opts, - }); - return {spec, def, ctx}; - }), - ); - } - throw new Error('No pkgManagerDefSpecs'); - }, - ), + queryWorkspaces, + readSmokerPkgJson: readSmokerPackageJson, + loadPkgManagers, + createReporterContexts, + createPkgManagerContexts, }, guards: { hasError: ({context: {error}}) => Boolean(error), @@ -434,15 +283,14 @@ export const ReifierMachine = setup({ ...input }, }): ReifierContext => { + const getId = pluginRegistry.getComponentId.bind(pluginRegistry); + const enabledReporterDefs = plugin.getEnabledReporterDefs( smokerOpts, - pluginRegistry.getComponentId.bind(pluginRegistry), + getId, ); - // TODO make plugin.getEnabledRuleDefs - const enabledRuleDefs = [...plugin.ruleDefMap.values()].filter((def) => { - const id = pluginRegistry.getComponentId(def); - return smokerOpts.rules[id].severity !== RuleSeverities.Off; - }); + const enabledRuleDefs = plugin.getEnabledRuleDefs(smokerOpts, getId); + return { component, plugin, @@ -475,8 +323,9 @@ export const ReifierMachine = setup({ invoke: { src: 'loadPkgManagers', input: ({context}): LoadPkgManagersInput => { - const {plugin, smokerOpts} = context; - const {cwd} = context.pkgManager!; + const {plugin, smokerOpts, pkgManager} = context; + assert.ok(pkgManager); + const {cwd} = pkgManager; return { cwd, plugin, @@ -510,11 +359,14 @@ export const ReifierMachine = setup({ queryWorkspaces: { invoke: { src: 'queryWorkspaces', - input: ({context}) => ({ - cwd: context.smokerOpts.cwd, - fm: context.pkgManager!.fm, - includeRoot: context.smokerOpts.includeRoot, - }), + input: ({context: {smokerOpts, pkgManager}}) => { + assert.ok(pkgManager); + return { + cwd: smokerOpts.cwd, + fm: pkgManager.fm, + includeRoot: smokerOpts.includeRoot, + }; + }, onDone: { actions: [ { @@ -539,8 +391,14 @@ export const ReifierMachine = setup({ invoke: { src: 'createPkgManagerContexts', input: ({context}): CreatePkgManagerContextsInput => { - const {pkgManagerDefSpecs, pluginRegistry, workspaces} = - context; + const { + pkgManager, + pkgManagerDefSpecs, + pluginRegistry, + workspaces, + } = context; + assert.ok(pkgManager); + assert.ok(pkgManagerDefSpecs); const { fm, systemExecutor = pluginRegistry.getExecutor( @@ -550,7 +408,7 @@ export const ReifierMachine = setup({ DEFAULT_EXECUTOR_ID, ), pkgManagerOpts, - } = context.pkgManager!; + } = pkgManager; return { pkgManagerDefSpecs, fm, @@ -645,11 +503,14 @@ export const ReifierMachine = setup({ src: 'createReporterContexts', input: ({ context: {smokerOpts, enabledReporterDefs, smokerPkgJson}, - }): CreateReporterContextsInput => ({ - reporterDefs: enabledReporterDefs, - smokerOpts, - pkgJson: smokerPkgJson!, - }), + }): CreateReporterContextsInput => { + assert.ok(smokerPkgJson); + return { + reporterDefs: enabledReporterDefs, + smokerOpts, + pkgJson: smokerPkgJson, + }; + }, onDone: { actions: [ { diff --git a/packages/midnight-smoker/src/machine/reporter/reporter-machine-actors.ts b/packages/midnight-smoker/src/machine/reporter/reporter-machine-actors.ts index 513318e5..947f3475 100644 --- a/packages/midnight-smoker/src/machine/reporter/reporter-machine-actors.ts +++ b/packages/midnight-smoker/src/machine/reporter/reporter-machine-actors.ts @@ -1,33 +1,25 @@ -import {type ReporterListeners} from '#reporter'; +import {type CtrlEmitted} from '#machine/controller'; import {type SomeReporter} from '#reporter/reporter'; import {fromPromise} from 'xstate'; -import {type CtrlEmitted} from '../controller/control-machine-events'; export interface DrainQueueInput { - listeners: Partial; queue: CtrlEmitted[]; reporter: SomeReporter; } +/** + * Drains the queue of events and invokes the listener for each event. + */ export const drainQueue = fromPromise( async ({input: {reporter, queue}}): Promise => { while (queue.length) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const event = queue.shift()!; const {type, ...rest} = event; - // @ts-expect-error fix later + + // If this rejects, it should be a ReporterError. + // @ts-expect-error TODO fix later await reporter.invokeListener({...rest, type}); } }, ); - -export const teardownReporter = fromPromise( - async ({input: reporter}): Promise => { - await reporter.teardown(); - }, -); - -export const setupReporter = fromPromise( - async ({input: reporter}): Promise => { - await reporter.setup(); - }, -); diff --git a/packages/midnight-smoker/src/machine/reporter/reporter-machine-events.ts b/packages/midnight-smoker/src/machine/reporter/reporter-machine-events.ts index 3fd987d8..f7eef72f 100644 --- a/packages/midnight-smoker/src/machine/reporter/reporter-machine-events.ts +++ b/packages/midnight-smoker/src/machine/reporter/reporter-machine-events.ts @@ -1,4 +1,4 @@ -import {type CtrlEmitted} from '../controller/control-machine-events'; +import {type CtrlEmitted} from '#machine/controller'; export type ReporterMachineEvents = | ReporterMachineCtrlEvent diff --git a/packages/midnight-smoker/src/machine/reporter/reporter-machine.ts b/packages/midnight-smoker/src/machine/reporter/reporter-machine.ts index fcb358ec..39ab0230 100644 --- a/packages/midnight-smoker/src/machine/reporter/reporter-machine.ts +++ b/packages/midnight-smoker/src/machine/reporter/reporter-machine.ts @@ -1,22 +1,10 @@ -import {fromUnknownError} from '#error'; -import {ReporterListenerEventMap, type ReporterListeners} from '#reporter'; +import {type ReporterError} from '#error'; +import {type CtrlEmitted} from '#machine/controller'; +import {type MachineOutputError, type MachineOutputOk} from '#machine/util'; import {type SomeReporter} from '#reporter/reporter'; -import {isFunction, pickBy} from 'lodash'; -import { - assign, - log, - not, - setup, - type ActorRef, - type Subscription, -} from 'xstate'; -import {type CtrlEmitted} from '../controller/control-machine-events'; -import {type MachineOutputError, type MachineOutputOk} from '../machine-util'; -import { - drainQueue, - setupReporter, - teardownReporter, -} from './reporter-machine-actors'; +import {isEmpty} from 'lodash'; +import {assign, log, not, setup, type ActorRef} from 'xstate'; +import {drainQueue} from './reporter-machine-actors'; import {type ReporterMachineEvents} from './reporter-machine-events'; export type ReporterMachineOutput = @@ -28,10 +16,9 @@ export type ReporterMachineOutputError = MachineOutputError; export type ReporterMachineOutputOk = MachineOutputOk; export interface ReporterMachineContext extends ReporterMachineInput { - error?: Error; - listeners: Partial; + error?: ReporterError; queue: CtrlEmitted[]; - subscriptions: Subscription[]; + shouldHalt: boolean; } export interface ReporterMachineInput { @@ -47,19 +34,20 @@ export const ReporterMachine = setup({ output: {} as ReporterMachineOutput, }, actors: { - teardownReporter, - setupReporter, drainQueue, }, guards: { - hasEvents: ({context: {queue}}) => Boolean(queue.length), - isQueueEmpty: not('hasEvents'), + hasEvents: not('isQueueEmpty'), + isQueueEmpty: ({context: {queue}}) => isEmpty(queue), hasError: ({context: {error}}) => Boolean(error), notHasError: not('hasError'), + shouldHalt: ({context: {queue, shouldHalt}}) => + Boolean(shouldHalt && isEmpty(queue)), + shouldListen: not('shouldHalt'), }, actions: { assignError: assign({ - error: (_, {error}: {error: unknown}) => fromUnknownError(error), + error: (_, {error}: {error: ReporterError}) => error, }), enqueue: assign({ queue: ({context: {queue}}, {event}: {event: CtrlEmitted}) => [ @@ -67,178 +55,97 @@ export const ReporterMachine = setup({ event, ], }), - createListeners: assign({ - listeners: ({context: {reporter}}) => { - const listeners = pickBy( - reporter.def, - (val, key) => key in ReporterListenerEventMap && isFunction(val), - ) as Partial; - - return listeners; - }, - }), - destroyListeners: assign({ - listeners: {}, - }), + assignShouldHalt: assign({shouldHalt: true}), }, }).createMachine({ - initial: 'setup', + initial: 'listening', context: ({input}) => ({ ...input, - listeners: {}, - subscriptions: [], queue: [], + shouldHalt: false, }), id: 'ReporterMachine', on: { HALT: { - target: '.cleanup', - }, - EVENT: { + description: 'Mark the machine for halting after draining the queue', actions: [ + log( + ({context: {queue}}) => + `will halt after emitting ${queue.length} event(s)`, + ), { - type: 'enqueue', - params: ({event: {event}}) => ({event}), + type: 'assignShouldHalt', }, ], }, + EVENT: [ + { + description: + 'Ignore event if marked for halting; this may or may not ever happen', + guard: {type: 'shouldHalt'}, + actions: [ + log( + ({ + event: { + event: {type}, + }, + }) => `received event during cleanup operation: ${type}; ignoring`, + ), + ], + }, + { + description: 'Enqueue the event for re-emission to the reporter', + guard: {type: 'shouldListen'}, + actions: [ + { + type: 'enqueue', + params: ({event: {event}}) => ({event}), + }, + ], + }, + ], }, states: { - setup: { - entry: [log(({context: {reporter}}) => `setting up ${reporter}`)], - invoke: { - src: 'setupReporter', - input: ({context: {reporter}}) => reporter, - onError: { - target: 'done', - actions: [ - log(({event: {error}}) => `error setting up reporter: ${error}`), - { - type: 'assignError', - params: ({event: {error}}) => ({error}), - }, - ], - }, - onDone: { - target: 'listening', - actions: [log('binding...'), {type: 'createListeners'}], - }, - }, - exit: [log('setup complete')], - }, listening: { + description: 'Determines whether to process events or exit', always: [ { guard: {type: 'hasEvents'}, - target: 'draining', + target: '#ReporterMachine.draining', + }, + { + guard: {type: 'shouldHalt'}, + target: '#ReporterMachine.done', }, ], }, draining: { + description: 'Drains the event queue by emitting events to the reporter', invoke: { src: 'drainQueue', - input: ({context: {reporter, listeners, queue}}) => ({ + input: ({context: {reporter, queue}}) => ({ queue, reporter, - listeners, }), onDone: { - target: 'listening', + target: '#ReporterMachine.listening', }, onError: { target: '#ReporterMachine.errored', actions: [ { type: 'assignError', - params: ({event: {error}}) => ({error}), + params: ({event: {error}}) => ({error: error as ReporterError}), }, ], }, }, }, - cleanup: { - initial: 'unsubscribing', - entry: [log('cleaning up...')], - states: { - unsubscribing: { - // entry: [log('unsubscribing'), {type: 'unsubscribe'}], - always: [ - { - target: 'draining', - guard: {type: 'hasEvents'}, - }, - { - target: 'teardown', - guard: {type: 'isQueueEmpty'}, - }, - ], - }, - draining: { - invoke: { - src: 'drainQueue', - input: ({context: {reporter, listeners, queue}}) => ({ - queue, - reporter, - listeners, - }), - onDone: { - target: 'teardown', - }, - onError: { - target: 'teardown', - actions: [ - { - type: 'assignError', - params: ({event: {error}}) => ({error}), - }, - ], - }, - }, - }, - teardown: { - entry: [ - log('tearing down'), - { - type: 'destroyListeners', - }, - ], - invoke: { - src: 'teardownReporter', - input: ({context: {reporter}}) => reporter, - onError: { - actions: [ - { - type: 'assignError', - params: ({event: {error}}) => ({error}), - }, - ], - target: '#ReporterMachine.errored', - }, - onDone: { - target: 'done', - }, - }, - }, - done: { - type: 'final', - }, - }, - onDone: [ - { - guard: {type: 'hasError'}, - target: 'errored', - }, - { - guard: {type: 'notHasError'}, - target: 'done', - }, - ], - }, done: { type: 'final', }, errored: { - entry: [log(({context: {error}}) => `finished w/ error: ${error}`)], + entry: [log(({context: {error}}) => error)], type: 'final', }, }, diff --git a/packages/midnight-smoker/src/machine/runner/run-machine.ts b/packages/midnight-smoker/src/machine/runner/run-machine.ts index 85ddcf61..cab1e641 100644 --- a/packages/midnight-smoker/src/machine/runner/run-machine.ts +++ b/packages/midnight-smoker/src/machine/runner/run-machine.ts @@ -1,10 +1,12 @@ -import {fromUnknownError, ScriptBailed} from '#error'; +import {ScriptBailed} from '#error'; +import {type MachineOutputError, type MachineOutputOk} from '#machine/util'; import {type PkgManager} from '#pkg-manager'; import { type RunScriptManifest, type RunScriptResult, type ScriptError, } from '#schema'; +import assert from 'node:assert'; import { assign, fromPromise, @@ -14,7 +16,6 @@ import { setup, type AnyActorRef, } from 'xstate'; -import {type MachineOutputError, type MachineOutputOk} from '../machine-util'; import {type RunMachineRunScriptBeginEvent} from './runner-machine-events'; export interface RunMachineInput { @@ -71,6 +72,12 @@ export const RunMachine = setup({ }; }, ), + assignResult: assign({ + result: (_, result: RunScriptResult) => result, + }), + assignError: assign({ + error: (_, error: ScriptError) => error, + }), }, actors: { runScript: fromPromise( @@ -112,9 +119,10 @@ export const RunMachine = setup({ }), onDone: { actions: [ - assign({ - result: ({event: {output: result}}) => result, - }), + { + type: 'assignResult', + params: ({event: {output: result}}) => result, + }, ], target: 'done', }, @@ -122,19 +130,20 @@ export const RunMachine = setup({ { guard: {type: 'isBailed'}, actions: [ - assign({ - result: {skipped: true}, - }), + { + type: 'assignResult', + params: {skipped: true}, + }, ], target: 'aborted', }, { guard: {type: 'isNotBailed'}, actions: [ - assign({ - error: ({event: {error}}) => - fromUnknownError(error) as ScriptError, - }), + { + type: 'assignError', + params: ({event: {error}}) => error as ScriptError, + }, ], target: 'errored', }, @@ -165,11 +174,12 @@ export const RunMachine = setup({ error, }; } + assert.ok(result); return { type: 'OK', id, manifest: runScriptManifest, - result: result!, + result, scriptIndex: index, }; }, diff --git a/packages/midnight-smoker/src/machine/runner/runner-machine.ts b/packages/midnight-smoker/src/machine/runner/runner-machine.ts index 874cacc7..1095d257 100644 --- a/packages/midnight-smoker/src/machine/runner/runner-machine.ts +++ b/packages/midnight-smoker/src/machine/runner/runner-machine.ts @@ -1,22 +1,10 @@ -import {type PkgManager} from '#pkg-manager'; -import {type RunScriptManifest, type RunScriptResult} from '#schema'; -import {isEmpty} from 'lodash'; -import { - assign, - enqueueActions, - log, - sendTo, - setup, - type ActorRefFrom, - type AnyActorRef, -} from 'xstate'; import { type CtrlPkgManagerRunScriptsBeginEvent, type CtrlRunScriptBeginEvent, type CtrlRunScriptFailedEvent, type CtrlRunScriptOkEvent, type CtrlRunScriptSkippedEvent, -} from '../controller/control-machine-events'; +} from '#machine/controller'; import { assertMachineOutputOk, isMachineOutputOk, @@ -24,7 +12,19 @@ import { monkeypatchActorLogger, type MachineOutputLike, type MachineOutputOk, -} from '../machine-util'; +} from '#machine/util'; +import {type PkgManager} from '#pkg-manager'; +import {type RunScriptManifest, type RunScriptResult} from '#schema'; +import {isEmpty} from 'lodash'; +import { + assign, + enqueueActions, + log, + sendTo, + setup, + type ActorRefFrom, + type AnyActorRef, +} from 'xstate'; import {RunMachine, type RunMachineOutputOk} from './run-machine'; import { type RunMachineRunScriptBeginEvent, diff --git a/packages/midnight-smoker/src/machine/util/index.ts b/packages/midnight-smoker/src/machine/util/index.ts new file mode 100644 index 00000000..fbc61702 --- /dev/null +++ b/packages/midnight-smoker/src/machine/util/index.ts @@ -0,0 +1,160 @@ +import {MIDNIGHT_SMOKER} from '#constants'; +import Debug from 'debug'; +import {map, memoize, uniqBy} from 'lodash'; +import {AssertionError} from 'node:assert'; +import {type AnyActorRef} from 'xstate'; + +/** + * `MachineOutput` is a convention for machine output. + * + * Machines adhering to this convention can exit with a {@link MachineOutputOk} + * or, in case of error, a {@link MachineOutputError}. + * + * The two types are discriminated on the `type` property. + */ +export type MachineOutput< + Ok extends object = object, + Err extends Error = Error, +> = MachineOutputOk | MachineOutputError; + +/** + * Represents the output of a machine when an error occurs. + * + * @template Err The type of the error. + * @template Ctx The type of the context object. + */ +export type MachineOutputError< + Err extends Error = Error, + Ctx extends object = object, +> = { + id: string; + type: 'ERROR'; + error: Err; +} & Ctx; + +/** + * Represents the output of a machine when it is in a successful state. + * + * @template Ctx - The type of the additional context information. + */ +export type MachineOutputOk = { + type: 'OK'; + id: string; +} & Ctx; + +/** + * @deprecated + */ +export interface MachineOutputLike { + id: string; +} + +/** + * Asserts that the machine output is not ok and throws an error if it is. + * + * @template Ok - The type of the successful machine output. + * @template Err - The type of the error in the machine output. + * @param output - The machine output to be checked. + * @throws AssertionError - If the machine output is ok. + */ +export function assertMachineOutputNotOk< + Ok extends object = object, + Err extends Error = Error, +>(output: MachineOutput): asserts output is MachineOutputError { + if (isMachineOutputOk(output)) { + throw new AssertionError({ + message: 'Expected an error in machine output', + actual: output, + }); + } +} + +/** + * Asserts that the machine output is of type `MachineOutputOk`. If the output + * is not of type `MachineOutputOk`, an `AssertionError` is thrown. + * + * @template Ok - The type of the successful machine output. + * @template Err - The type of the error in the machine output. + * @param output - The machine output to be asserted. + * @throws AssertionError if the output is not of type `MachineOutputOk`. + */ +export function assertMachineOutputOk< + Ok extends object = object, + Err extends Error = Error, +>(output: MachineOutput): asserts output is MachineOutputOk { + if (isMachineOutputNotOk(output)) { + throw new AssertionError({ + message: 'Unexpected error in machine output', + actual: output, + }); + } +} + +/** + * Checks if the given machine output is an error. + * + * @template Ok - The type of the successful machine output. + * @template Err - The type of the error in the machine output. + * @param output - The machine output to check. + * @returns `true` if the output is an error, `false` otherwise. + */ +export function isMachineOutputNotOk< + Ok extends object = object, + Err extends Error = Error, +>(output: MachineOutput): output is MachineOutputError { + return output.type === 'ERROR'; +} + +/** + * Checks if the provided `output` is of type `MachineOutputOk`. + * + * @template Ok - The type of the successful machine output. + * @template Err - The type of the error in the machine output. + * @param output - The output to check. + * @returns `true` if the `output` is of type `MachineOutputOk`, `false` + * otherwise. + */ +export function isMachineOutputOk< + Ok extends object = object, + Err extends Error = Error, +>(output: MachineOutput): output is MachineOutputOk { + return output.type === 'OK'; +} + +/** + * Generates a random ID. + * + * @returns A random ID string. + */ +export function makeId() { + return Math.random().toString(36).substring(7); +} + +/** + * Monkeypatches {@link ActorRef.logger} with a debug instance. + * + * @template T - The type of the actor. + * @param {T} actor - The actor to monkeypatch. + * @param {string} namespace - The custom namespace for the logger. + * @returns {T} - The monkeypatched actor. + * @see {@link https://github.com/statelyai/xstate/issues/4634} + * @todo Currently, XState's API only allows the setting of `logger` via + * `createActor`; it should also work with `spawn`, `spawnChild`, and/or + * `invoke`. + */ +export function monkeypatchActorLogger( + actor: T, + namespace: string, +): T { + // @ts-expect-error private + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + actor.logger = actor._actorScope.logger = Debug( + `${MIDNIGHT_SMOKER}:${namespace}`, + ); + return actor; +} + +export const uniquePkgNames = memoize( + (manifests: {pkgName: string}[]): string[] => + map(uniqBy(manifests, 'pkgName'), 'pkgName'), +); diff --git a/packages/midnight-smoker/src/plugin/plugin-metadata.ts b/packages/midnight-smoker/src/plugin/plugin-metadata.ts index 4e4fdfa9..33d82ac1 100644 --- a/packages/midnight-smoker/src/plugin/plugin-metadata.ts +++ b/packages/midnight-smoker/src/plugin/plugin-metadata.ts @@ -36,6 +36,7 @@ import path from 'node:path'; import type {LiteralUnion, PackageJson} from 'type-fest'; import {z} from 'zod'; import {fromZodError} from 'zod-validation-error'; +import {RuleSeverities} from '../constants'; const debug = Debug('midnight-smoker:plugin:metadata'); @@ -440,6 +441,16 @@ export class PluginMetadata implements StaticPluginMetadata { return enabledReporters; } + + public getEnabledRuleDefs( + opts: SmokerOptions, + getComponentId: (def: object) => string, + ) { + return [...this.ruleDefMap.values()].filter((def) => { + const id = getComponentId(def); + return opts.rules[id].severity !== RuleSeverities.Off; + }); + } } export type {LiteralUnion}; diff --git a/packages/midnight-smoker/src/smoker.ts b/packages/midnight-smoker/src/smoker.ts index eec93ee5..de940127 100644 --- a/packages/midnight-smoker/src/smoker.ts +++ b/packages/midnight-smoker/src/smoker.ts @@ -41,7 +41,7 @@ import { ControlMachine, type CtrlMachineOutput, } from './machine/controller/control-machine'; -import {isMachineOutputOk} from './machine/machine-util'; +import {isMachineOutputOk} from './machine/util'; import {FileManager} from './util/filemanager'; type SetupResult = diff --git a/packages/midnight-smoker/test/unit/component/rule/context.spec.ts b/packages/midnight-smoker/test/unit/component/rule/context.spec.ts index 0ad74f99..239e055c 100644 --- a/packages/midnight-smoker/test/unit/component/rule/context.spec.ts +++ b/packages/midnight-smoker/test/unit/component/rule/context.spec.ts @@ -38,6 +38,7 @@ describe('midnight-smoker', function () { }, pkgJsonPath: '/path/to/example-package/package.json', installPath: '/path/to/example-package', + localPath: '/path/to/example-package', ruleName: rule.name, pkgManager: 'smthing', severity: 'error', diff --git a/packages/midnight-smoker/test/unit/component/rule/issue.spec.ts b/packages/midnight-smoker/test/unit/component/rule/issue.spec.ts index 87f1a09b..c0bfa7f7 100644 --- a/packages/midnight-smoker/test/unit/component/rule/issue.spec.ts +++ b/packages/midnight-smoker/test/unit/component/rule/issue.spec.ts @@ -37,6 +37,7 @@ describe('midnight-smoker', function () { }, pkgJsonPath: '/path/to/example-package/package.json', installPath: '/path/to/example-package', + localPath: '/path/to/example-package', severity: 'error', ruleName: exampleStaticRule.name, pkgManager: 'bebebebebee', diff --git a/packages/plugin-default/data/pnpm-dist-tags.json b/packages/plugin-default/data/pnpm-dist-tags.json index d38c77e0..2a0fd772 100644 --- a/packages/plugin-default/data/pnpm-dist-tags.json +++ b/packages/plugin-default/data/pnpm-dist-tags.json @@ -1,5 +1,5 @@ { - "latest": "9.0.4", + "latest": "9.0.5", "latest-1": "1.43.1", "latest-2": "2.25.7", "latest-3": "3.8.1", @@ -13,6 +13,6 @@ "next-8": "8.15.7", "latest-7": "7.33.5", "latest-8": "8.15.7", - "next-9": "9.0.4", - "latest-9": "9.0.4" + "next-9": "9.0.5", + "latest-9": "9.0.5" } diff --git a/packages/plugin-default/data/pnpm-versions.json b/packages/plugin-default/data/pnpm-versions.json index a5e2abee..b393bfe7 100644 --- a/packages/plugin-default/data/pnpm-versions.json +++ b/packages/plugin-default/data/pnpm-versions.json @@ -1043,5 +1043,6 @@ "9.0.1", "9.0.2", "9.0.3", - "9.0.4" + "9.0.4", + "9.0.5" ] diff --git a/packages/plugin-default/test/unit/package-manager/npm7.spec.ts b/packages/plugin-default/test/unit/package-manager/npm7.spec.ts index dc4f3457..5e6ac018 100644 --- a/packages/plugin-default/test/unit/package-manager/npm7.spec.ts +++ b/packages/plugin-default/test/unit/package-manager/npm7.spec.ts @@ -108,6 +108,7 @@ describe('@midnight-smoker/plugin-default', function () { stdout: JSON.stringify(npmPackItems), } as any); ctx = { + workspaceInfo: {}, spec, tmpdir: MOCK_TMPDIR, executor, @@ -259,6 +260,7 @@ describe('@midnight-smoker/plugin-default', function () { beforeEach(function () { executor.resolves({stdout: 'stuff', exitCode: 0} as any); ctx = { + workspaceInfo: {}, spec, tmpdir: MOCK_TMPDIR, executor, @@ -332,6 +334,7 @@ describe('@midnight-smoker/plugin-default', function () { beforeEach(function () { executor.resolves({failed: false, stdout: 'stuff'} as any); ctx = { + workspaceInfo: {}, signal: new AbortController().signal, spec, executor, diff --git a/packages/plugin-default/test/unit/package-manager/npm9.spec.ts b/packages/plugin-default/test/unit/package-manager/npm9.spec.ts index b712176f..de5b515e 100644 --- a/packages/plugin-default/test/unit/package-manager/npm9.spec.ts +++ b/packages/plugin-default/test/unit/package-manager/npm9.spec.ts @@ -81,6 +81,7 @@ describe('@midnight-smoker/plugin-default', function () { beforeEach(function () { ctx = { + workspaceInfo: {}, signal: new AbortController().signal, spec, tmpdir: MOCK_TMPDIR,