Last active
April 29, 2020 08:10
-
-
Save aem/dab2c7eabec5d2acce26702198b6e3a2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
An installer is a package that has a single index.(ts|js) file with a single | |
default export. Its type is | |
class Installer<Options extends InstallerOptions> { | |
steps: InstallerStep[] | |
options: Options | |
} | |
The Installer class is, at its core, a flexible step execution framework for | |
clients. It's likely that much of this code can be reused for plugins when | |
the time comes. | |
The client exporting its own instance of Installer allows installers authored | |
in TypeScript to ensure thier installer conforms to the Installer's interface. | |
In the Blitz CLI, the `install` command will know how to fetch installers from | |
the Blitz-hosted installer set. Alternatively, you could supply a relative | |
filesystem path to an installer or an absolute URL to a GitHub repo that houses | |
an installer. | |
The `install` command will read the package, execute the script steps, guiding | |
the user through the installation step-by-step based on the `steps` config. | |
Any extra CLI args passed will be parsed into a JS object and passed directly | |
to each installer step and lifecycle method. | |
We'll begin by supporting three step types, or executors: transform files, add | |
files, and add dependencies. These steps are each strongly typed and have | |
strict validation, including requirements for explanations of the changes | |
provided. We'll use these fields to create the wizard for the end user. | |
*/ | |
export interface BaseInstallerStep { | |
stepId: string | number | |
stepName: string | |
// a bit to display to the user to give context to the change | |
explanation: string | |
} | |
export interface TransformInstallerStep extends BaseInstallerStep { | |
// a user can either provide a function that will execute and use AST parsing to detect | |
// possible candidate files useful if you might need to update all usages of a library | |
selectTargetFiles?(api: Jscodeshift): string[] | |
// they can also provide a glob pattern to filter the filesystem for potential | |
// destinations. we'll provide a selection screen in the console if this option is used | |
singleFileSearch?: string | |
transform(fileInfo: FileInfo, api: Jscodeshift, options: any): void | |
} | |
function isTransformStep(step: BaseInstallerStep): step is TransformInstallerStep { | |
return (step as TransformInstallerStep).transform !== undefined | |
} | |
export interface NewFileInstallerStep extends BaseInstallerStep { | |
templatePath: string | |
destinationPath: string | |
destinationPathPrompt: string | |
} | |
function isNewFileStep(step: BaseInstallerStep): step is NewFileInstallerStep { | |
return (step as NewFileInstallerStep).templatePath !== undefined | |
} | |
export interface AddDependencyInstallerStep extends BaseInstallerStep { | |
packageName: string | |
// if absent we'll install the latest version of the dependency by default | |
packageVersion?: string | |
} | |
function isAddDependencyStep(step: BaseInstallerStep): step is AddDependencyInstallerStep { | |
return (step as AddDependencyInstallerStep).packageName !== undefined | |
} | |
type InstallerStep = TransformInstallerStep | AddDependencyInstallerStep | |
interface InstallerOptions { | |
packageName: string | |
packageDescription: string | |
packageOwner: string | |
packageRepo: string | |
// validator should throw an error with a helpful message if arguments are invalid | |
validateArgs?(args: {}): Promise<void> | |
// I'm still not certain of which lifecycle hooks we should expose, if any. I can | |
// definitely think of valuable reasons for each, and they're cheap to have, just | |
// not sure how much we want each plugin to be a different experience. | |
preInstall?(): Promise<void> | |
beforeEach?(stepId: string | number): Promise<void> | |
afterEach?(stepId: string | number): Promise<void> | |
postInstall?(): Promise<void> | |
} | |
export class Installer<Options extends InstallerOptions> { | |
private readonly steps: InstallerStep[] | |
private readonly options: Options | |
constructor(steps: InstallerStep[], options: Options) { | |
this.steps = steps | |
this.options = options | |
} | |
private async executeTransform(step: TransformInstallerStep): Promise<void> { | |
// show step context | |
// find all target files or prompt for file based on step options | |
// resolve prompts | |
// display diff of transform. if there are multiple targets, display the first | |
// confirm execution | |
// execute step | |
} | |
private async executeAddDependency(step: AddDependencyInstallerStep): Promise<void> { | |
// show step context | |
// show version that will be installed and get confirmation | |
// install dependency | |
} | |
private async executeNewFile(step: NewFileInstallerStep): Promise<void> { | |
// can use existing generator infrastructure | |
} | |
private async validateArgs(cliArgs: {}): Promise<void> { | |
if (this.options.validateArgs) return this.options.validateArgs(cliArgs) | |
} | |
private async preInstall(): Promise<void> { | |
if (this.options.preInstall) return this.options.preInstall() | |
} | |
private async beforeEach(stepId: string | number): Promise<void> { | |
if (this.options.beforeEach) return this.options.beforeEach(stepId) | |
} | |
private async afterEach(stepId: string | number): Promise<void> { | |
if (this.options.afterEach) return this.options.afterEach(stepId) | |
} | |
private async postInstall(): Promise<void> { | |
if (this.options.postInstall) return this.options.postInstall() | |
} | |
async run(cliArgs: {}): Promise<void> { | |
try { | |
await this.validateArgs(cliArgs) | |
} catch (err) { | |
console.error(err) | |
return | |
} | |
await this.preInstall() | |
for (const step of this.steps) { | |
await this.beforeEach(step.stepId) | |
// using `if` instead of a switch allows us to strongly type the executors | |
if (isTransformStep(step)) { | |
await this.executeTransform(step) | |
} else if (isAddDependencyStep(step)) { | |
await this.executeAddDependency(step) | |
} else if (isNewFileStep(step)) { | |
await this.executeNewFile(step) | |
} | |
await this.afterEach(step.stepId) | |
} | |
await this.postInstall() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment