azure-sdk-for-js/common/tools/dev-tool
Maor Leger 86ed16f451
[identity] Migrate to ESM and tshy (#31647)
### Packages impacted by this PR

@azure/identity-broker
@azure/identity 

### Issues associated with this PR

#31338 

### Describe the problem that is addressed by this PR

Migrates identity and identity-broker to ESM, tshy, and Vitest
2024-11-13 15:26:24 -08:00
..
shared-config [EngSys] standardize OSS copyright header 2024-08-27 13:01:38 -07:00
src [identity] Migrate to ESM and tshy (#31647) 2024-11-13 15:26:24 -08:00
test [EngSys] move to vendored version of cross-env via dev-tool 2024-11-02 00:48:06 +00:00
README.md [dev-tool] Update readme (#31295) 2024-10-02 13:54:13 -07:00
dtx.js [EngSys] standardize OSS copyright header 2024-08-27 13:01:38 -07:00
eslint.config.mjs [eslint-plugin] upgrade typescript-eslint to v8 2024-08-23 10:19:23 -07:00
launch.js [EngSys] standardize OSS copyright header 2024-08-27 13:01:38 -07:00
package.json [EngSys] move `uglify-js` to dev-tool vendored 2024-10-30 18:01:20 +00:00
register.d.ts [EngSys] standardize OSS copyright header 2024-08-27 13:01:38 -07:00
register.js [dev-tool] Use tsx (#31284) 2024-10-02 12:23:56 -07:00
tsconfig.json [EngSys] remove `downlevelIteration` typescript option (#30398) 2024-07-12 12:34:56 -07:00
vitest.config.mts [EngSys] standardize OSS copyright header 2024-08-27 13:01:38 -07:00

README.md

@azure/dev-tool

dev-tool is an extensible command-line utility for Azure SDK for JS contributors.

It provides a place to centralize scripts, resources, and processes for development of the Azure SDK for JavaScript. It is its own unpublished package and has the ability to use dependencies that are managed with Rush in the development process, and it is written in TypeScript.

Installation

dev-tool runs using tsx, so it does not need to be built. It is ready-to-go after a rush update. It additionally does not need to be installed to a user's machine in order to be used in package.json scripts, since it provides the dev-tool binary to any dependent packages through the bin entry in its package.json. Simply add @azure/dev-tool to the devDependencies of a package, and the dev-tool binary will become available. If you wish to use dev-tool from the CLI manually, you can install it globally on your system by running npm install -g from this directory.

Usage

dev-tool uses a command hierarchy. For example, at the time of writing, the command tree looks like this:

dev-tool

  • about (display command help and information)

  • package

    • resolve (display information about the project that owns a directory)
  • samples

    • dev (link samples to local sources for access to IntelliSense during development)
    • prep (prepare samples for local source-linked execution)
    • publish (make a "camera-ready" copy of a package's samples)
    • run (execute a sample or all samples within a directory)
    • check-node-versions (execute samples with different node versions, typically in preparation for release)
  • test-proxy

    • init (initializes assets.json in your package folder)
    • push (pushes the assets, referenced by assets.json, into git)
    • reset (reset the assets, referenced by assets.json, from git to their original files referenced by the tag. Will prompt if there's pending changes)
    • restore (restore the assets, referenced by assets.json, from git)
    • wait-for-proxy-endpoint (waits until the proxy endpoint is ready or aborts in 120 seconds, whichever happens first)
  • run

    • test:node-ts-input (runs the node tests with TS input files with the default mocha configs, and concurrently runs the proxy tool in record/playback modes if it is not already active)

      • Mocha settings added by default

        -r esm -r ts-node/register --reporter ../../../common/tools/mocha-multi-reporter.js --full-trace

      • Example usage

        dev-tool run test:node-ts-input -- --timeout 1200000 'test/*.spec.ts'
        
    • test:node-js-input (runs the node tests with JS input files with the default mocha configs, and concurrently runs the proxy tool in record/playback modes if it is not already active)

      • Mocha settings added by default

        -r esm --require source-map-support/register --reporter ../../../common/tools/mocha-multi-reporter.js --full-trace

      • Also, calls mocha with nyc for code coverage

      • Example usage

        dev-tool run test:node-js-input -- --timeout 5000000 "dist-esm/test/{,!(browser)/**/}/*.spec.js"
        
    • test:browser (runs the browser tests using karma, and concurrently runs the proxy tool in record/playback modes if it is not already active)

      • Example usage
        dev-tool run test:browser
        

The dev-tool about command will print some information about how to use the command. All commands additionally accept the --help argument, which will print information about the usage of that specific command. For example, to show help information for the resolve command above, issue the command dev-tool package resolve --help.

Extending the Tool

The source hierarchy matches the command hierarchy. Every sub-command has its own folder and index.ts file in src/commands, where src/commands/index.ts defines the behavior of the root dev-tool command, and each subfolder's index.ts file describes a nested sub-command. Every leaf node in the command tree ("leaf command") has its own TypeScript file. For example, src/commands/about.ts defines the behavior of the dev-tool about command, and src/commands/package/resolve.ts defines the behavior of the dev-tool package resolve command.

Command Interface

Every command file's exports must implement the CommandModule interface defined in src/util/commandModule.ts. The interface requires that every command export a constant commandInfo that implements the CommandInfo interface defined in the same file. A helper command makeCommandInfo is provided to assist with the creation of this interface while providing strong type-checking of command-line options. The CommandInfo interface specifies the name, description, and options (command-line arguments) of the command. The command module must also export an async handler function as its default export. Two helper functions, leafCommand and subCommand are provided to assist with development and to provide strong type-checking when extending dev-tool.

Creating a new leaf command

To create a new leaf command in one of the existing sub-command, create a new TypeScript file for that command. Make sure that your module exports the required commandInfo and default handler function. When creating the commandInfo object, use the makeCommandInfo helper function. When creating a command, use the leafCommand helper to get a strongly-typed options parameter for your handler.

As an example, we can create a new hello-world command under the dev-tool package sub-command. The command will print out a string using the many different logging functions. It will accept an argument --echo <string here> that specifies the string to be printed.

src/commands/package/hello-world.ts

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License

import { createPrinter } from "../../util/printer";
import { leafCommand, makeCommandInfo } from "../../framework/command";

const log = createPrinter("hello-world");

export const commandInfo = makeCommandInfo("hello-world", "print a lovely message", {
  echo: {
    kind: "string",
    description: "override the message to be printed",
    default: "Hello world!",
  },
});

export default leafCommand(commandInfo, async (options) => {
  // Demonstrate the colorized command output.
  log("Normal:", options.echo);
  log.success("Success:", options.echo);
  log.info("Info:", options.echo);
  log.warn("Warn:", options.echo);
  log.error("Error:", options.echo);
  log.debug("Debug:", options.echo);

  return true;
});

(Note: using the makeCommandInfo function is required to have strong type-checking on the options parameter of the handler. The options field of commandInfo must have a very strong type, and makeCommandInfo takes care of ensuring that the type is as strongly specified as possible.)

As a last step, add a mapping for the "hello-world" command to the sub-command map in src/commands/package/index.ts. This will allow the command to resolve:

src/commands/package/index.ts

// ...

export default subCommand(commandInfo, {
  "hello-world": () => import("./hello-world"),
  // ... rest of the sub-commands still here
});

At this point, the command is ready. When using leafCommand or subCommand, parsing and handling of arguments, including the --help output will be handled automatically by the command infrastructure. Debug output will only be shown if the DEBUG environment variable is set. Try it out:

  • Use dev-tool package hello-world to see the default output of the command
  • Use DEBUG=true dev-tool package hello-world to see the full debugging output
  • Use dev-tool package hello-world --help to view the generated help pages and make sure they are correct
  • Use dev-tool package hello-world --echo <another string> to change the default "Hello world!" text to something else.
  • Use dev-tool package --help to see the hello-world command in the help message of its parent command

Creating a new command with sub-commands

To create a new branching sub-command, create a new folder in the source tree and add an index.ts file. The folder should be named the same as the new command. The subCommand helper function can assist with creating a branching command.

As an example, we can convert the hello-world example above into a branching command hello with a single sub-command world. Instead of adding it to the package sub-command, we will add it to the root dev-tool command.

Instead of creating a single file hello-world.ts, we will instead create a folder src/commands/hello and two ts files: src/commands/hello/index.ts and src/commands/hello/world.ts. In src/commands/hello/index.ts, we can define our new sub-command:

src/commands/hello/index.ts

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License

import { subCommand, makeCommandInfo } from "../../framework/command";

export const commandInfo = makeCommandInfo("hello", "commands for printing some lovely messages");

export default subCommand(commandInfo, {
  world: () => import("./world"),
});

(Note: Since we don't have any arguments or options to add to the sub-command, the options argument to makeCommandInfo is omitted (since the sub-command just delegates to its child commands, we wouldn't be able to use any options in this parent command anyway).)

This simple file establishes the mapping from the command name "world" to our new command module src/commands/hello/world.ts. The contents of world.ts are very similar to the previous hello-world.ts module, but we will change the name field of commandInfo and the argument to createPrinter:

src/commands/hello/world.ts

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License

import { createPrinter } from "../../util/printer";
import { leafCommand, makeCommandInfo } from "../../framework/command";

const log = createPrinter("world");

export const commandInfo = makeCommandInfo("world", "print a lovely message", {
  echo: {
    kind: "string",
    description: "override the message to be printed",
    default: "Hello world!",
  },
});

export default leafCommand(commandInfo, async (options) => {
  // Demonstrate the colorized command output.
  log("Normal:", options.echo);
  log.success("Success:", options.echo);
  log.info("Info:", options.echo);
  log.warn("Warn:", options.echo);
  log.error("Error:", options.echo);
  log.debug("Debug:", options.echo);

  return true;
});

The final step is to add a mapping to our new subcommand to thebaseCommands map root src/commands/index.ts file:

src/commands/index.ts

// ...

/**
 * All of dev-tool's base commands and the modules that define them
 */
export const baseCommands = {
  hello: () => import("./hello"),
  // ... all other sub-commands still here
} as const;

// ...

(Note: If we were adding our hello command to another sub-command rather than the root, we would just add it to that sub-command's index.ts instead of the root src/commands/index.ts, similar to how we added hello-world to src/commands/package/index.ts in the previous example.)

Understanding the Options Type

When using leafCommand, the handler function takes a value options with a type that is generated from the options property of the CommandInfo object given as the first argument to leafCommand. The underlying parsing behavior is implemented by minimist and is validated in the parseOptions function in src/util/commandBuilder.ts.

The structure of the CommandInfo.options field is a map from option names to a tagged union that supports three variants (using the "kind" property as the disciminant):

  • "string" for command-line flags that have a string value (for example, --directory path/to/directory)
  • "boolean" for command-line flags that have a boolean value (for example, --quiet with no argument)
  • "multistring" for command-line flags that have string values and may be specified more than once (for example, --add-dir path/to/dir1 --add-dir path/to/dir2)

Each variant supports an optional shortName field that specifies a one-letter command alias (e.g. a value of shortName: "d" would make -d an alias of the --directory option above). Each also has an optional default parameter to specify the default value should the argument not be specified on the command-line. If no default value is provided, the type of the options value passed to the handler will be expanded to include undefined as a possible value. Finally, each option has a description field that includes the help text shown in the messages produced by --help.

Final Developer Notes

  • Using the subCommand and leafCommand helpers is not required. If a command module exports any function with the signature (...args: string[]) => Promise<boolean> as its default export, it will run when the command is invoked and will be given the arguments passed in the parameters. However, only subCommand and leafCommand provide automatic argument parsing and handling of --help. The functions used to provide this behavior are located in the src/util/commandBuilder.ts module.
  • Some additional helper modules can be found in src/util such as resolveProject.ts which walks up the directory hierarchy and finds the absolute path of the nearest SDK package directory (useful for commands like samples which always operate relative to the package directory)
  • The tool runs using the transpileOnly option in the ts-node configuration, meaning it does not perform run-time type-checking. The build step of the package will run type-checking using tsc, so to check the tool's code for type errors, simply use rushx build.

Contributing

This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com.

When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

If you'd like to contribute to this library, please read the contributing guide to learn more about how to build and test the code.