- Node@>=LTS
- Yarn@Stable
- Azure CLI
Install the project dependencies:
yarn install
yarn lint
To run the tslint autofixer:
yarn lint-fix
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 `./spk` or 'node spk.js'
# You can now do things like
ts-node src/index.ts project init # same as running `./spk project init`
Refer to this doc for guidelines for implementing commands.
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
Prerequisites:
yarn add ts-node
To debug on Visual Studio Code:
- On the top menu select Debug > Start Debugging
- It will prompt you to create a
launch.json
file for the go language, proceed to create it. - Add the settings found below to the
launch.json
file. Change theargs
with the command and options that you want to debug. In this case, it will debugdeployment get
.
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"]
}
]
}
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
, andlinux
. 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
See release doc
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:
- https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode
- https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-tslint-plugin
Note: the Prettier VSCode extension will overwrite the existing formatting
hot-key (alt
+shift
+f
)
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.
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.
NEVER modify
dependencies
ordevDependencies
manually in package.json!
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
yarn remove react # Will remove react from both package.json and yarn.lock
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.
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
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";
};
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();
};
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;
}