diff --git a/package.json b/package.json index 167de6f51..4638c5fcf 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,7 @@ "scripts": { "prepare": "beemo create-config --silent", "setup": "beemo typescript", - "build": "NODE_ENV=production yarn run packemon build --addEngines --generateDeclaration=api", - "build:fast": "yarn run packemon build", + "build": "yarn run packemon build", "validate": "yarn run packemon validate", "ci": "yarn run type && yarn run test && yarn run lint", "clean": "rm -rf {build,dts,lib}", @@ -22,7 +21,8 @@ "release": "npx np --yolo", "test": "beemo jest", "type": "beemo typescript --noEmit", - "prerelease": "yarn run ci && yarn run setup && yarn run build && yarn run validate", + "prerelease": "yarn run ci && yarn run setup && yarn run pack", + "pack": "NODE_ENV=production yarn run packemon pack --addEngines --generateDeclaration=api", "packemon": "node ./build/bin.js" }, "repository": "https://github.com/milesj/packemon.git", diff --git a/src/BundleArtifact.ts b/src/BundleArtifact.ts index ffc790176..034ab0196 100644 --- a/src/BundleArtifact.ts +++ b/src/BundleArtifact.ts @@ -97,7 +97,7 @@ export default class BundleArtifact extends Artifact { toArray(output).map(async (out, index) => { const { originalFormat = 'lib', ...outOptions } = out; - this.debug('- Writing `%s` output', originalFormat); + this.debug(' - Writing `%s` output', originalFormat); const result = await bundle.write(outOptions); diff --git a/src/Package.ts b/src/Package.ts index 51094b57e..256c3415c 100644 --- a/src/Package.ts +++ b/src/Package.ts @@ -79,7 +79,7 @@ export default class Package { } async cleanup(): Promise { - this.debug('Cleaning artifacts'); + this.debug('Cleaning build artifacts'); await Promise.all(this.artifacts.map((artifact) => artifact.cleanup())); } @@ -104,7 +104,7 @@ export default class Package { if (this.hasDependency('react')) { flags.react = true; - this.debug('- React'); + this.debug(' - React'); } // TypeScript @@ -120,9 +120,9 @@ export default class Package { flags.strict = Boolean(tsConfig?.options.strict); this.debug( - `- TypeScript (${flags.strict ? 'strict' : 'non-strict'}, ${ - flags.decorators ? 'decorators' : 'non-decorators' - })`, + ' - TypeScript (%s, %s)', + flags.strict ? 'strict' : 'non-strict', + flags.decorators ? 'decorators' : 'non-decorators', ); } @@ -136,7 +136,7 @@ export default class Package { ) { flags.flow = true; - this.debug('- Flow'); + this.debug(' - Flow'); } return flags; diff --git a/src/PackageValidator.ts b/src/PackageValidator.ts index fdc52f4b0..9c1e627b4 100644 --- a/src/PackageValidator.ts +++ b/src/PackageValidator.ts @@ -317,7 +317,7 @@ export default class PackageValidator { } if (isObject(repo)) { - const dir = (repo as { directory?: string }).directory; + const dir = repo.directory; if (dir && !this.doesPathExist(dir)) { this.errors.push(`Repository directory "${dir}" does not exist.`); diff --git a/src/Packemon.ts b/src/Packemon.ts index c127c2444..dfcd0726f 100644 --- a/src/Packemon.ts +++ b/src/Packemon.ts @@ -67,7 +67,7 @@ export default class Packemon { readonly root: Path; - constructor(cwd: string) { + constructor(cwd: string = process.cwd()) { this.root = Path.resolve(cwd); this.project = new Project(this.root); @@ -76,7 +76,7 @@ export default class Packemon { this.project.checkEngineVersionConstraint(); } - async build(baseOptions: BuildOptions) { + async build(baseOptions: Partial) { debug('Starting `build` process'); const options = optimal(baseOptions, { @@ -108,8 +108,6 @@ export default class Packemon { }); }); - debug('Building artifacts'); - const { errors } = await pipeline.run(); // Always cleanup whether a successful or failed build @@ -146,13 +144,11 @@ export default class Packemon { pathsToRemove.push(`./${formatFolders}`); } - debug('Cleaning build artifacts'); - await Promise.all( pathsToRemove.map( (path) => new Promise((resolve, reject) => { - debug('- %s', path); + debug(' - %s', path); rimraf(path, (error) => { if (error) { @@ -166,7 +162,7 @@ export default class Packemon { ); } - async validate(baseOptions: ValidateOptions): Promise { + async validate(baseOptions: Partial): Promise { debug('Starting `validate` process'); const options = optimal(baseOptions, { @@ -191,6 +187,10 @@ export default class Packemon { } protected async findPackages(skipPrivate: boolean = false) { + if (this.packages.length > 0) { + return; + } + debug('Finding packages in project'); const pkgPaths: Path[] = []; @@ -221,7 +221,7 @@ export default class Packemon { const contents = json.parse(await fs.readFile(pkgPath.path(), 'utf8')); debug( - '- %s: %s', + ' - %s: %s', contents.name, pkgPath.path().replace(this.root.path(), '').replace('package.json', ''), ); @@ -284,7 +284,7 @@ export default class Packemon { pkg.addArtifact(artifact); } - debug('- %s: %s', pkg.getName(), pkg.artifacts.join(', ')); + debug(' - %s: %s', pkg.getName(), pkg.artifacts.join(', ')); }); } diff --git a/src/bin.ts b/src/bin.ts index 8fa294058..7a582300f 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,5 +1,5 @@ import { Program, checkPackageOutdated } from '@boost/cli'; -import { BuildCommand, CleanCommand, ValidateCommand } from '.'; +import { BuildCommand, CleanCommand, PackCommand, ValidateCommand } from '.'; const version = String(require('../package.json').version); @@ -15,6 +15,7 @@ async function run() { .middleware(checkPackageOutdated('packemon', version)) .register(new BuildCommand()) .register(new CleanCommand()) + .register(new PackCommand()) .register(new ValidateCommand()); await program.runAndExit(process.argv); diff --git a/src/commands/Build.tsx b/src/commands/Build.tsx index f3ca47941..fb5fb03ac 100644 --- a/src/commands/Build.tsx +++ b/src/commands/Build.tsx @@ -5,10 +5,8 @@ import Build from '../components/Build'; import Packemon from '../Packemon'; import { AnalyzeType, BuildOptions, DeclarationType } from '../types'; -export type BuildParams = [string]; - @Config('build', 'Build standardized packages for distribution') -export class BuildCommand extends Command { +export class BuildCommand extends Command { @Arg.Flag('Add `engine` versions to each `package.json`') addEngines: boolean = false; @@ -34,15 +32,10 @@ export class BuildCommand extends Command({ - description: 'Project root that contains a `package.json`', - label: 'cwd', - type: 'string', - }) - run(cwd: string = process.cwd()) { + run() { return ( { - @Arg.Params({ - description: 'Project root that contains a `package.json`', - label: 'cwd', - type: 'string', - }) - async run(cwd: string = process.cwd()) { - await new Packemon(cwd).clean(); + async run() { + await new Packemon().clean(); } } diff --git a/src/commands/Pack.tsx b/src/commands/Pack.tsx new file mode 100644 index 000000000..6d6471d0b --- /dev/null +++ b/src/commands/Pack.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Config } from '@boost/cli'; +import Pack from '../components/Pack'; +import Packemon from '../Packemon'; +import { BuildCommand } from './Build'; + +@Config('pack', 'Clean, build, and validate packages for distribution') +export class PackCommand extends BuildCommand { + run() { + return ( + + ); + } +} diff --git a/src/commands/Validate.tsx b/src/commands/Validate.tsx index c6c1cb491..1bd6c1c91 100644 --- a/src/commands/Validate.tsx +++ b/src/commands/Validate.tsx @@ -4,10 +4,8 @@ import Packemon from '../Packemon'; import { ValidateOptions } from '../types'; import Validate from '../components/Validate'; -export type ValidateParams = [string]; - @Config('validate', 'Validate package metadata and configuration') -export class ValidateCommand extends Command { +export class ValidateCommand extends Command { @Arg.Flag('Check that dependencies have valid versions and constraints') deps: boolean = true; @@ -29,22 +27,18 @@ export class ValidateCommand extends Command({ - description: 'Project root that contains a `package.json`', - label: 'cwd', - type: 'string', - }) - async run(cwd: string = process.cwd()) { - const validators = await new Packemon(cwd).validate({ - deps: this.deps, - engines: this.engines, - entries: this.entries, - license: this.license, - links: this.links, - people: this.people, - repo: this.repo, - }); - - return ; + run() { + return ( + + ); } } diff --git a/src/components/Build.tsx b/src/components/Build.tsx index 7189b920e..e131548e8 100644 --- a/src/components/Build.tsx +++ b/src/components/Build.tsx @@ -1,29 +1,28 @@ -import React, { useEffect, useReducer, useRef, useState } from 'react'; +import React, { useRef, useState } from 'react'; import { Box, Static } from 'ink'; -import { Header } from '@boost/cli'; +import { Header, useProgram } from '@boost/cli'; import Packemon from '../Packemon'; import PackageList from './PackageList'; import PackageRow from './PackageRow'; import Package from '../Package'; import { BuildOptions } from '../types'; +import useRenderLoop from './hooks/useRenderLoop'; +import useOnMount from './hooks/useOnMount'; -export interface BuildProps extends Required { +export interface BuildProps extends Partial { packemon: Packemon; + onBuilt?: () => void; } -export default function Build({ packemon, ...options }: BuildProps) { - const [, forceUpdate] = useReducer((count) => count + 1, 0); - const [error, setError] = useState(); +export default function Build({ packemon, onBuilt, ...options }: BuildProps) { + const { exit } = useProgram(); const [staticPackages, setStaticPackages] = useState([]); const staticNames = useRef(new Set()); + const clearLoop = useRenderLoop(); - useEffect(() => { - // Continuously render at 30 FPS - const timer = setInterval(forceUpdate, 1000 / 30); - const clear = () => clearInterval(timer); - - // Run the packemon process on mount - void packemon.build(options).catch(setError).finally(clear); + // Run the build process on mount + useOnMount(() => { + void packemon.build(options).then(onBuilt).catch(exit).finally(clearLoop); // Add complete packages to the static list const unlisten = packemon.onPackageBuilt.listen((pkg) => { @@ -34,23 +33,17 @@ export default function Build({ packemon, ...options }: BuildProps) { }); return () => { - clear(); + clearLoop(); unlisten(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Bubble up errors to the program - if (error) { - throw error; - } + }); const runningPackages = packemon.packages.filter((pkg) => pkg.isRunning()); return ( <> - {(pkg) => } + {(pkg) => } {runningPackages.length > 0 && ( diff --git a/src/components/Pack.tsx b/src/components/Pack.tsx new file mode 100644 index 000000000..40c8ab3f0 --- /dev/null +++ b/src/components/Pack.tsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; +import { useProgram, Header } from '@boost/cli'; +import Packemon from '../Packemon'; +import { BuildOptions } from '../types'; +import Build from './Build'; +import Validate from './Validate'; +import useOnMount from './hooks/useOnMount'; + +export interface PackProps extends BuildOptions { + packemon: Packemon; +} + +export default function Pack({ packemon, ...options }: PackProps) { + const { exit } = useProgram(); + const [phase, setPhase] = useState('clean'); + + // Start the clean process first + useOnMount(() => { + void packemon + .clean() + .then(() => { + setPhase('build'); + }) + .catch(exit); + }); + + return ( + <> + {phase === 'clean' &&
} + + {phase === 'build' && ( + { + setPhase('validate'); + }} + /> + )} + + {phase === 'validate' && ( + { + setPhase('packed'); + }} + /> + )} + + ); +} diff --git a/src/components/PackageList.tsx b/src/components/PackageList.tsx index d3208f509..e55d758e6 100644 --- a/src/components/PackageList.tsx +++ b/src/components/PackageList.tsx @@ -52,7 +52,7 @@ export default function PackageList({ packages }: PackageListProps) { return ( <> {visiblePackages.map((pkg) => ( - + ))} ); diff --git a/src/components/Validate.tsx b/src/components/Validate.tsx index f525dfe34..a9b9e062a 100644 --- a/src/components/Validate.tsx +++ b/src/components/Validate.tsx @@ -1,42 +1,64 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { Box } from 'ink'; -import { useProgram } from '@boost/cli'; +import { Header, useProgram } from '@boost/cli'; +import Packemon from '../Packemon'; import PackageValidator from '../PackageValidator'; import ValidateRow from './ValidateRow'; +import { ValidateOptions } from '../types'; +import useRenderLoop from './hooks/useRenderLoop'; +import useOnMount from './hooks/useOnMount'; -export interface ValidateProps { - validators: PackageValidator[]; +export interface ValidateProps extends Partial { + packemon: Packemon; + onValidated?: () => void; } -export default function Validate({ validators }: ValidateProps) { +export default function Validate({ packemon, onValidated, ...options }: ValidateProps) { const { exit } = useProgram(); - const failedValidators = validators.filter( - (validator) => validator.hasErrors() || validator.hasWarnings(), - ); - const errorCount = failedValidators.filter((validator) => validator.hasErrors()).length; - const errorMessage = - errorCount === 1 - ? `Found errors in ${failedValidators[0].package.getName()} package!` - : `Found errors in ${errorCount} packages!`; + const clearLoop = useRenderLoop(); + const [isValidating, setIsValidating] = useState(true); + const [failedValidators, setFailedValidators] = useState([]); + + // Run the validate process on mount + useOnMount(() => { + void packemon + .validate(options) + .then((validators) => { + setIsValidating(false); + setFailedValidators( + validators.filter((validator) => validator.hasErrors() || validator.hasWarnings()), + ); + + onValidated?.(); + }) + .catch(exit) + .finally(clearLoop); + + return clearLoop; + }); + // Exit validation if there are any errors useEffect(() => { + const errorCount = failedValidators.filter((validator) => validator.hasErrors()).length; + const errorMessage = + errorCount === 1 + ? `Found errors in ${failedValidators[0].package.getName()} package!` + : `Found errors in ${errorCount} packages!`; + if (errorCount > 0) { exit(`Validation failed. ${errorMessage}`); } - }, [errorCount, errorMessage, exit]); - - if (failedValidators.length === 0) { - return null; - } + }, [failedValidators, exit]); return ( - <> + + {(isValidating || failedValidators.length > 0) && ( +
+ )} + {failedValidators.map((validator, i) => ( - - {i > 0 && } - - + ))} - + ); } diff --git a/src/components/ValidateRow.tsx b/src/components/ValidateRow.tsx index 48064fc2a..b5c465b06 100644 --- a/src/components/ValidateRow.tsx +++ b/src/components/ValidateRow.tsx @@ -12,7 +12,7 @@ export interface ValidateRowProps { export default function ValidateRow({ validator }: ValidateRowProps) { return ( - +