Skip to content

Commit

Permalink
Implement node wrapper (#298)
Browse files Browse the repository at this point in the history
See changesets and README for full details.

---

`skuba node` runs a TypeScript file; it's like a one-off `skuba start`.
Unlike start, it gives you a REPL when you omit the command line entry
point rather than falling back to the entry point in `package.json`.

This PR also adds some secret sauce to automatically register the `src`
module alias when you're running under `skuba node` or `skuba start`.
This means that you only need a runtime module alias resolver like
`import skuba-dive/register` at the top of production entry points;
local scripts can be run with little fanfare using the skuba commands.

---

Notes:

- I considered calling the command `skuba exec`, but that sounds a bit
  awkward when it supports opening a REPL.

- You may be wondering why don't we just use `tsconfig-paths`.

  This is tempting but I'm not convinced that we should depend on
  reading configuration out of `tsconfig.json` at runtime. This can be
  confusing given TypeScript is generally considered a compile-time
  concern, and our own templates only keep `lib` and `node_modules` in
  the runtime Docker image.

- Diving into this has uncovered that the `@babel/node` REPL is pretty
  unusable with modern JavaScript/TypeScript. See the README for more.
  • Loading branch information
72636c authored Dec 14, 2020
1 parent 65f3e14 commit 641accc
Show file tree
Hide file tree
Showing 14 changed files with 290 additions and 19 deletions.
7 changes: 7 additions & 0 deletions .changeset/late-carrots-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'skuba': patch
---

**start:** Improve support for non-HTTP server entry points

You can now run arbitrary TypeScript files without them exiting on a `You must export callback or requestListener` error.
33 changes: 33 additions & 0 deletions .changeset/light-files-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
'skuba': minor
---

**node:** Add command

`skuba node` lets you run a TypeScript source file, or open a REPL if none is provided:

- `skuba node src/some-cli-script.ts`
- `skuba node`

This automatically registers a `src` module alias for ease of local development. For example, you can run a prospective `src/someLocalCliScript.ts` without having to register a module alias resolver:

```typescript
// This `src` module alias just works under `skuba node` and `skuba start`
import { rootLogger } from 'src/framework/logging';
```

```bash
yarn skuba node src/someLocalCliScript
```

If you use this alias in your production code,
your production entry point(s) will need to import a runtime module alias resolver like [`skuba-dive/register`](https://github.com/seek-oss/skuba-dive#register).
For example, your `src/app.ts` may look like:

```typescript
// This must be imported directly within the `src` directory
import 'skuba-dive/register';

// You can use the `src` module alias after registration
import { rootLogger } 'src/framework/logging';
```
5 changes: 5 additions & 0 deletions .changeset/strange-candles-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'skuba': patch
---

**start:** Support `src` module alias
5 changes: 5 additions & 0 deletions .changeset/thick-moons-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'skuba': patch
---

**start:** Support source maps
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,48 @@ Check for code quality issues.

This script should be run in CI to verify that [`skuba format`] was applied and triaged locally.

### `skuba node`

Run a TypeScript source file, or open a REPL if none is provided:

- `skuba node src/some-cli-script.ts`
- `skuba node`

This automatically registers a `src` module alias for ease of local development.
If you use this alias in your production code,
your production entry point(s) will need to import a runtime module alias resolver like [`skuba-dive/register`].
For example, your `src/app.ts` may look like:

```typescript
// This must be imported directly within the `src` directory
import 'skuba-dive/register';

// You can use the `src` module alias after registration
import { rootLogger } 'src/framework/logging';
```
> **Note:** if you're using the [experimental Babel toolchain],
> you'll be limited to the fairly primitive `babel-node` REPL.
> While it can import TypeScript modules,
> it does not support interactive TypeScript nor modern JavaScript syntax:
>
> ```typescript
> import { someExport } from 'src/someModule';
> // Thrown: [...] Modules aren't supported in the REPL
>
> const { someExport } = require('src/someModule');
> // Thrown: [...] Only `var` variables are supported in the REPL
>
> var { someExport } = require('src/someModule');
> // undefined
>
> var v: undefined;
> // Thrown: [...] Unexpected token
> ```
[`skuba-dive/register`]: https://github.com/seek-oss/skuba-dive#register
[experimental babel toolchain]: ./docs/babel.md
### `skuba start`
Start a live-reloading server for local development.
Expand All @@ -228,6 +270,19 @@ For example, in Visual Studio Code:
1. Run `skuba start --inspect-brk`
1. Run the built-in `Node.js: Attach` launch configuration
This automatically registers a `src` module alias for ease of local development.
If you use this alias in your production code,
your production entry point(s) will need to import a runtime module alias resolver like [`skuba-dive/register`].
For example, your `src/app.ts` may look like:
```typescript
// This must be imported directly within the `src` directory
import 'skuba-dive/register';

// You can use the `src` module alias after registration
import { rootLogger } 'src/framework/logging';
```
[node.js options]: https://nodejs.org/en/docs/guides/debugging-getting-started/#command-line-options
#### Start an executable script
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@types/ejs": "3.0.5",
"@types/fs-extra": "9.0.5",
"@types/lodash.mergewith": "4.6.6",
"@types/module-alias": "2.0.0",
"@types/npm-which": "3.0.0",
"type-fest": "0.20.2"
},
Expand All @@ -65,6 +66,7 @@
"jest": "^26.6.0",
"latest-version": "^5.1.0",
"lodash.mergewith": "^4.6.2",
"module-alias": "^2.2.2",
"nodemon": "^2.0.6",
"normalize-package-data": "^3.0.0",
"npm-run-path": "^4.0.1",
Expand All @@ -73,6 +75,7 @@
"read-pkg-up": "^7.0.1",
"runtypes": "^5.0.1",
"semantic-release": "^17.2.3",
"source-map-support": "^0.5.19",
"ts-jest": "^26.4.1",
"ts-node": "^9.0.0",
"ts-node-dev": "1.1.1",
Expand Down
66 changes: 66 additions & 0 deletions src/cli/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import path from 'path';

import getPort from 'get-port';
import parse from 'yargs-parser';

import { unsafeMapYargs } from '../utils/args';
import { createExec } from '../utils/exec';
import { isBabelFromManifest } from '../utils/manifest';

const parseArgs = () => {
const {
_: [entryPointArg],
...yargs
} = parse(process.argv.slice(2));

const entryPoint = typeof entryPointArg === 'string' ? entryPointArg : null;

const inspect = unsafeMapYargs({
inspect: yargs.inspect as unknown,
'inspect-brk': yargs['inspect-brk'] as unknown,
});

return {
entryPoint,
inspect,
};
};

export const node = async () => {
const args = parseArgs();

const [port, isBabel] = await Promise.all([getPort(), isBabelFromManifest()]);

const exec = createExec({
env: isBabel ? undefined : { __SKUBA_REGISTER_MODULE_ALIASES: '1' },
});

if (isBabel) {
return exec(
'babel-node',
...args.inspect,
'--extensions',
['.js', '.json', '.ts'].join(','),
'--require',
path.join('skuba', 'lib', 'register'),
...(args.entryPoint === null
? []
: [
path.join(__dirname, '..', 'wrapper.js'),
args.entryPoint,
String(port),
]),
);
}

return exec(
'ts-node',
...args.inspect,
'--require',
path.join('skuba', 'lib', 'register'),
'--transpile-only',
...(args.entryPoint === null
? []
: [path.join(__dirname, '..', 'wrapper'), args.entryPoint, String(port)]),
);
};
22 changes: 15 additions & 7 deletions src/cli/start/index.ts → src/cli/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import path from 'path';
import getPort from 'get-port';
import parse from 'yargs-parser';

import { unsafeMapYargs } from '../../utils/args';
import { exec } from '../../utils/exec';
import { unsafeMapYargs } from '../utils/args';
import { createExec } from '../utils/exec';
import {
getEntryPointFromManifest,
isBabelFromManifest,
} from '../../utils/manifest';
} from '../utils/manifest';

const parseArgs = async () => {
const {
Expand Down Expand Up @@ -39,8 +39,12 @@ export const start = async () => {
isBabelFromManifest(),
]);

const execProcess = createExec({
env: isBabel ? undefined : { __SKUBA_REGISTER_MODULE_ALIASES: '1' },
});

if (isBabel) {
return exec(
return execProcess(
'nodemon',
'--ext',
['.js', '.json', '.ts'].join(','),
Expand All @@ -50,18 +54,22 @@ export const start = async () => {
'babel-node',
'--extensions',
['.js', '.json', '.ts'].join(','),
path.join(__dirname, 'http.js'),
'--require',
path.join('skuba', 'lib', 'register'),
path.join(__dirname, '..', 'wrapper.js'),
args.entryPoint,
String(port),
);
}

return exec(
return execProcess(
'ts-node-dev',
...args.inspect,
'--require',
path.join('skuba', 'lib', 'register'),
'--respawn',
'--transpile-only',
path.join(__dirname, 'http'),
path.join(__dirname, '..', 'wrapper'),
args.entryPoint,
String(port),
);
Expand Down
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
/**
* Entry point for the Node.js development API.
*
* This is where skuba imports point to:
*
* ```typescript
* import { Net } from 'skuba';
*
* const { Net } = require('skuba');
* ```
*/

export * as Jest from './api/jest';
export * as Net from './api/net';
53 changes: 53 additions & 0 deletions src/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Local hook for module alias and source map support.
*
* This is only intended for use with `skuba node` and `skuba start`,
* where it is loaded before the entry point provided by the user:
*
* ```bash
* ts-node --require skuba/lib/register src/userProvidedEntryPoint
* ```
*
* These commands make use of development tooling like `ts-node`,
* which may not exactly match the runtime characteristics of `node`.
* Production code should be compiled down to JavaScript and run with Node.js.
*
* For equivalent module alias and source map support in production,
* import the `skuba-dive/register` hook.
*
* {@link https://github.com/seek-oss/skuba-dive#register}
*/

import 'source-map-support/register';

import path from 'path';

import { addAlias } from 'module-alias';
import readPkgUp from 'read-pkg-up';

import { log } from './utils/logging';

const registerModuleAliases = () => {
if (!process.env.__SKUBA_REGISTER_MODULE_ALIASES) {
return;
}

// This may pick the wrong `package.json` if we are in a nested directory.
// Consider revisiting this when we decide how to better support monorepos.
const result = readPkgUp.sync();

if (typeof result === 'undefined') {
log.warn(log.bold('src'), '→', 'not found');

return;
}

const root = path.dirname(result.path);
const src = path.join(root, 'src');

log.subtle(log.bold('src'), '→', src);

addAlias('src', src);
};

registerModuleAliases();
10 changes: 10 additions & 0 deletions src/skuba.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
#!/usr/bin/env node

/**
* Entry point for the CLI.
*
* This is where you end up when you run:
*
* ```bash
* [yarn] skuba help
* ```
*/

import path from 'path';

import { parseArgs } from './utils/args';
Expand Down
1 change: 1 addition & 0 deletions src/utils/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const COMMAND_LIST = [
'help',
'init',
'lint',
'node',
'release',
'start',
'test',
Expand Down
Loading

0 comments on commit 641accc

Please sign in to comment.