Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new: Add watch command. #11

Merged
merged 4 commits into from
Nov 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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