Skip to content

Commit

Permalink
perf: 5-10x performance improvement on hot reloading (#58)
Browse files Browse the repository at this point in the history
* chore: register compiler types in poodle-surf

* refactor: add --mode support in `diez compile`

* fix: micromanage compiler watch host to greatly speed up builds in hot mode

* fix: stabilize scenarios where multiple hot modes are running against the same Diez project

* tweak: do not rebind assets if rebuilding in dev mode and targeting a native platform

* tweak: disable noEmitHelpers so decorators work at runtime

* chore: restore cli coverage

* feat: add the ability for arbitrary modules to extend CLI commands

* refactor: CompilerProgram holds an exandable CompilerOptions interface

* tweak: spell outputPath correctly on validator

* feat: add --baseUrl and --staticRoot command extensions for diez compile --target=web

* chore: drop --mode and fix tests

* tweak: remove needless early return

* chore: fix build script for web examples

* tweak: ensure devMode holds a proper boolean

* tweak: put @diez/targets on mono root

* chore: move stub .gitignore to facilitate releasing.

* chore: get changelog conventional

* tweak: add codecov target
  • Loading branch information
gumptious committed Nov 14, 2019
1 parent 53e06d7 commit 01b7948
Show file tree
Hide file tree
Showing 58 changed files with 575 additions and 275 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

### Features

* first Diez beta release
* first Diez beta release ([97bb558](https://github.com/diez/diez/commit/97bb558))
4 changes: 3 additions & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ coverage:
range: "70...100"

status:
project: yes
project:
default:
target: 80%
patch: no
changes: no

Expand Down
1 change: 0 additions & 1 deletion examples/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@
*/web/diez/index.js
*/web/diez/static/
*/web/diez/yarn.lock
stub/
.idea
2 changes: 1 addition & 1 deletion examples/playground/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

### Features

* first Diez beta release
* first Diez beta release ([97bb558](https://github.com/diez/diez/commit/97bb558))
2 changes: 1 addition & 1 deletion examples/playground/web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

### Features

* first Diez beta release
* first Diez beta release ([97bb558](https://github.com/diez/diez/commit/97bb558))
2 changes: 1 addition & 1 deletion examples/poodle-surf/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

### Features

* first Diez beta release
* first Diez beta release ([97bb558](https://github.com/diez/diez/commit/97bb558))
5 changes: 4 additions & 1 deletion examples/poodle-surf/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
"outDir": "lib",
"experimentalDecorators": true,
"module": "commonjs",
"strict": true
"strict": true,
"types": [
"@diez/compiler"
]
},
"include": ["src/**/*.ts"]
}
5 changes: 5 additions & 0 deletions examples/stub/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
src/
lib/
tsconfig.tsbuildinfo
.diez
node_modules
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"devDependencies": {
"@diez/cli": "1.0.0-beta.0",
"@diez/compiler": "1.0.0-beta.0",
"@diez/targets": "1.0.0-beta.0",
"@strictsoftware/typedoc-plugin-monorepo": "^0.2.1",
"@types/chalk": "^2.2.0",
"@types/fs-extra": "^5.0.4",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

### Features

* first Diez beta release
* first Diez beta release ([97bb558](https://github.com/diez/diez/commit/97bb558))
1 change: 1 addition & 0 deletions packages/cli/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ module.exports = {
moduleNameMapper: {
'^starting-point$': '<rootDir>/test/fixtures/starting-point',
'^command-provider$': '<rootDir>/test/fixtures/command-provider',
'^command-extender$': '<rootDir>/test/fixtures/command-extender',
},
};
86 changes: 71 additions & 15 deletions packages/cli/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {Command} from 'commander';

/**
* A CLI action. Receives the arguments of a CLI command.
*/
Expand All @@ -7,7 +9,16 @@ export type CliAction = (command: any, ...args: string[]) => void;
* A generic interface for a CLI command option validator. Options can be either a boolean
* (for flags like `--option`) or a string (for flags like `--option <option-value>`).
*/
export type CliOptionValidator = (value: string | boolean) => void;
export type CliOptionValidator = (commandFlags: any) => Promise<void>;

/**
* Provides a ledger for registered commands and their validators.
* @ignore
*/
export interface ValidatedCommand {
command: Command;
validators: CliOptionValidator[];
}

/**
* A generic interface for a CLI command option.
Expand All @@ -20,9 +31,9 @@ export interface CliCommandOption {
/**
* The description for the command option. Printed when the associated command is run with `--help`.
*/
description: string;
description?: string;
/**
* An optional, one-character option alias. e.g. `-d` for `--dev`. Should be specified without the leading dash.
* An optional, one-character option alias. e.g. `-d` for `--devMode`. Should be specified without the leading dash.
*/
shortName?: string;
/**
Expand All @@ -45,19 +56,64 @@ export interface CliCommandProvider {
* The name of the command.
*/
name: string;
/**
* The action that should be executed when the command is invoked by name.
*/
action: CliAction;
/**
* The command description.
*/
description: string;
/**
* A set of options the command should receive. These are passed into the action as properties
* of the first argument.
*/
options?: CliCommandOption[];
/**
* An optional pre-registration hook to modify the command before it's bootstrapped.
*/
preinstall?: (provider?: CliCommandProvider) => Promise<void>;
}

/**
* Provides a generic interface for a CLI command.
*/
export interface CliCommandProvider {
/**
* The name of the command.
*/
name: string;
/**
* The action that should be executed when the command is invoked by name.
*/
action: CliAction;
/**
* The command description.
*/
description: string;
/**
* A set of options the command should receive. These are passed into the action as properties
* of the first argument.
*/
options?: CliCommandOption[];
/**
* An optional pre-registration hook to modify the command before it's bootstrapped.
*/
preinstall?: (provider?: CliCommandProvider) => Promise<void>;
}

/**
* Provides a generic interface for a CLI command extension.
*/
export interface CliCommandExtension {
/**
* The name of the command to extend.
*/
name: string;
/**
* A set of _additional_ options the command should receive.
*/
options?: CliCommandOption[];
}

/**
Expand All @@ -68,23 +124,23 @@ export interface TargetBinding {
}

/**
* A Diez configuration, which can be provided by a module either as the `"diez"` key in `package.json` or in a separate
* `.diezrc` file located at the project root.
*
* See [here](https://github.com/diez/diez/blob/master/packages/targets/.diezrc) for an example.
* The full Diez configuration.
*/
export type DiezConfiguration = Partial<{
export interface FullDiezConfiguration {
/**
* Paths to local providers associated
*/
providers: Partial<{
commands: Iterable<string>;
extensions: Iterable<string>;
targets: Iterable<string>;
}>;
/**
* Bindings, which associate a namespaced component to a [[TargetBinding]].
*/
bindings?: {
[componentHash: string]: TargetBinding;
};
}>;
}

/**
* A Diez configuration, which can be provided by a module either as the `"diez"` key in `package.json` or in a separate
* `.diezrc` file located at the project root.
*
* See [here](https://github.com/diez/diez/blob/master/packages/targets/.diezrc) for an example.
*/
export type DiezConfiguration = Partial<FullDiezConfiguration>;
125 changes: 96 additions & 29 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,95 @@
import {args, command, help, on, parse, version} from 'commander';
import packageJson from 'package-json';
import semver from 'semver';
import {CliCommandProvider} from './api';
import {CliCommandExtension, CliCommandOption, CliCommandProvider, CliOptionValidator, ValidatedCommand} from './api';
import {fatalError, warning} from './reporting';
import {cliRequire, diezVersion, findPlugins} from './utils';

version(diezVersion).name('diez');

/**
* Registers a list of options with a command.
* @internal
*/
const registerWithProvider = (provider: CliCommandProvider) => {
const validators: (() => void)[] = [];
const registeredCommand = command(provider.name)
.description(provider.description)
.action(async (...options: any[]) => {
try {
for (const validator of validators) {
await validator();
}
await provider.action.call(undefined, registeredCommand, ...options);
} catch (error) {
fatalError(error.message);
}
});
const registerOptions = (validatedCommand: ValidatedCommand, options?: CliCommandOption[]) => {
if (!options) {
return;
}

for (const option of options) {
let optionText = option.shortName ? `-${option.shortName}, --${option.longName}` : `--${option.longName}`;
if (option.valueName) {
optionText += ` <${option.valueName}>`;
}
validatedCommand.command.option(optionText, option.description);
if (!option.validator) {
continue;
}

validatedCommand.validators.push(option.validator);
}
};

/**
* Registers a command from its provider.
* @internal
*/
const registerWithProvider = async (provider: CliCommandProvider) => {
if (provider.preinstall) {
await provider.preinstall();
}

const validators: CliOptionValidator[] = [];

if (provider.options) {
for (const option of provider.options) {
let optionText = option.shortName ? `-${option.shortName}, --${option.longName}` : `--${option.longName}`;
if (option.valueName) {
optionText += ` <${option.valueName}>`;
const registeredCommand = command(provider.name).action(async (...options: any[]) => {
try {
for (const validator of validators) {
await validator(registeredCommand);
}
registeredCommand.option(optionText, option.description);
if (!option.validator) {
await provider.action.call(undefined, registeredCommand, ...options);
} catch (error) {
fatalError(error.message);
}
});

const validatedCommand = {
validators,
command: registeredCommand,
};

if (provider.description) {
registeredCommand.description(provider.description);
}

registerOptions(validatedCommand, provider.options);

return validatedCommand;
};

/**
* Registers with providers.
* @internal
*/
const registerWithProviders = async (
registry: Map<string, ValidatedCommand>,
plugin: string,
providers?: Iterable<string>,
) => {
if (!providers) {
return;
}

for (const path of providers) {
try {
const provider = cliRequire(plugin, path) as CliCommandProvider;
if (registry.has(provider.name)) {
warning(`Ignoring attempt to reregister command ${provider.name}.`);
continue;
}

validators.push(() => {
option.validator!(registeredCommand[option.longName]);
});
registry.set(provider.name, await registerWithProvider(provider));
} catch (error) {
warning(`An invalid command provider was specified at ${path}.`);
}
}
};
Expand All @@ -59,19 +110,35 @@ export const bootstrap = async (rootPackageName = global.process.cwd()) => {
}

const plugins = await findPlugins(rootPackageName);
const registeredCommands = new Map<string, ValidatedCommand>();
const deferredExtensions: CliCommandExtension[] = [];
for (const [plugin, {providers}] of plugins) {
if (!providers || !providers.commands) {
if (!providers) {
continue;
}

await registerWithProviders(registeredCommands, plugin, providers.commands);

if (!providers.extensions) {
continue;
}

for (const path of providers.commands) {
for (const path of providers.extensions) {
try {
registerWithProvider(cliRequire(plugin, path));
deferredExtensions.push(cliRequire(plugin, path));
} catch (error) {
warning('An invalid command provider was specified in the Diez configuration.');
warning(`An invalid command extension was specified at ${path}.`);
}
}
}

for (const extension of deferredExtensions) {
const validatedCommand = registeredCommands.get(extension.name);
if (!validatedCommand) {
continue;
}
registerOptions(validatedCommand, extension.options);
}
};

/**
Expand Down
Loading

0 comments on commit 01b7948

Please sign in to comment.