@travetto/cli 7.0.0-rc.2 → 7.0.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -472,7 +472,7 @@ import type { WebHttpServer } from '../src/types.ts';
472
472
  /**
473
473
  * Run a web server
474
474
  */
475
- @CliCommand({ runTarget: true, with: { debugIpc: true, canRestart: true, module: true, env: true } })
475
+ @CliCommand({ runTarget: true, with: { debugIpc: true, restartForDev: true, module: true, env: true } })
476
476
  export class WebHttpCommand implements CliCommandShape {
477
477
 
478
478
  /** Port to run on */
@@ -502,7 +502,7 @@ export class WebHttpCommand implements CliCommandShape {
502
502
  }
503
503
  ```
504
504
 
505
- As noted in the example above, `fields` is specified in this execution, with support for `module`, and `env`. These env flag is directly tied to the [Runtime](https://github.com/travetto/travetto/tree/main/module/runtime/src/context.ts#L12) `name` defined in the [Runtime](https://github.com/travetto/travetto/tree/main/module/runtime#readme "Runtime for travetto applications.") module.
505
+ As noted in the example above, `fields` is specified in this execution, with support for `module`, and `env`. These env flag is directly tied to the [Runtime](https://github.com/travetto/travetto/tree/main/module/runtime/src/context.ts#L13) `name` defined in the [Runtime](https://github.com/travetto/travetto/tree/main/module/runtime#readme "Runtime for travetto applications.") module.
506
506
 
507
507
  The `module` field is slightly more complex, but is geared towards supporting commands within a monorepo context. This flag ensures that a module is specified if running from the root of the monorepo, and that the module provided is real, and can run the desired command. When running from an explicit module folder in the monorepo, the module flag is ignored.
508
508
 
package/bin/trv.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ globalThis.__entry_point__ = __filename;
4
+
3
5
  // @ts-check
4
6
  require('@travetto/compiler/bin/entry.common.js')
5
7
  .load(operations => operations.exec('@travetto/cli/support/entry.trv.js'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/cli",
3
- "version": "7.0.0-rc.2",
3
+ "version": "7.0.0-rc.3",
4
4
  "description": "CLI infrastructure for Travetto framework",
5
5
  "keywords": [
6
6
  "cli",
@@ -28,8 +28,8 @@
28
28
  "directory": "module/cli"
29
29
  },
30
30
  "dependencies": {
31
- "@travetto/schema": "^7.0.0-rc.2",
32
- "@travetto/terminal": "^7.0.0-rc.2"
31
+ "@travetto/schema": "^7.0.0-rc.3",
32
+ "@travetto/terminal": "^7.0.0-rc.3"
33
33
  },
34
34
  "travetto": {
35
35
  "displayName": "Command Line Interface",
package/src/error.ts CHANGED
@@ -21,7 +21,7 @@ const COMMAND_PACKAGE = [
21
21
  export class CliUnknownCommandError extends Error {
22
22
 
23
23
  #getMissingCommandHelp(cmd: string): string | undefined {
24
- const matchedConfig = COMMAND_PACKAGE.find(([re]) => re.test(cmd));
24
+ const matchedConfig = COMMAND_PACKAGE.find(([regex]) => regex.test(cmd));
25
25
  if (matchedConfig) {
26
26
  const [, pkg, prod] = matchedConfig;
27
27
  const install = PackageUtil.getInstallCommand(Runtime, `@travetto/${pkg}`, prod);
@@ -19,8 +19,8 @@ type CliCommandConfigOptions = {
19
19
  module?: boolean;
20
20
  /** Should debug invocation trigger via ipc */
21
21
  debugIpc?: boolean;
22
- /** Should the invocation automatically restart on exit */
23
- canRestart?: boolean;
22
+ /** Should restart on code change */
23
+ restartForDev?: boolean;
24
24
  };
25
25
  };
26
26
 
@@ -53,7 +53,7 @@ const FIELD_CONFIG: {
53
53
  },
54
54
  {
55
55
  name: 'debugIpc',
56
- run: cmd => CliUtil.debugIfIpc(cmd).then((flag) => flag && process.exit(0)),
56
+ run: cmd => CliUtil.debugIfIpc(cmd).then((executed) => executed && process.exit(0)),
57
57
  field: {
58
58
  type: Boolean,
59
59
  aliases: ['-di'],
@@ -63,13 +63,13 @@ const FIELD_CONFIG: {
63
63
  },
64
64
  },
65
65
  {
66
- name: 'canRestart',
67
- run: cmd => CliUtil.runWithRestart(cmd)?.then((flag) => flag && process.exit(0)),
66
+ name: 'restartForDev',
67
+ run: cmd => CliUtil.runWithRestartOnCodeChanges(cmd),
68
68
  field: {
69
69
  type: Boolean,
70
70
  aliases: ['-cr'],
71
- description: 'Should the invocation automatically restart on exit',
72
- default: false,
71
+ description: 'Should the invocation automatically restart on source changes',
72
+ default: Runtime.env === 'development',
73
73
  required: { active: false },
74
74
  },
75
75
  }
@@ -27,7 +27,7 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
27
27
  (schema.fields ??= {}).help = {
28
28
  type: Boolean,
29
29
  name: 'help',
30
- owner: this.#cls,
30
+ class: this.#cls,
31
31
  description: 'display help for command',
32
32
  required: { active: false },
33
33
  default: false,
@@ -1,4 +1,4 @@
1
- import { Class, getClass, getParentClass, Runtime, RuntimeIndex } from '@travetto/runtime';
1
+ import { Class, getClass, getParentClass, isClass, Runtime, RuntimeIndex } from '@travetto/runtime';
2
2
  import { RegistryAdapter, RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
3
3
  import { SchemaClassConfig, SchemaRegistryIndex } from '@travetto/schema';
4
4
 
@@ -32,6 +32,8 @@ export class CliCommandRegistryIndex implements RegistryIndex {
32
32
 
33
33
  store = new RegistryIndexStore(CliCommandRegistryAdapter);
34
34
 
35
+ /** @private */ constructor(source: unknown) { Registry.validateConstructor(source); }
36
+
35
37
  /**
36
38
  * Get list of all commands available
37
39
  */
@@ -50,11 +52,6 @@ export class CliCommandRegistryIndex implements RegistryIndex {
50
52
  return this.#fileMapping;
51
53
  }
52
54
 
53
-
54
- process(): void {
55
- // Do nothing for now?
56
- }
57
-
58
55
  /**
59
56
  * Import command into an instance
60
57
  */
@@ -70,7 +67,7 @@ export class CliCommandRegistryIndex implements RegistryIndex {
70
67
  const found = this.#commandMapping.get(name)!;
71
68
  const values = Object.values(await Runtime.importFrom<Record<string, Class>>(found));
72
69
  const filtered = values
73
- .filter((value): value is Class => typeof value === 'function')
70
+ .filter(isClass)
74
71
  .reduce<Class[]>((classes, cls) => {
75
72
  const parent = getParentClass(cls);
76
73
  if (parent && !classes.includes(parent)) {
@@ -87,7 +84,7 @@ export class CliCommandRegistryIndex implements RegistryIndex {
87
84
  // Initialize any uninitialized commands
88
85
  if (uninitialized.length) {
89
86
  // Ensure processed
90
- Registry.process(uninitialized.map(cls => ({ type: 'added', current: cls })));
87
+ Registry.process(uninitialized);
91
88
  }
92
89
 
93
90
  for (const cls of values) {
@@ -35,8 +35,8 @@ export interface CliCommandSchema<K extends string = string> {
35
35
  export class CliSchemaExportUtil {
36
36
 
37
37
  /**
38
- * Get the base type for a CLI command input
39
- */
38
+ * Get the base type for a CLI command input
39
+ */
40
40
  static baseInputType(config: SchemaInputConfig): Pick<CliCommandInput, 'type' | 'fileExtensions'> {
41
41
  switch (config.type) {
42
42
  case Date: return { type: 'date' };
package/src/types.ts CHANGED
@@ -88,9 +88,9 @@ export type CliCommandShapeFields = {
88
88
  */
89
89
  debugIpc?: boolean;
90
90
  /**
91
- * Should the invocation run with auto-restart
91
+ * Should the invocation run with auto-restart on source changes
92
92
  */
93
- canRestart?: boolean;
93
+ restartForDev?: boolean;
94
94
  /**
95
95
  * The module to run the command from
96
96
  */
package/src/util.ts CHANGED
@@ -1,15 +1,24 @@
1
- import { spawn } from 'node:child_process';
1
+ import { spawn, type ChildProcess } from 'node:child_process';
2
2
 
3
- import { describeFunction, Env, ExecUtil, Runtime } from '@travetto/runtime';
3
+ import { describeFunction, Env, ExecUtil, Runtime, listenForSourceChanges, type ExecutionResult, ShutdownManager } from '@travetto/runtime';
4
4
 
5
5
  import { CliCommandShape, CliCommandShapeFields } from './types.ts';
6
6
 
7
+ const CODE_RESTART = { type: 'code_change', code: 200 };
7
8
  const IPC_ALLOWED_ENV = new Set(['NODE_OPTIONS']);
8
9
  const IPC_INVALID_ENV = new Set(['PS1', 'INIT_CWD', 'COLOR', 'LANGUAGE', 'PROFILEHOME', '_']);
9
10
  const validEnv = (key: string): boolean => IPC_ALLOWED_ENV.has(key) || (
10
11
  !IPC_INVALID_ENV.has(key) && !/^(npm_|GTK|GDK|TRV|NODE|GIT|TERM_)/.test(key) && !/VSCODE/.test(key)
11
12
  );
12
13
 
14
+ const isCodeRestart = (input: unknown): input is typeof CODE_RESTART =>
15
+ typeof input === 'object' && !!input && 'type' in input && input.type === CODE_RESTART.type;
16
+
17
+ type RunWithRestartOptions = {
18
+ maxRetriesPerMinute?: number;
19
+ relayInterrupt?: boolean;
20
+ };
21
+
13
22
  export class CliUtil {
14
23
  /**
15
24
  * Get a simplified version of a module name
@@ -28,21 +37,68 @@ export class CliUtil {
28
37
  /**
29
38
  * Run a command as restartable, linking into self
30
39
  */
31
- static runWithRestart<T extends CliCommandShapeFields & CliCommandShape>(cmd: T, ipc?: boolean): Promise<unknown> | undefined {
32
- if (ipc && process.connected) {
33
- process.once('disconnect', () => process.exit());
40
+ static async runWithRestartOnCodeChanges<T extends CliCommandShapeFields & CliCommandShape>(cmd: T, config?: RunWithRestartOptions): Promise<boolean> {
41
+
42
+ if (Env.TRV_CAN_RESTART.isFalse || cmd.restartForDev !== true) {
43
+ process.on('message', event => isCodeRestart(event) && process.exit(event.code));
44
+ return false;
34
45
  }
35
- if (Env.TRV_CAN_RESTART.isFalse || !(cmd.canRestart ?? !Runtime.production)) {
36
- Env.TRV_CAN_RESTART.clear();
37
- return;
46
+
47
+ let result: ExecutionResult | undefined;
48
+ let exhaustedRestarts = false;
49
+ let subProcess: ChildProcess | undefined;
50
+
51
+ const env = { ...process.env, ...Env.TRV_CAN_RESTART.export(false) };
52
+ const maxRetries = config?.maxRetriesPerMinute ?? 5;
53
+ const relayInterrupt = config?.relayInterrupt ?? true;
54
+ const restarts: number[] = [];
55
+ listenForSourceChanges(() => { subProcess?.send(CODE_RESTART); });
56
+
57
+ if (!relayInterrupt) {
58
+ process.removeAllListeners('SIGINT'); // Remove any existing listeners
59
+ process.on('SIGINT', () => { }); // Prevents SIGINT from killing parent process, the child will handle
38
60
  }
39
- return ExecUtil.withRestart(() => spawn(process.argv0, process.argv.slice(1), {
40
- env: {
41
- ...process.env,
42
- ...Env.TRV_CAN_RESTART.export(false)
43
- },
44
- stdio: [0, 1, 2, ipc ? 'ipc' : undefined]
45
- }));
61
+
62
+ while (
63
+ (result === undefined || result.code === CODE_RESTART.code) &&
64
+ !exhaustedRestarts
65
+ ) {
66
+ if (restarts.length) {
67
+ console.error('Restarting...', { pid: process.pid, time: restarts[0] });
68
+ }
69
+
70
+ // Ensure restarts length is capped
71
+ subProcess = spawn(process.argv0, process.argv.slice(1), { env, stdio: [0, 1, 2, 'ipc'] })
72
+ .on('message', value => process.send?.(value));
73
+
74
+ const interrupt = (): void => { subProcess?.kill('SIGINT'); };
75
+ const toMessage = (value: unknown): void => { subProcess?.send(value!); };
76
+
77
+ // Proxy kill requests
78
+ process.on('message', toMessage);
79
+ if (relayInterrupt) {
80
+ process.on('SIGINT', interrupt);
81
+ }
82
+
83
+ result = await ExecUtil.getResult(subProcess, { catch: true });
84
+ process.exitCode = subProcess.exitCode;
85
+ process.off('message', toMessage);
86
+ process.off('SIGINT', interrupt);
87
+
88
+ if (restarts.length >= maxRetries) {
89
+ exhaustedRestarts = (Date.now() - restarts[0]) < (10 * 1000);
90
+ restarts.shift();
91
+ }
92
+ restarts.push(Date.now());
93
+ }
94
+
95
+
96
+ if (exhaustedRestarts) {
97
+ console.error(`Bailing, due to ${maxRetries} restarts in under 10s`);
98
+ }
99
+
100
+ await ShutdownManager.gracefulShutdown('cli-restart');
101
+ process.exit();
46
102
  }
47
103
 
48
104
  /**
@@ -82,7 +138,7 @@ export class CliUtil {
82
138
  * Debug if IPC available
83
139
  */
84
140
  static async debugIfIpc<T extends CliCommandShapeFields & CliCommandShape>(cmd: T): Promise<boolean> {
85
- return (cmd.debugIpc ?? !Runtime.production) && this.triggerIpc('run', cmd);
141
+ return cmd.debugIpc === true && this.triggerIpc('run', cmd);
86
142
  }
87
143
 
88
144
  /**
@@ -91,4 +147,11 @@ export class CliUtil {
91
147
  static async writeAndEnsureComplete(data: unknown, channel: 'stdout' | 'stderr' = 'stdout'): Promise<void> {
92
148
  return await new Promise(resolve => process[channel].write(typeof data === 'string' ? data : JSON.stringify(data, null, 2), () => resolve()));
93
149
  }
150
+
151
+ /**
152
+ * Read extended options from cli inputs, in the form of -o key:value or -o key
153
+ */
154
+ static readExtendedOptions(options?: string[]): Record<string, string | boolean> {
155
+ return Object.fromEntries((options ?? [])?.map(option => [...option.split(':'), true]));
156
+ }
94
157
  }