Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

Latest commit

 

History

History
362 lines (278 loc) · 9.85 KB

contributing.md

File metadata and controls

362 lines (278 loc) · 9.85 KB

Contributing

System Prerequisites

  • Node@>=LTS
  • Yarn@Stable
  • Azure CLI

Installation

Install the project dependencies:

yarn install

Running the linter

yarn lint

To run the tslint autofixer:

 yarn lint-fix

Running The Code

Install ts-node to make your development cycle easier:

yarn global add ts-node
ts-node src/index.ts # this is the same as running `./bedrock` or 'node bedrock.js'

# You can now do things like
ts-node src/index.ts project init # same as running `./bedrock project init`

Implementing Commands

Refer to this doc for guidelines for implementing commands.

Running Tests

To run a one-time test of all tests:

yarn test

Recommended: To keep tests running in the background and constantly test newly created tests and code:

yarn test-watch

Running the Debugger

Prerequisites:

yarn add ts-node

To debug on Visual Studio Code:

  1. On the top menu select Run > Start Debugging
  2. It will prompt you to create a launch.json file for the go language, proceed to create it.
  3. Add the settings found below to the launch.json file. You can add a relative path to from the root to target a subdirectory. I.e.:

    "cwd": "${workspaceFolder}/tests/bedrock-env/fabrikam-infra-hld/fabrikam-base-env",

  4. Change the args with the command and options that you want to debug. In this case, it will debug deployment get.
  5. For more information on using the debuggin support in VS Code check out the Visual Studio Code documentation.

Sample launch.json:

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Debug index.ts",
            "cwd": "${workspaceFolder}",
            "runtimeArgs": ["-r", "ts-node/register"],​
            "args": ["${workspaceRoot}/src/index.ts", "deployment", "get"]​
        }
    ]
}

Production Builds

We use two tools for creating distributable production builds:

  • webpack - For compiling TypeScript code to a single minified JS bundle.
  • pkg - For packaging the output of webpack to 3 standalone binaries targeting win32, macos, and linux. These binaries contain their own self contained versions of Node and can be distributed as standalone executables which can be run even if he host does not have Node installed.

To run do a production build, just run:

yarn build

Cutting new release

See release doc

IDE Configuration

This project uses TSLint for linting and Prettier for code formatting. For best integration with these tools, VSCode is recommended along with the corresponding extensions:

Note: the Prettier VSCode extension will overwrite the existing formatting hot-key (alt+shift+f)

Pre-commit Prettier Hook

A pre-commit hook will automatically get installed when running yarn on the project. This hook will automatically format all staged files with Prettier before committing them. Although useful, make sure to format your code regularly throughout your dev cycle (alt+shift+f in VSCode) to make sure that no unintended format changes happen.

Testing

We use a TypeScript variant of Jest for testing. To create a test, create a file anywhere in the project with name <insert-filename-of-file-testing>.test.ts; Jest will automatically do a glob search for files matching /.+\.test\.tsx?/ig and run the tests contained in them.

By convention, we will store the test file in the same directory as the file its testing. When/if this becomes too burdensome, we can move them to a tests directory.

Adding/Removing/Modifying Project Dependencies

NEVER modify dependencies or devDependencies manually in package.json!

Adding a dependency

yarn add react # This will add react to both package.json and the yarn.lock lockfile

or

yarn add react@^16.9.0 # you can specify target semver's as well

We also want to keep all @types in devDependencies instead of dependencies.

$ yarn add -D @types/node-emoji

Removing a dependency

yarn remove react # Will remove react from both package.json and yarn.lock

Code Style Guide

Avoid classes and concretions; don't complect the codebase with state

Prefer the usage of vanilla JS maps which implement TypeScript interfaces. State is one of the hardest things to deal with in a concurrent system (which JS is by nature with the event-loop) and concrete classes are one of the easiest first steps to making your system rigid and not async friendly.

For example:

interface IAnimal {
  says: () => string;
  leg: number;
}

// Don't do
class Sheep implements IAnimal {
  public says = () => "bahhh";
  public legs = 4;

  constructor({ says, legs }: IAnimal) {
    this.says = says;
    this.legs = legs;
  }
}

// Do
const Sheep = ({ says, legs }: IAnimal) => {
  return {
    says: () => "bahhh",
    legs: 4,
    says,
    legs,
  };
};

Along with being more composable, this also enables us to easily keep a more immutable codebase as we can now more easily pass copies of IAnimal around via the the ... (spread) operator.

Try to be pure

Write pure function whenever possible. The value of Objects and Arrays in JS are pointers, if a function takes in an object or an array, modifies it, and returns the the modified value, it has actually mutated the array/object that was passed as an argument (note this only applies to objects and arrays, all other types are pass-by-value). This is a style of coding you want to avoid when dealing with JS as multiple functions may take in the same object as an argument throughout your code and the ordering of the functions cannot be assured when dealing with async code.

Instead, what we want to do is create copies of the information in your function and return a modified copy of the original using the ... (spread) operator. This will allow you to be more confident that async code is able to still evaluate the same data that you initially passed it even if the event loop caused your code to run out of order.

interface IHuman {
  name: string;
  age: number;
}

// Don't do
// Objects and arrays are passed as pointers.
const jack = { name: "Jack", age: 20 };
const incrementAge = (human: IHuman): IHuman => {
  human.age = age + 1;
  return human;
};
const agedJack = incrementAge(jack);
// NOTE: both agedJack AND jack are now age 21 as you modified the literal
// object passed to the function

// Do
const jack = { name: "Jack", age: 20 };
const incrementAge = (human: IHuman): IHuman => {
  // We use use the `...` (pronounced "spread") operator to make copies of all
  // the values in `human` and place them in agedHuman. We then overwrite the
  // value of `age` with the new value.
  const agedHuman = { ...human, age: human.age + 1 };
  return agedHuman;
};
const agedJack = incrementAge(jack);
// jack remains 20 and agedJack is 21

Treat files like namespaces

One of the best features of es2016 was the import/export specification which allows for better control of what is importable to files from other files. Use this as a method of encapsulation, instead of relying on classes to hide/expose functionality, treat the file as a class and use export as means to declare something public.

// Don't do
export class MyClass {
  public static foo(): string {
    return "bar";
  }
}

// Do
export const foo = (): string => {
  return "bar";
};

Use arrow functions

The es2015 spec introduced arrow functions to JavaScript, these greatly reduce the previously confusing usage of this in JavaScript as the arrow functions bind this the where the function gets initialized. Along with this, they are just cleaner and easier to debug in general.

// Don't do
function foo(bar: number): string {
  return bar.toString();
}

// Don't do
const foo = function (bar: number): string {
  return bar.toString();
};

// Do
const foo = (bar: number): string => {
  return bar.toString();
};

Don't use callbacks

Although Promise and async/await are now pretty standard across modern browsers and is supported in Node LTS, many libraries (including those in the node standard library) use callbacks. These functions are a one-way ticket to Callback Hell, and should avoided at all costs. If you need to use a node function that has a callback, node includes a util function call promisify which will turn the callback into a returned promise:

import { promisify } from "util";
import { readFile } from "fs";

// normally readFile requires a callback
readFile("/etc/passwd", (err, data) => {
  if (err) throw err;
  console.log(data);
});

// But we can turn that function into something promise based
const promiseBasedReadFile = promisify(readFile);

// Full promise based
promiseBasedReadFile("/etc/passwd")
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.error(err);
    return Promise.reject(err);
  });

// Async/Await based
try {
  const passwd = await promiseBasedReadFile("etc/passwd");
  console.log(passwd);
} catch (err) {
  console.error(err);
  throw err;
}