Skip to content

Commit

Permalink
new: Add watch command. (#11)
Browse files Browse the repository at this point in the history
* Start on watch command.

* Use debounce.

* Update types.

* Add docs.
  • Loading branch information
milesj authored Nov 15, 2020
1 parent 31508cc commit 16f297e
Show file tree
Hide file tree
Showing 14 changed files with 193 additions and 51 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"package"
],
"scripts": {
"docs": "cd website && yarn run start",
"prepare": "beemo create-config --silent",
"setup": "beemo typescript",
"build": "yarn run packemon build",
Expand Down Expand Up @@ -86,6 +87,7 @@
"babel-plugin-transform-async-to-promises": "^0.8.15",
"babel-plugin-transform-dev": "^2.0.1",
"builtin-modules": "^3.1.0",
"chokidar": "^3.4.3",
"execa": "^4.1.0",
"fast-glob": "^3.2.4",
"filesize": "^6.1.0",
Expand Down
54 changes: 28 additions & 26 deletions src/Packemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
toArray,
WorkspacePackage,
} from '@boost/common';
import { createDebugger } from '@boost/debug';
import { createDebugger, Debugger } from '@boost/debug';
import { Event } from '@boost/event';
import { PooledPipeline, Context } from '@boost/pipeline';
import Package from './Package';
Expand All @@ -31,7 +31,6 @@ import {
ValidateOptions,
} from './types';

const debug = createDebugger('packemon:core');
const { array, bool, custom, number, object, string, union } = predicates;

const platformPredicate = string<Platform>('browser').oneOf(['node', 'browser']);
Expand Down Expand Up @@ -59,6 +58,8 @@ const blueprint: Blueprint<Required<PackemonPackageConfig>> = {
};

export default class Packemon {
readonly debug: Debugger;

readonly onPackageBuilt = new Event<[Package]>('package-built');

packages: Package[] = [];
Expand All @@ -70,14 +71,15 @@ export default class Packemon {
constructor(cwd: string = process.cwd()) {
this.root = Path.resolve(cwd);
this.project = new Project(this.root);
this.debug = createDebugger('packemon:core');

debug('Initializing packemon in project %s', this.root);
this.debug('Initializing packemon in project %s', this.root);

this.project.checkEngineVersionConstraint();
}

async build(baseOptions: Partial<BuildOptions>) {
debug('Starting `build` process');
this.debug('Starting `build` process');

const options = optimal(baseOptions, {
addEngines: bool(),
Expand Down Expand Up @@ -120,7 +122,7 @@ export default class Packemon {
}

async clean() {
debug('Starting `clean` process');
this.debug('Starting `clean` process');

await this.findPackages();
await this.cleanTemporaryFiles();
Expand Down Expand Up @@ -148,7 +150,7 @@ export default class Packemon {
pathsToRemove.map(
(path) =>
new Promise((resolve, reject) => {
debug(' - %s', path);
this.debug(' - %s', path);

rimraf(path, (error) => {
if (error) {
Expand All @@ -163,7 +165,7 @@ export default class Packemon {
}

async validate(baseOptions: Partial<ValidateOptions>): Promise<PackageValidator[]> {
debug('Starting `validate` process');
this.debug('Starting `validate` process');

const options = optimal(baseOptions, {
deps: bool(true),
Expand All @@ -180,47 +182,41 @@ export default class Packemon {
return Promise.all(this.packages.map((pkg) => new PackageValidator(pkg).validate(options)));
}

protected async cleanTemporaryFiles() {
debug('Cleaning temporary build files');

await Promise.all(this.packages.map((pkg) => pkg.cleanup()));
}

protected async findPackages(skipPrivate: boolean = false) {
async findPackages(skipPrivate: boolean = false) {
if (this.packages.length > 0) {
return;
}

debug('Finding packages in project');
this.debug('Finding packages in project');

const pkgPaths: Path[] = [];

this.project.workspaces = this.project.getWorkspaceGlobs({ relative: true });

// Multi package repo
if (this.project.workspaces.length > 0) {
debug('Workspaces enabled, finding packages using globs');
this.debug('Workspaces enabled, finding packages using globs');

this.project.getWorkspacePackagePaths().forEach((filePath) => {
pkgPaths.push(Path.create(filePath).append('package.json'));
});

// Single package repo
} else {
debug('Not workspaces enabled, using root as package');
this.debug('Not workspaces enabled, using root as package');

pkgPaths.push(this.root.append('package.json'));
}

debug('Found %d package(s)', pkgPaths.length);
this.debug('Found %d package(s)', pkgPaths.length);

const privatePackageNames: string[] = [];

let packages: WorkspacePackage<PackemonPackage>[] = await Promise.all(
pkgPaths.map(async (pkgPath) => {
const contents = json.parse<PackemonPackage>(await fs.readFile(pkgPath.path(), 'utf8'));

debug(
this.debug(
' - %s: %s',
contents.name,
pkgPath.path().replace(this.root.path(), '').replace('package.json', ''),
Expand All @@ -241,14 +237,14 @@ export default class Packemon {
if (skipPrivate) {
packages = packages.filter((pkg) => !pkg.package.private);

debug('Filtering private packages: %s', privatePackageNames.join(', '));
this.debug('Filtering private packages: %s', privatePackageNames.join(', '));
}

this.packages = this.validateAndPreparePackages(packages);
}

protected generateArtifacts(declarationType: DeclarationType) {
debug('Generating build artifacts for packages');
generateArtifacts(declarationType?: DeclarationType) {
this.debug('Generating build artifacts for packages');

this.packages.forEach((pkg) => {
const typesBuilds: TypesBuild[] = [];
Expand Down Expand Up @@ -277,17 +273,23 @@ export default class Packemon {
});
});

if (declarationType !== 'none') {
if (declarationType && declarationType !== 'none') {
const artifact = new TypesArtifact(pkg, typesBuilds);
artifact.declarationType = declarationType;

pkg.addArtifact(artifact);
}

debug(' - %s: %s', pkg.getName(), pkg.artifacts.join(', '));
this.debug(' - %s: %s', pkg.getName(), pkg.artifacts.join(', '));
});
}

protected async cleanTemporaryFiles() {
this.debug('Cleaning temporary build files');

await Promise.all(this.packages.map((pkg) => pkg.cleanup()));
}

protected requiresSharedLib(pkg: Package): boolean {
const platformsToBuild = new Set<Platform>();
let libFormatCount = 0;
Expand All @@ -308,13 +310,13 @@ export default class Packemon {
}

protected validateAndPreparePackages(packages: WorkspacePackage<PackemonPackage>[]): Package[] {
debug('Validating found packages');
this.debug('Validating found packages');

const nextPackages: Package[] = [];

packages.forEach(({ metadata, package: contents }) => {
if (!contents.packemon) {
debug('No `packemon` configuration found for %s, skipping', contents.name);
this.debug('No `packemon` configuration found for %s, skipping', contents.name);

return;
}
Expand Down
5 changes: 3 additions & 2 deletions src/bin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Program, checkPackageOutdated } from '@boost/cli';
import { BuildCommand, CleanCommand, PackCommand, ValidateCommand } from '.';
import { BuildCommand, CleanCommand, PackCommand, ValidateCommand, WatchCommand } from '.';

const version = String(require('../package.json').version);

Expand All @@ -16,7 +16,8 @@ async function run() {
.register(new BuildCommand())
.register(new CleanCommand())
.register(new PackCommand())
.register(new ValidateCommand());
.register(new ValidateCommand())
.register(new WatchCommand());

await program.runAndExit(process.argv);
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/Build.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Packemon from '../Packemon';
import { AnalyzeType, BuildOptions, DeclarationType } from '../types';

@Config('build', 'Build standardized packages for distribution')
export class BuildCommand extends Command<GlobalOptions & BuildOptions> {
export class BuildCommand extends Command<GlobalOptions & Required<BuildOptions>> {
@Arg.Flag('Add `engine` versions to each `package.json`')
addEngines: boolean = false;

Expand Down
4 changes: 1 addition & 3 deletions src/commands/Clean.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Command, Config, GlobalOptions } from '@boost/cli';
import Packemon from '../Packemon';

export type CleanParams = [string];

@Config('clean', 'Clean build artifacts from packages')
export class CleanCommand extends Command<GlobalOptions, CleanParams> {
export class CleanCommand extends Command<GlobalOptions> {
async run() {
await new Packemon().clean();
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/Validate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ValidateOptions } from '../types';
import Validate from '../components/Validate';

@Config('validate', 'Validate package metadata and configuration')
export class ValidateCommand extends Command<GlobalOptions & ValidateOptions> {
export class ValidateCommand extends Command<GlobalOptions & Required<ValidateOptions>> {
@Arg.Flag('Check that dependencies have valid versions and constraints')
deps: boolean = true;

Expand Down
113 changes: 113 additions & 0 deletions src/commands/Watch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { applyStyle, Arg, Command, Config, GlobalOptions } from '@boost/cli';
import { Bind, formatMs } from '@boost/common';
import chokidar from 'chokidar';
import Package from '../Package';
import Packemon from '../Packemon';

@Config('watch', 'Watch local files for changes and rebuild')
export class WatchCommand extends Command<GlobalOptions> {
@Arg.Number('Number of milliseconds to wait after a change before triggering a rebuild')
debounce: number = 150;

@Arg.Flag('Poll for file changes instead of using file system events')
poll: boolean = false;

protected packemon!: Packemon;

protected packagesToRebuild = new Set<Package>();

protected rebuilding: boolean = false;

protected rebuildTimer?: NodeJS.Timeout;

async run() {
const packemon = new Packemon();

this.packemon = packemon;
packemon.debug('Starting `watch` process');

// Generate all our build artifacts
await packemon.findPackages();
await packemon.generateArtifacts();

// Instantiate the watcher for each package source
const watchPaths = packemon.packages.map((pkg) => pkg.path.append('src/**/*').path());

packemon.debug('Initializing chokidar watcher for paths:');
packemon.debug(watchPaths.map((path) => ` - ${path}`).join('\n'));

const watcher = chokidar.watch(watchPaths, {
ignored: /(^|[/\\])\../u, // dotfiles
ignoreInitial: true,
persistent: true,
usePolling: this.poll,
});

// Rebuild when files change
watcher.on('all', this.enqueueRebuild);

this.log('Watching for changes...');
}

enqueueRebuild = (event: string, path: string) => {
if (event !== 'add' && event !== 'change' && event !== 'unlink') {
return;
}

this.log(applyStyle(' - %s', 'muted'), path.replace(`${this.packemon.root.path()}/`, ''));

const changedPkg = this.packemon.packages.find((pkg) => path.startsWith(pkg.path.path()));

if (changedPkg) {
this.packagesToRebuild.add(changedPkg);
this.triggerRebuilds();
}
};

triggerRebuilds() {
if (this.rebuildTimer) {
clearTimeout(this.rebuildTimer);
}

this.rebuildTimer = setTimeout(() => {
void this.rebuildPackages();
}, this.debounce);
}

@Bind()
async rebuildPackages() {
if (this.rebuilding) {
this.triggerRebuilds();

return;
}

const pkgs = Array.from(this.packagesToRebuild);
const pkgNames = pkgs.map((pkg) => pkg.getName());

if (pkgs.length === 0) {
return;
}

this.packagesToRebuild.clear();
this.rebuilding = true;

try {
const start = Date.now();

await Promise.all(pkgs.map((pkg) => pkg.build({})));

this.log(
applyStyle('Built %s in %s', 'success'),
pkgNames.join(', '),
formatMs(Date.now() - start),
);
} catch (error) {
this.log.error(error.message);

this.log(applyStyle('Failed to build %s', 'failure'), pkgNames.join(', '));
} finally {
this.rebuilding = false;
}
}
}
2 changes: 1 addition & 1 deletion src/components/Build.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { BuildOptions } from '../types';
import useRenderLoop from './hooks/useRenderLoop';
import useOnMount from './hooks/useOnMount';

export interface BuildProps extends Partial<BuildOptions> {
export interface BuildProps extends BuildOptions {
packemon: Packemon;
onBuilt?: () => void;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Validate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ValidateOptions } from '../types';
import useRenderLoop from './hooks/useRenderLoop';
import useOnMount from './hooks/useOnMount';

export interface ValidateProps extends Partial<ValidateOptions> {
export interface ValidateProps extends ValidateOptions {
packemon: Packemon;
onValidated?: () => void;
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from './commands/Build';
export * from './commands/Clean';
export * from './commands/Pack';
export * from './commands/Validate';
export * from './commands/Watch';
export * from './constants';
export * from './types';

Expand Down
Loading

0 comments on commit 16f297e

Please sign in to comment.