1 | #!/usr/bin/env node
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | import Module from 'module';
|
7 | import http from 'http';
|
8 | import path from 'path';
|
9 | import { existsSync } from 'fs';
|
10 |
|
11 | import arg from 'arg';
|
12 |
|
13 | import { serve } from '../lib';
|
14 | import { handle } from '../lib/handler';
|
15 | import { version } from '../../package.json';
|
16 | import { logError } from '../lib/error';
|
17 | import { parseEndpoint } from '../lib/parse-endpoint';
|
18 | import type { AddressInfo } from 'net';
|
19 | import type { RequestHandler } from '../lib';
|
20 |
|
21 |
|
22 | const args = arg({
|
23 | '--listen': parseEndpoint,
|
24 | '-l': '--listen',
|
25 | '--help': Boolean,
|
26 | '--version': Boolean,
|
27 | '-v': '--version',
|
28 | });
|
29 |
|
30 |
|
31 |
|
32 | if (args['--help']) {
|
33 | console.error(`
|
34 | micro - Asynchronous HTTP microservices
|
35 |
|
36 | USAGE
|
37 |
|
38 | $ micro --help
|
39 | $ micro --version
|
40 | $ micro [-l listen_uri [-l ...]] [entry_point.js]
|
41 |
|
42 | By default micro will listen on 0.0.0.0:3000 and will look first
|
43 | for the "main" property in package.json and subsequently for index.js
|
44 | as the default entry_point.
|
45 |
|
46 | Specifying a single --listen argument will overwrite the default, not supplement it.
|
47 |
|
48 | OPTIONS
|
49 |
|
50 | --help shows this help message
|
51 |
|
52 | -v, --version displays the current version of micro
|
53 |
|
54 | -l, --listen listen_uri specify a URI endpoint on which to listen (see below) -
|
55 | more than one may be specified to listen in multiple places
|
56 |
|
57 | ENDPOINTS
|
58 |
|
59 | Listen endpoints (specified by the --listen or -l options above) instruct micro
|
60 | to listen on one or more interfaces/ports, UNIX domain sockets, or Windows named pipes.
|
61 |
|
62 | For TCP (traditional host/port) endpoints:
|
63 |
|
64 | $ micro -l tcp://hostname:1234
|
65 |
|
66 | For UNIX domain socket endpoints:
|
67 |
|
68 | $ micro -l unix:/path/to/socket.sock
|
69 |
|
70 | For Windows named pipe endpoints:
|
71 |
|
72 | $ micro -l pipe:\\\\.\\pipe\\PipeName
|
73 | `);
|
74 | process.exit(2);
|
75 | }
|
76 |
|
77 |
|
78 |
|
79 | if (args['--version']) {
|
80 | console.log(version);
|
81 | process.exit();
|
82 | }
|
83 |
|
84 | if (!args['--listen']) {
|
85 |
|
86 | args['--listen'] = [String(3000)];
|
87 | }
|
88 |
|
89 | let file = args._[0];
|
90 |
|
91 | if (!file) {
|
92 | try {
|
93 | const req = Module.createRequire(module.filename);
|
94 | const packageJson: unknown = req(
|
95 | path.resolve(process.cwd(), 'package.json'),
|
96 | );
|
97 | if (hasMain(packageJson)) {
|
98 | file = packageJson.main;
|
99 | } else {
|
100 | file = 'index.js';
|
101 | }
|
102 | } catch (err) {
|
103 | if (isNodeError(err) && err.code !== 'MODULE_NOT_FOUND') {
|
104 | logError(
|
105 | `Could not read \`package.json\`: ${err.message}`,
|
106 | 'invalid-package-json',
|
107 | );
|
108 | process.exit(1);
|
109 | }
|
110 | }
|
111 | }
|
112 |
|
113 | if (!file) {
|
114 | logError('Please supply a file!', 'path-missing');
|
115 | process.exit(1);
|
116 | }
|
117 |
|
118 | if (!file.startsWith('/')) {
|
119 | file = path.resolve(process.cwd(), file);
|
120 | }
|
121 |
|
122 | if (!existsSync(file)) {
|
123 | logError(
|
124 | `The file or directory "${path.basename(file)}" doesn't exist!`,
|
125 | 'path-not-existent',
|
126 | );
|
127 | process.exit(1);
|
128 | }
|
129 |
|
130 | function registerShutdown(fn: () => void) {
|
131 | let run = false;
|
132 |
|
133 | const wrapper = () => {
|
134 | if (!run) {
|
135 | run = true;
|
136 | fn();
|
137 | }
|
138 | };
|
139 |
|
140 | process.on('SIGINT', wrapper);
|
141 | process.on('SIGTERM', wrapper);
|
142 | process.on('exit', wrapper);
|
143 | }
|
144 |
|
145 | function startEndpoint(module: RequestHandler, endpoint: string) {
|
146 | const server = new http.Server(serve(module));
|
147 |
|
148 | server.on('error', (err) => {
|
149 | console.error('micro:', err.stack);
|
150 | process.exit(1);
|
151 | });
|
152 |
|
153 | server.listen(endpoint, () => {
|
154 | const details = server.address();
|
155 | registerShutdown(() => {
|
156 | console.log('micro: Gracefully shutting down. Please wait...');
|
157 | server.close();
|
158 | process.exit();
|
159 | });
|
160 |
|
161 |
|
162 |
|
163 | if (typeof details === 'string') {
|
164 | console.log(`micro: Accepting connections on ${details}`);
|
165 | } else if (isAddressInfo(details)) {
|
166 | console.log(`micro: Accepting connections on port ${details.port}`);
|
167 | } else {
|
168 | console.log('micro: Accepting connections');
|
169 | }
|
170 | });
|
171 | }
|
172 |
|
173 | async function start() {
|
174 | if (file && args['--listen']) {
|
175 | const loadedModule = await handle(file);
|
176 |
|
177 | for (const endpoint of args['--listen']) {
|
178 | startEndpoint(loadedModule as RequestHandler, endpoint);
|
179 | }
|
180 | }
|
181 | }
|
182 |
|
183 | start()
|
184 | .then()
|
185 | .catch((error) => {
|
186 | if (error instanceof Error) {
|
187 | logError(error.message, 'STARTUP_FAILURE');
|
188 | }
|
189 | process.exit(1);
|
190 | });
|
191 |
|
192 | function hasMain(packageJson: unknown): packageJson is { main: string } {
|
193 | return (
|
194 | typeof packageJson === 'object' &&
|
195 | packageJson !== null &&
|
196 | 'main' in packageJson
|
197 | );
|
198 | }
|
199 |
|
200 | function isNodeError(
|
201 | error: unknown,
|
202 | ): error is { code: string; message: string } {
|
203 | return error instanceof Error && 'code' in error;
|
204 | }
|
205 |
|
206 | function isAddressInfo(obj: unknown): obj is AddressInfo {
|
207 | return 'port' in (obj as AddressInfo);
|
208 | }
|