[major] TypeScript rewrite, support ES Modules. (#706)
This commit is contained in:
parent
8949c70d68
commit
d2a5187b36
|
@ -0,0 +1,4 @@
|
|||
# .gitattributes
|
||||
# Makes sure all line endings are LF.
|
||||
|
||||
* text=auto eol=lf
|
|
@ -0,0 +1,31 @@
|
|||
name: Report a bug
|
||||
description: ———
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Thanks for reporting this bug!
|
||||
|
||||
Help us replicate and find a fix for the issue by filling in this form.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: |
|
||||
Describe the issue and how to replicate it. If possible, please include
|
||||
a minimal example to reproduce the issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Library version
|
||||
description: |
|
||||
Output of the `serve --version` command
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Node version
|
||||
description: Output of the `node --version` command
|
||||
validations:
|
||||
required: true
|
|
@ -0,0 +1,28 @@
|
|||
name: Suggest an improvement or new feature
|
||||
description: ———
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Thanks for filing this feature request!
|
||||
|
||||
Help us understanding this feature and the need for it better by filling in this form.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Describe the feature in detail
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Why
|
||||
description: Why should we add this feature? What are potential use cases for it?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives
|
||||
description: Describe the alternatives you have considered, or existing workarounds
|
||||
validations:
|
||||
required: true
|
|
@ -0,0 +1,56 @@
|
|||
<!--
|
||||
Hi there! Thanks for contributing! Please fill in this template to help us
|
||||
review and merge the PR as quickly and easily as possible!
|
||||
-->
|
||||
|
||||
## Related Issues
|
||||
|
||||
<!--
|
||||
If this is a bug fix, or adds a feature mentioned in another issue, mention
|
||||
it as follows:
|
||||
|
||||
- Closes #10
|
||||
- Fixes #15
|
||||
-->
|
||||
|
||||
## Description
|
||||
|
||||
<!--
|
||||
Explain what has been added/changed/removed, in
|
||||
[keepachangelog.com](https://keepachangelog.com) style.
|
||||
-->
|
||||
|
||||
### Added
|
||||
|
||||
<!--
|
||||
- Added a new method on the limiter object to reset the count for a certain IP [#10]
|
||||
-->
|
||||
|
||||
### Changed
|
||||
|
||||
<!--
|
||||
- Deprecated `global` option
|
||||
- Fixed test for deprecated options [#15]
|
||||
-->
|
||||
|
||||
### Removed
|
||||
|
||||
<!--
|
||||
- Removed deprecated `headers` option
|
||||
-->
|
||||
|
||||
## Caveats/Problems/Issues
|
||||
|
||||
<!--
|
||||
Any weird code/problems you faced while making this PR. Feel free to ask for
|
||||
help with anything, especially if it's your first time contributing!
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] The issues that this PR fixes/closes have been mentioned above.
|
||||
- [ ] What this PR adds/changes/removes has been explained.
|
||||
- [ ] All tests (`pnpm test`) pass.
|
||||
- [ ] The linter (`pnpm lint`) does not throw an errors.
|
||||
- [ ] All added/modified code has been commented, and
|
||||
methods/classes/constants/types have been annotated with TSDoc comments.
|
|
@ -1,20 +1,47 @@
|
|||
# .github/workflows/ci.yaml
|
||||
# Lints and tests the server every time a commit is pushed to the remote
|
||||
# repository.
|
||||
|
||||
name: CI
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
name: Node.js ${{ matrix.node-version }}
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version:
|
||||
- 16
|
||||
- 14
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup PNPM 7
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
version: latest
|
||||
- name: Setup Node 18
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Check for errors in code/formatting
|
||||
run: pnpm lint
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup PNPM 7
|
||||
uses: pnpm/action-setup@v2.0.1
|
||||
with:
|
||||
version: latest
|
||||
- name: Setup Node 18
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Run all tests
|
||||
run: pnpm test
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
node_modules
|
||||
# .gitignore
|
||||
# A list of files and folders that should not be tracked by Git.
|
||||
|
||||
node_modules/
|
||||
coverage/
|
||||
build/
|
||||
.cache/
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
*.log
|
||||
.nyc_output
|
||||
*.tgz
|
||||
*.bak
|
||||
*.tmp
|
||||
|
|
9
.npmrc
9
.npmrc
|
@ -1 +1,8 @@
|
|||
save-exact = true
|
||||
# .npmrc
|
||||
# Configuration for pnpm.
|
||||
|
||||
# Uses the exact version instead of any within-patch-range version of an
|
||||
# installed package.
|
||||
save-exact=true
|
||||
# Do not error out on missing peer dependencies.
|
||||
strict-peer-dependencies=false
|
||||
|
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022 Vercel, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
87
README.md
87
README.md
|
@ -1,87 +0,0 @@
|
|||

|
||||
|
||||
<a aria-label="Vercel logo" href="https://vercel.com">
|
||||
<img src="https://img.shields.io/badge/MADE%20BY%20Vercel-000000.svg?style=for-the-badge&logo=ZEIT&labelColor=000000&logoWidth=20">
|
||||
</a>
|
||||
|
||||
[](https://circleci.com/gh/vercel/serve)
|
||||
[](https://packagephobia.now.sh/result?p=serve)
|
||||
|
||||
Assuming you would like to serve a static site, single page application or just a static file (no matter if on your device or on the local network), this package is just the right choice for you.
|
||||
|
||||
Once it's time to push your site to production, we recommend using [Vercel](https://vercel.com).
|
||||
|
||||
In general, `serve` also provides a neat interface for listing the directory's contents:
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
The quickest way to get started is to just run `npx serve` in your project's directory.
|
||||
|
||||
If you prefer, you can also install the package globally using [Yarn](https://yarnpkg.com/en/) (you'll need at least [Node.js LTS](https://nodejs.org/en/)):
|
||||
|
||||
```bash
|
||||
yarn global add serve
|
||||
```
|
||||
|
||||
Once that's done, you can run this command inside your project's directory...
|
||||
|
||||
```bash
|
||||
serve
|
||||
```
|
||||
|
||||
...or specify which folder you want to serve:
|
||||
|
||||
```bash
|
||||
serve folder_name
|
||||
```
|
||||
|
||||
Finally, run this command to see a list of all available options:
|
||||
|
||||
```bash
|
||||
serve --help
|
||||
```
|
||||
|
||||
Now you understand how the package works! :tada:
|
||||
|
||||
## Configuration
|
||||
|
||||
To customize `serve`'s behavior, create a `serve.json` file in the public folder and insert any of [these properties](https://github.com/vercel/serve-handler#options).
|
||||
|
||||
## API
|
||||
|
||||
The core of `serve` is [serve-handler](https://github.com/vercel/serve-handler), which can be used as middleware in existing HTTP servers:
|
||||
|
||||
```js
|
||||
const handler = require('serve-handler');
|
||||
const http = require('http');
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
// You pass two more arguments for config and middleware
|
||||
// More details here: https://github.com/vercel/serve-handler#options
|
||||
return handler(request, response);
|
||||
});
|
||||
|
||||
server.listen(3000, () => {
|
||||
console.log('Running at http://localhost:3000');
|
||||
});
|
||||
```
|
||||
|
||||
**NOTE:** You can also replace `http.createServer` with [micro](https://github.com/vercel/micro), if you want.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device
|
||||
2. Uninstall `serve` if it's already installed: `npm uninstall -g serve`
|
||||
3. Link it to the global module directory: `npm link`
|
||||
|
||||
After that, you can use the `serve` command everywhere. [Here](https://github.com/vercel/serve/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+for+beginners%22)'s a list of issues that are great for beginners.
|
||||
|
||||
## Credits
|
||||
|
||||
This project used to be called "list" and "micro-list". But thanks to [TJ Holowaychuk](https://github.com/tj) handing us the new name, it's now called "serve" (which is much more definite).
|
||||
|
||||
## Author
|
||||
|
||||
Leo Lamprecht ([@notquiteleo](https://twitter.com/notquiteleo))
|
462
bin/serve.js
462
bin/serve.js
|
@ -1,462 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Native
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const {promisify} = require('util');
|
||||
const {parse} = require('url');
|
||||
const os = require('os');
|
||||
|
||||
// Packages
|
||||
const Ajv = require('ajv');
|
||||
const checkForUpdate = require('update-check');
|
||||
const chalk = require('chalk');
|
||||
const arg = require('arg');
|
||||
const {write: copy} = require('clipboardy');
|
||||
const handler = require('serve-handler');
|
||||
const schema = require('@zeit/schemas/deployment/config-static');
|
||||
const boxen = require('boxen');
|
||||
const compression = require('compression');
|
||||
|
||||
// Utilities
|
||||
const pkg = require('../package');
|
||||
|
||||
const readFile = promisify(fs.readFile);
|
||||
const compressionHandler = promisify(compression());
|
||||
|
||||
const interfaces = os.networkInterfaces();
|
||||
|
||||
const warning = (message) => chalk`{yellow WARNING:} ${message}`;
|
||||
const info = (message) => chalk`{magenta INFO:} ${message}`;
|
||||
const error = (message) => chalk`{red ERROR:} ${message}`;
|
||||
|
||||
const updateCheck = async (isDebugging) => {
|
||||
let update = null;
|
||||
|
||||
try {
|
||||
update = await checkForUpdate(pkg);
|
||||
} catch (err) {
|
||||
const suffix = isDebugging ? ':' : ' (use `--debug` to see full error)';
|
||||
console.error(warning(`Checking for updates failed${suffix}`));
|
||||
|
||||
if (isDebugging) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${chalk.bgRed('UPDATE AVAILABLE')} The latest version of \`serve\` is ${update.latest}`);
|
||||
};
|
||||
|
||||
const getHelp = () => chalk`
|
||||
{bold.cyan serve} - Static file serving and directory listing
|
||||
|
||||
{bold USAGE}
|
||||
|
||||
{bold $} {cyan serve} --help
|
||||
{bold $} {cyan serve} --version
|
||||
{bold $} {cyan serve} folder_name
|
||||
{bold $} {cyan serve} [-l {underline listen_uri} [-l ...]] [{underline directory}]
|
||||
|
||||
By default, {cyan serve} will listen on {bold 0.0.0.0:3000} and serve the
|
||||
current working directory on that address.
|
||||
|
||||
Specifying a single {bold --listen} argument will overwrite the default, not supplement it.
|
||||
|
||||
{bold OPTIONS}
|
||||
|
||||
--help Shows this help message
|
||||
|
||||
-v, --version Displays the current version of serve
|
||||
|
||||
-l, --listen {underline listen_uri} Specify a URI endpoint on which to listen (see below) -
|
||||
more than one may be specified to listen in multiple places
|
||||
|
||||
-p Specify custom port
|
||||
|
||||
-d, --debug Show debugging information
|
||||
|
||||
-s, --single Rewrite all not-found requests to \`index.html\`
|
||||
|
||||
-c, --config Specify custom path to \`serve.json\`
|
||||
|
||||
-C, --cors Enable CORS, sets \`Access-Control-Allow-Origin\` to \`*\`
|
||||
|
||||
-n, --no-clipboard Do not copy the local address to the clipboard
|
||||
|
||||
-u, --no-compression Do not compress files
|
||||
|
||||
--no-etag Send \`Last-Modified\` header instead of \`ETag\`
|
||||
|
||||
-S, --symlinks Resolve symlinks instead of showing 404 errors
|
||||
|
||||
--ssl-cert Optional path to an SSL/TLS certificate to serve with HTTPS
|
||||
|
||||
--ssl-key Optional path to the SSL/TLS certificate\'s private key
|
||||
|
||||
--ssl-pass Optional path to the SSL/TLS certificate\'s passphrase
|
||||
|
||||
--no-port-switching Do not open a port other than the one specified when it\'s taken.
|
||||
|
||||
{bold ENDPOINTS}
|
||||
|
||||
Listen endpoints (specified by the {bold --listen} or {bold -l} options above) instruct {cyan serve}
|
||||
to listen on one or more interfaces/ports, UNIX domain sockets, or Windows named pipes.
|
||||
|
||||
For TCP ports on hostname "localhost":
|
||||
|
||||
{bold $} {cyan serve} -l {underline 1234}
|
||||
|
||||
For TCP (traditional host/port) endpoints:
|
||||
|
||||
{bold $} {cyan serve} -l tcp://{underline hostname}:{underline 1234}
|
||||
|
||||
For UNIX domain socket endpoints:
|
||||
|
||||
{bold $} {cyan serve} -l unix:{underline /path/to/socket.sock}
|
||||
|
||||
For Windows named pipe endpoints:
|
||||
|
||||
{bold $} {cyan serve} -l pipe:\\\\.\\pipe\\{underline PipeName}
|
||||
`;
|
||||
|
||||
const parseEndpoint = (str) => {
|
||||
if (!isNaN(str)) {
|
||||
return [str];
|
||||
}
|
||||
|
||||
// We cannot use `new URL` here, otherwise it will not
|
||||
// parse the host properly and it would drop support for IPv6.
|
||||
const url = parse(str);
|
||||
|
||||
switch (url.protocol) {
|
||||
case 'pipe:': {
|
||||
// some special handling
|
||||
const cutStr = str.replace(/^pipe:/, '');
|
||||
|
||||
if (cutStr.slice(0, 4) !== '\\\\.\\') {
|
||||
throw new Error(`Invalid Windows named pipe endpoint: ${str}`);
|
||||
}
|
||||
|
||||
return [cutStr];
|
||||
}
|
||||
case 'unix:':
|
||||
if (!url.pathname) {
|
||||
throw new Error(`Invalid UNIX domain socket endpoint: ${str}`);
|
||||
}
|
||||
|
||||
return [url.pathname];
|
||||
case 'tcp:':
|
||||
url.port = url.port || '3000';
|
||||
return [parseInt(url.port, 10), url.hostname];
|
||||
default:
|
||||
throw new Error(`Unknown --listen endpoint scheme (protocol): ${url.protocol}`);
|
||||
}
|
||||
};
|
||||
|
||||
const registerShutdown = (fn) => {
|
||||
let run = false;
|
||||
|
||||
const wrapper = () => {
|
||||
if (!run) {
|
||||
run = true;
|
||||
fn();
|
||||
}
|
||||
};
|
||||
|
||||
process.on('SIGINT', wrapper);
|
||||
process.on('SIGTERM', wrapper);
|
||||
process.on('exit', wrapper);
|
||||
};
|
||||
|
||||
const getNetworkAddress = () => {
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
for (const interface of interfaces[name]) {
|
||||
const {address, family, internal} = interface;
|
||||
if (family === 'IPv4' && !internal) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startEndpoint = (endpoint, config, args, previous) => {
|
||||
const {isTTY} = process.stdout;
|
||||
const clipboard = args['--no-clipboard'] !== true;
|
||||
const compress = args['--no-compression'] !== true;
|
||||
const httpMode = args['--ssl-cert'] && args['--ssl-key'] ? 'https' : 'http';
|
||||
|
||||
const serverHandler = async (request, response) => {
|
||||
if (args['--cors']) {
|
||||
response.setHeader('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
if (compress) {
|
||||
await compressionHandler(request, response);
|
||||
}
|
||||
|
||||
return handler(request, response, config);
|
||||
};
|
||||
|
||||
const sslPass = args['--ssl-pass'];
|
||||
|
||||
const server = httpMode === 'https'
|
||||
? https.createServer({
|
||||
key: fs.readFileSync(args['--ssl-key']),
|
||||
cert: fs.readFileSync(args['--ssl-cert']),
|
||||
passphrase: sslPass ? fs.readFileSync(sslPass, "utf8") : ''
|
||||
}, serverHandler)
|
||||
: http.createServer(serverHandler);
|
||||
|
||||
server.on('error', (err) => {
|
||||
if (err.code === 'EADDRINUSE' && endpoint.length === 1 && !isNaN(endpoint[0]) && args['--no-port-switching'] !== true) {
|
||||
startEndpoint([0], config, args, endpoint[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(error(`Failed to serve: ${err.stack}`));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
server.listen(...endpoint, async () => {
|
||||
const details = server.address();
|
||||
registerShutdown(() => server.close());
|
||||
|
||||
let localAddress = null;
|
||||
let networkAddress = null;
|
||||
|
||||
if (typeof details === 'string') {
|
||||
localAddress = details;
|
||||
} else if (typeof details === 'object' && details.port) {
|
||||
const address = details.address === '::' ? 'localhost' : details.address;
|
||||
const ip = getNetworkAddress();
|
||||
|
||||
localAddress = `${httpMode}://${address}:${details.port}`;
|
||||
networkAddress = ip ? `${httpMode}://${ip}:${details.port}` : null;
|
||||
}
|
||||
|
||||
if (isTTY && process.env.NODE_ENV !== 'production') {
|
||||
let message = chalk.green('Serving!');
|
||||
|
||||
if (localAddress) {
|
||||
const prefix = networkAddress ? '- ' : '';
|
||||
const space = networkAddress ? ' ' : ' ';
|
||||
|
||||
message += `\n\n${chalk.bold(`${prefix}Local:`)}${space}${localAddress}`;
|
||||
}
|
||||
|
||||
if (networkAddress) {
|
||||
message += `\n${chalk.bold('- On Your Network:')} ${networkAddress}`;
|
||||
}
|
||||
|
||||
if (previous) {
|
||||
message += chalk.red(`\n\nThis port was picked because ${chalk.underline(previous)} is in use.`);
|
||||
}
|
||||
|
||||
if (clipboard) {
|
||||
try {
|
||||
await copy(localAddress);
|
||||
message += `\n\n${chalk.grey('Copied local address to clipboard!')}`;
|
||||
} catch (err) {
|
||||
console.error(error(`Cannot copy to clipboard: ${err.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(boxen(message, {
|
||||
padding: 1,
|
||||
borderColor: 'green',
|
||||
margin: 1
|
||||
}));
|
||||
} else {
|
||||
const suffix = localAddress ? ` at ${localAddress}` : '';
|
||||
console.log(info(`Accepting connections${suffix}`));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const loadConfig = async (cwd, entry, args) => {
|
||||
const files = [
|
||||
'serve.json',
|
||||
'now.json',
|
||||
'package.json'
|
||||
];
|
||||
|
||||
if (args['--config']) {
|
||||
files.unshift(args['--config']);
|
||||
}
|
||||
|
||||
const config = {};
|
||||
|
||||
for (const file of files) {
|
||||
const location = path.resolve(entry, file);
|
||||
let content = null;
|
||||
|
||||
try {
|
||||
content = await readFile(location, 'utf8');
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT' && file !== args['--config']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.error(error(`Not able to read ${location}: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
content = JSON.parse(content);
|
||||
} catch (err) {
|
||||
console.error(error(`Could not parse ${location} as JSON: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (typeof content !== 'object') {
|
||||
console.error(warning(`Didn't find a valid object in ${location}. Skipping...`));
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (file) {
|
||||
case 'now.json':
|
||||
content = content.static;
|
||||
break;
|
||||
case 'package.json':
|
||||
content = content.now.static;
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object.assign(config, content);
|
||||
console.log(info(`Discovered configuration in \`${file}\``));
|
||||
|
||||
if (file === 'now.json' || file === 'package.json') {
|
||||
console.error(warning('The config files `now.json` and `package.json` are deprecated. Please use `serve.json`.'));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry) {
|
||||
const {public} = config;
|
||||
config.public = path.relative(cwd, (public ? path.resolve(entry, public) : entry));
|
||||
}
|
||||
|
||||
if (Object.keys(config).length !== 0) {
|
||||
const ajv = new Ajv();
|
||||
const validateSchema = ajv.compile(schema);
|
||||
|
||||
if (!validateSchema(config)) {
|
||||
const defaultMessage = error('The configuration you provided is wrong:');
|
||||
const {message, params} = validateSchema.errors[0];
|
||||
|
||||
console.error(`${defaultMessage}\n${message}\n${JSON.stringify(params)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// "ETag" headers are enabled by default unless `--no-etag` is provided
|
||||
config.etag = !args['--no-etag'];
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
(async () => {
|
||||
let args = null;
|
||||
|
||||
try {
|
||||
args = arg({
|
||||
'--help': Boolean,
|
||||
'--version': Boolean,
|
||||
'--listen': [parseEndpoint],
|
||||
'--single': Boolean,
|
||||
'--debug': Boolean,
|
||||
'--config': String,
|
||||
'--no-clipboard': Boolean,
|
||||
'--no-compression': Boolean,
|
||||
'--no-etag': Boolean,
|
||||
'--symlinks': Boolean,
|
||||
'--cors': Boolean,
|
||||
'--no-port-switching': Boolean,
|
||||
'--ssl-cert': String,
|
||||
'--ssl-key': String,
|
||||
'--ssl-pass': String,
|
||||
'-h': '--help',
|
||||
'-v': '--version',
|
||||
'-l': '--listen',
|
||||
'-s': '--single',
|
||||
'-d': '--debug',
|
||||
'-c': '--config',
|
||||
'-n': '--no-clipboard',
|
||||
'-u': '--no-compression',
|
||||
'-S': '--symlinks',
|
||||
'-C': '--cors',
|
||||
// This is deprecated and only for backwards-compatibility.
|
||||
'-p': '--listen'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(error(err.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (process.env.NO_UPDATE_CHECK !== '1') {
|
||||
await updateCheck(args['--debug']);
|
||||
}
|
||||
|
||||
if (args['--version']) {
|
||||
console.log(pkg.version);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args['--help']) {
|
||||
console.log(getHelp());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args['--listen']) {
|
||||
// Default endpoint
|
||||
args['--listen'] = [[process.env.PORT || 3000]];
|
||||
}
|
||||
|
||||
if (args._.length > 1) {
|
||||
console.error(error('Please provide one path argument at maximum'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
const entry = args._.length > 0 ? path.resolve(args._[0]) : cwd;
|
||||
|
||||
const config = await loadConfig(cwd, entry, args);
|
||||
|
||||
if (args['--single']) {
|
||||
const {rewrites} = config;
|
||||
const existingRewrites = Array.isArray(rewrites) ? rewrites : [];
|
||||
|
||||
// As the first rewrite rule, make `--single` work
|
||||
config.rewrites = [{
|
||||
source: '**',
|
||||
destination: '/index.html'
|
||||
}, ...existingRewrites];
|
||||
}
|
||||
|
||||
if (args['--symlinks']) {
|
||||
config.symlinks = true;
|
||||
}
|
||||
|
||||
for (const endpoint of args['--listen']) {
|
||||
startEndpoint(endpoint, config, args);
|
||||
}
|
||||
|
||||
registerShutdown(() => {
|
||||
console.log(`\n${info('Gracefully shutting down. Please wait...')}`);
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log(`\n${warning('Force-closing all open sockets...')}`);
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
|
||||
# config/husky/pre-commit
|
||||
# Run `lint-staged` before every commit.
|
||||
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
FORCE_COLOR=2 pnpm lint-staged
|
|
@ -0,0 +1,205 @@
|
|||
# Contributing Guide
|
||||
|
||||
Thanks for your interest in contributing to `serve`! This guide will
|
||||
show you how to set up your environment and contribute to this library.
|
||||
|
||||
## Set Up
|
||||
|
||||
First, you need to install and be familiar the following:
|
||||
|
||||
- `git`: [Here](https://github.com/git-guides) is a great guide by GitHub on
|
||||
installing and getting started with Git.
|
||||
- `node` and `pnpm`:
|
||||
[This guide](https://nodejs.org/en/download/package-manager/) will help you
|
||||
install Node and [this one](https://pnpm.io/installation) will help you install PNPM. The
|
||||
recommended method is using the `n` version manager if you are on MacOS or Linux. Make sure
|
||||
you are using the [`current` version](https://github.com/nodejs/Release#release-schedule) of
|
||||
Node.
|
||||
|
||||
Once you have installed the above, follow
|
||||
[these instructions](https://docs.github.com/en/get-started/quickstart/fork-a-repo)
|
||||
to
|
||||
[`fork`](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks)
|
||||
and [`clone`](https://github.com/git-guides/git-clone) the repository
|
||||
(`vercel/serve`).
|
||||
|
||||
Once you have forked and cloned the repository, you can
|
||||
[pick out an issue](https://github.com/vercel/serve/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)
|
||||
you want to fix/implement!
|
||||
|
||||
## Making Changes
|
||||
|
||||
Once you have cloned the repository to your computer (say, in
|
||||
`~/code/serve`) and picked the issue you want to tackle, create a
|
||||
branch:
|
||||
|
||||
```sh
|
||||
> git switch --create branch-name
|
||||
```
|
||||
|
||||
While naming your branch, try to follow the below guidelines:
|
||||
|
||||
1. Prefix the branch name with the type of change being made:
|
||||
- `fix`: For a bug fix.
|
||||
- `feat`: For a new feature.
|
||||
- `test`: For any change related to tests.
|
||||
- `perf`: For a performance related change.
|
||||
- `build`: For changes related to the build process.
|
||||
- `ci`: For all changes to the CI files.
|
||||
- `refc`: For any refactoring work.
|
||||
- `docs`: For any documentation related changes.
|
||||
2. Make the branch name short but self-explanatory.
|
||||
|
||||
Once you have created a branch, you can start coding!
|
||||
|
||||
The CLI is written in
|
||||
[Typescript](https://github.com/microsoft/TypeScript#readme) and uses the
|
||||
[`current` version](https://github.com/nodejs/Release#release-schedule) of Node.
|
||||
The code is structured as follows:
|
||||
|
||||
```sh
|
||||
serve
|
||||
├── config
|
||||
│ └── husky
|
||||
│ ├── _
|
||||
│ └── pre-commit
|
||||
├── media
|
||||
│ ├── banner.png
|
||||
│ └── listing-ui.png
|
||||
├── source
|
||||
│ ├── utilities
|
||||
│ │ ├── cli.ts
|
||||
│ │ ├── config.ts
|
||||
│ │ ├── http.ts
|
||||
│ │ ├── logger.ts
|
||||
│ │ ├── promise.ts
|
||||
│ │ └── server.ts
|
||||
│ ├── main.ts
|
||||
│ └── types.ts
|
||||
├── contributing.md
|
||||
├── license.md
|
||||
├── package.json
|
||||
├── pnpm-lock.yaml
|
||||
├── readme.md
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
> Most files have a little description of what they do at the top.
|
||||
|
||||
#### `./`
|
||||
|
||||
- `package.json`: Node package manifest. This file contains the name, version,
|
||||
description, dependencies, scripts and package configuration of the project.
|
||||
- `pnpm-lock.yaml`: PNPM lock file, please do not modify it manually. Run
|
||||
`pnpm install` to update it if you add/remove a dependency to/from
|
||||
`package.json` manually.
|
||||
- `tsconfig.json`: The Typescript configuration for this project.
|
||||
- `contributing.md`: The file you are reading. It helps contributors get
|
||||
started.
|
||||
- `license.md`: Tells people how they can use the code.
|
||||
- `readme.md`: The file everyone should read before running the server. Contains
|
||||
installation and usage instructions.
|
||||
|
||||
#### `config/husky/`
|
||||
|
||||
- `pre-commit`: This file is a script that runs before Git commits code.
|
||||
|
||||
#### `source/utilities/`
|
||||
|
||||
- `utilities/config.ts`: Searches and parses the configuration for the CLI.
|
||||
- `utilities/http.ts`: Defines and exports helper functions for the server.
|
||||
- `utilities/server.ts`: Exports a function used to start the server with a
|
||||
given configuration on a certain port.
|
||||
- `utilities/promise.ts`: Exports utility functions and wrappers that help
|
||||
resolve `Promise`s.
|
||||
- `utilities/cli.ts`: Exports functions that help with CLI-related stuff, e.g.,
|
||||
parsing arguments and printing help text.
|
||||
- `utilities/logger.ts`: A barebones logger.
|
||||
|
||||
#### `source/`
|
||||
|
||||
- `main.ts`: Entrypoint for the CLI.
|
||||
- `types.ts`: Typescript types used in the project.
|
||||
|
||||
When adding a new feature/fixing a bug, please add/update the readme. Also make
|
||||
sure your code has been linted and that existing tests pass. You can run the linter
|
||||
using `pnpm lint`, the tests using `pnpm test` and try to automatically fix most lint
|
||||
issues using `pnpm lint --fix`.
|
||||
|
||||
You can run the CLI tool using `pnpm develop`, which will re-run the CLI everytime you
|
||||
save changes made to the code.
|
||||
|
||||
Once you have made changes to the code, you will want to
|
||||
[`commit`](https://github.com/git-guides/git-commit) (basically, Git's version
|
||||
of save) the changes. To commit the changes you have made locally:
|
||||
|
||||
```sh
|
||||
> git add this/folder that/file
|
||||
> git commit --message 'commit-message'
|
||||
```
|
||||
|
||||
While writing the `commit-message`, try to follow the below guidelines:
|
||||
|
||||
1. Prefix the message with `type:`, where `type` is one of the following
|
||||
dependending on what the commit does:
|
||||
- `fix`: Introduces a bug fix.
|
||||
- `feat`: Adds a new feature.
|
||||
- `test`: Any change related to tests.
|
||||
- `perf`: Any performance related change.
|
||||
- `build`: For changes related to the build process.
|
||||
- `ci`: For all changes to the CI files.
|
||||
- `refc`: Any refactoring work.
|
||||
- `docs`: Any documentation related changes.
|
||||
2. Keep the first line brief, and less than 60 characters.
|
||||
3. Try describing the change in detail in a new paragraph (double newline after
|
||||
the first line).
|
||||
|
||||
## Contributing Changes
|
||||
|
||||
Once you have committed your changes, you will want to
|
||||
[`push`](https://github.com/git-guides/git-push) (basically, publish your
|
||||
changes to GitHub) your commits. To push your changes to your fork:
|
||||
|
||||
```sh
|
||||
> git push -u origin branch-name
|
||||
```
|
||||
|
||||
If there are changes made to the `main` branch of the
|
||||
`vercel/serve` repository, you may wish to
|
||||
[`rebase`](https://docs.github.com/en/get-started/using-git/about-git-rebase)
|
||||
your branch to include those changes. To rebase, or include the changes from the
|
||||
`main` branch of the `vercel/serve` repository:
|
||||
|
||||
```
|
||||
> git fetch upstream main
|
||||
> git rebase upstream/main
|
||||
```
|
||||
|
||||
This will automatically add the changes from `main` branch of the
|
||||
`vercel/serve` repository to the current branch. If you encounter
|
||||
any merge conflicts, follow
|
||||
[this guide](https://docs.github.com/en/get-started/using-git/resolving-merge-conflicts-after-a-git-rebase)
|
||||
to resolve them.
|
||||
|
||||
Once you have pushed your changes to your fork, follow
|
||||
[these instructions](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
|
||||
to open a
|
||||
[`pull request`](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests):
|
||||
|
||||
Once you have submitted a pull request, the maintainers of the repository will
|
||||
review your pull requests. Whenever a maintainer reviews a pull request they may
|
||||
request changes. These may be small, such as fixing a typo, or may involve
|
||||
substantive changes. Such requests are intended to be helpful, but at times may
|
||||
come across as abrupt or unhelpful, especially if they do not include concrete
|
||||
suggestions on how to change them. Try not to be discouraged. If you feel that a
|
||||
review is unfair, say so or seek the input of another project contributor. Often
|
||||
such comments are the result of a reviewer having taken insufficient time to
|
||||
review and are not ill-intended. Such difficulties can often be resolved with a
|
||||
bit of patience. That said, reviewers should be expected to provide helpful
|
||||
feedback.
|
||||
|
||||
In order to land, a pull request needs to be reviewed and approved by at least
|
||||
one maintainer and pass CI. After that, if there are no objections from other
|
||||
contributors, the pull request can be merged.
|
||||
|
||||
#### Congratulations and thanks for your contribution!
|
|
@ -0,0 +1,20 @@
|
|||
# The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022 Vercel, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
After Width: | Height: | Size: 290 KiB |
File diff suppressed because it is too large
Load Diff
87
package.json
87
package.json
|
@ -2,32 +2,83 @@
|
|||
"name": "serve",
|
||||
"version": "13.0.4",
|
||||
"description": "Static file serving and directory listing",
|
||||
"scripts": {
|
||||
"test": ""
|
||||
},
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"repository": "vercel/serve",
|
||||
"bin": {
|
||||
"serve": "./bin/serve.js"
|
||||
},
|
||||
"keywords": [
|
||||
"vercel",
|
||||
"serve",
|
||||
"micro",
|
||||
"http-server"
|
||||
],
|
||||
"repository": "vercel/serve",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"serve": "./build/main.js"
|
||||
},
|
||||
"files": [
|
||||
"build/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
},
|
||||
"scripts": {
|
||||
"develop": "tsx watch ./source/main.ts",
|
||||
"start": "node ./build/main.js",
|
||||
"compile": "tsup ./source/main.ts",
|
||||
"test:tsc": "tsc --project tsconfig.json",
|
||||
"test": "pnpm test:tsc",
|
||||
"lint:code": "eslint --max-warnings 0 source/**/*.ts",
|
||||
"lint:style": "prettier --check --ignore-path .gitignore .",
|
||||
"lint": "pnpm lint:code && pnpm lint:style",
|
||||
"format": "prettier --write --ignore-path .gitignore .",
|
||||
"prepare": "husky install config/husky && pnpm compile"
|
||||
},
|
||||
"dependencies": {
|
||||
"@zeit/schemas": "2.6.0",
|
||||
"ajv": "6.12.6",
|
||||
"arg": "2.0.0",
|
||||
"boxen": "5.1.2",
|
||||
"chalk": "2.4.1",
|
||||
"clipboardy": "2.3.0",
|
||||
"compression": "1.7.3",
|
||||
"@zeit/schemas": "2.21.0",
|
||||
"ajv": "8.11.0",
|
||||
"arg": "5.0.2",
|
||||
"boxen": "7.0.0",
|
||||
"chalk": "5.0.1",
|
||||
"clipboardy": "3.0.0",
|
||||
"compression": "1.7.4",
|
||||
"is-port-reachable": "4.0.0",
|
||||
"serve-handler": "6.1.3",
|
||||
"update-check": "1.5.2"
|
||||
"update-check": "1.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "1.7.2",
|
||||
"@types/serve-handler": "6.1.1",
|
||||
"@vercel/style-guide": "3.0.0",
|
||||
"eslint": "8.19.0",
|
||||
"husky": "8.0.1",
|
||||
"lint-staged": "13.0.3",
|
||||
"prettier": "2.7.1",
|
||||
"tsup": "6.1.3",
|
||||
"tsx": "3.7.1",
|
||||
"typescript": "4.6.4"
|
||||
},
|
||||
"tsup": {
|
||||
"target": "esnext",
|
||||
"format": [
|
||||
"esm"
|
||||
],
|
||||
"outDir": "./build/"
|
||||
},
|
||||
"prettier": "@vercel/style-guide/prettier",
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"./node_modules/@vercel/style-guide/eslint/node.js",
|
||||
"./node_modules/@vercel/style-guide/eslint/typescript.js"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": "tsconfig.json"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": [
|
||||
"prettier --ignore-unknown --write"
|
||||
],
|
||||
"source/**/*.ts": [
|
||||
"eslint --max-warnings 0 --fix"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,96 @@
|
|||

|
||||
|
||||
<div align="center">
|
||||
<a aria-label="Vercel logo" href="https://vercel.com">
|
||||
<img src="https://img.shields.io/badge/made%20by-vercel-%23000000">
|
||||
</a>
|
||||
<br>
|
||||
<a aria-label="Install Size" href="https://packagephobia.com/result?p=serve">
|
||||
<img src="https://packagephobia.com/badge?p=serve">
|
||||
</a>
|
||||
<a aria-label="Stars" href="https://github.com/vercel/serve/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/vercel/serve">
|
||||
</a>
|
||||
<a aria-label="Build Status" href="https://github.com/vercel/serve/actions/workflows/ci.yaml">
|
||||
<img src="https://github.com/vercel/serve/actions/workflows/ci.yaml/badge.svg">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
`serve` helps you serve a static site, single page application or just a static file (no matter if on your device or on the local network). It also provides a neat interface for listing the directory's contents:
|
||||
|
||||

|
||||
|
||||
> Once it's time to push your site to production, we recommend using [Vercel](https://vercel.com).
|
||||
|
||||
## Usage
|
||||
|
||||
The quickest way to get started is to just run `npx serve` in your project's directory.
|
||||
|
||||
If you prefer, you can also install the package globally (you'll need at least [Node LTS](https://github.com/nodejs/Release#release-schedule)):
|
||||
|
||||
```bash
|
||||
> npm install --global serve
|
||||
```
|
||||
|
||||
Once that's done, you can run this command inside your project's directory...
|
||||
|
||||
```bash
|
||||
> serve
|
||||
```
|
||||
|
||||
...or specify which folder you want to serve:
|
||||
|
||||
```bash
|
||||
> serve folder-name/
|
||||
```
|
||||
|
||||
Finally, run this command to see a list of all available options:
|
||||
|
||||
```bash
|
||||
> serve --help
|
||||
```
|
||||
|
||||
Now you understand how the package works! :tada:
|
||||
|
||||
## Configuration
|
||||
|
||||
To customize `serve`'s behavior, create a `serve.json` file in the public folder and insert any of [these properties](https://github.com/vercel/serve-handler#options).
|
||||
|
||||
## API
|
||||
|
||||
The core of `serve` is [serve-handler](https://github.com/vercel/serve-handler), which can be used as middleware in existing HTTP servers:
|
||||
|
||||
```js
|
||||
const handler = require('serve-handler');
|
||||
const http = require('http');
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
// You pass two more arguments for config and middleware
|
||||
// More details here: https://github.com/vercel/serve-handler#options
|
||||
return handler(request, response);
|
||||
});
|
||||
|
||||
server.listen(3000, () => {
|
||||
console.log('Running at http://localhost:3000');
|
||||
});
|
||||
```
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> You can also replace `http.createServer` with [micro](https://github.com/vercel/micro).
|
||||
|
||||
## Issues and Contributing
|
||||
|
||||
If you want a feature to be added, or wish to report a bug, please open an issue [here](https://github.com/vercel/serve/issues/new).
|
||||
|
||||
If you wish to contribute to the project, please read the [contributing guide](contributing.md) first.
|
||||
|
||||
## Credits
|
||||
|
||||
This project used to be called "list" and "micro-list". But thanks to [TJ Holowaychuk](https://github.com/tj) handing us the new name, it's now called "serve" (which is much more definite).
|
||||
|
||||
## Author
|
||||
|
||||
Leo Lamprecht ([@notquiteleo](https://twitter.com/notquiteleo))
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// source/main.ts
|
||||
// The CLI for the `serve-handler` module.
|
||||
|
||||
import path from 'node:path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import clipboard from 'clipboardy';
|
||||
import checkForUpdate from 'update-check';
|
||||
import manifest from '../package.json';
|
||||
import { resolve } from './utilities/promise.js';
|
||||
import { startServer } from './utilities/server.js';
|
||||
import { registerCloseListener } from './utilities/http.js';
|
||||
import { parseArguments, getHelpText } from './utilities/cli.js';
|
||||
import { loadConfiguration } from './utilities/config.js';
|
||||
import { logger } from './utilities/logger.js';
|
||||
import type { Arguments } from './types.js';
|
||||
|
||||
/**
|
||||
* Checks for updates to this package. If an update is available, it brings it
|
||||
* to the user's notice by printing a message to the console.
|
||||
*
|
||||
* @param debugMode - Whether or not we should print additional debug information.
|
||||
* @returns
|
||||
*/
|
||||
const printUpdateNotification = async (debugMode?: boolean) => {
|
||||
const [error, update] = await resolve(checkForUpdate(manifest));
|
||||
|
||||
if (error) {
|
||||
const suffix = debugMode ? ':' : ' (use `--debug` to see full error)';
|
||||
logger.warn(`Checking for updates failed${suffix}`);
|
||||
|
||||
if (debugMode) logger.error(error.message);
|
||||
}
|
||||
if (!update) return;
|
||||
|
||||
logger.log(
|
||||
chalk` {bgRed.white UPDATE } The latest version of \`serve\` is ${update.latest}`,
|
||||
);
|
||||
};
|
||||
|
||||
// Parse the options passed by the user.
|
||||
let args: Arguments;
|
||||
try {
|
||||
args = parseArguments();
|
||||
} catch (error: unknown) {
|
||||
logger.error((error as Error).message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check for updates to the package unless the user sets the `NO_UPDATE_CHECK`
|
||||
// variable.
|
||||
if (process.env.NO_UPDATE_CHECK !== '1')
|
||||
await printUpdateNotification(args['--debug']);
|
||||
// If the `version` or `help` arguments are passed, print the version or the
|
||||
// help text and exit.
|
||||
if (args['--version']) {
|
||||
logger.log(manifest.version);
|
||||
process.exit(0);
|
||||
}
|
||||
if (args['--help']) {
|
||||
logger.log(getHelpText());
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Default to listening on port 3000.
|
||||
if (!args['--listen'])
|
||||
args['--listen'] = [
|
||||
[process.env.PORT ? parseInt(process.env.PORT, 10) : 3000],
|
||||
];
|
||||
// Ensure that the user has passed only one directory to serve.
|
||||
if (args._.length > 1) {
|
||||
logger.error('Please provide one path argument at maximum');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Warn the user about using deprecated configuration files.
|
||||
if (args['--config'] === 'now.json' || args['--config'] === 'package.json')
|
||||
logger.warn(
|
||||
'The config files `now.json` and `package.json` are deprecated. Please use `serve.json`.',
|
||||
);
|
||||
// Parse the configuration.
|
||||
const cwd = process.cwd();
|
||||
const entry = args._[0] ? path.resolve(args._[0]) : cwd;
|
||||
const config = await loadConfiguration(cwd, entry, args);
|
||||
|
||||
// If the user wants all the URLs rewritten to `/index.html`, make it happen.
|
||||
if (args['--single']) {
|
||||
const { rewrites } = config;
|
||||
const existingRewrites = Array.isArray(rewrites) ? rewrites : [];
|
||||
|
||||
// Ensure this is the first rewrite rule so it gets priority.
|
||||
config.rewrites = [
|
||||
{
|
||||
source: '**',
|
||||
destination: '/index.html',
|
||||
},
|
||||
...existingRewrites,
|
||||
];
|
||||
}
|
||||
|
||||
// Start the server for each endpoint passed by the user.
|
||||
for (const endpoint of args['--listen']) {
|
||||
// Disabling this rule as we want to start each server one by one.
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { local, network, previous } = await startServer(
|
||||
endpoint,
|
||||
config,
|
||||
args,
|
||||
);
|
||||
|
||||
const copyAddress = !args['--no-clipboard'];
|
||||
|
||||
// If we are not in a TTY or Node is running in production mode, print
|
||||
// a single line of text with the server address.
|
||||
if (!process.stdout.isTTY || process.env.NODE_ENV === 'production') {
|
||||
const suffix = local ? ` at ${local}` : '';
|
||||
logger.info(`Accepting connections${suffix}`);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Else print a fancy box with the server address.
|
||||
let message = chalk.green('Serving!');
|
||||
if (local) {
|
||||
const prefix = network ? '- ' : '';
|
||||
const space = network ? ' ' : ' ';
|
||||
|
||||
message += `\n\n${chalk.bold(`${prefix}Local:`)}${space}${local}`;
|
||||
}
|
||||
if (network) message += `\n${chalk.bold('- On Your Network:')} ${network}`;
|
||||
if (previous)
|
||||
message += chalk.red(
|
||||
`\n\nThis port was picked because ${chalk.underline(
|
||||
previous.toString(),
|
||||
)} is in use.`,
|
||||
);
|
||||
|
||||
// Try to copy the address to the user's clipboard too.
|
||||
if (copyAddress && local) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await clipboard.write(local);
|
||||
message += `\n\n${chalk.grey('Copied local address to clipboard!')}`;
|
||||
} catch (error: unknown) {
|
||||
logger.error(`Cannot copy to clipboard: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.log(
|
||||
boxen(message, {
|
||||
padding: 1,
|
||||
borderColor: 'green',
|
||||
margin: 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Print out a message to let the user know we are shutting down the server
|
||||
// when they press Ctrl+C or kill the process externally.
|
||||
registerCloseListener(() => {
|
||||
logger.log();
|
||||
logger.info('Gracefully shutting down. Please wait...');
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
logger.log();
|
||||
logger.warn('Force-closing all open sockets...');
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
// source/types.ts
|
||||
// Type definitions for the CLI.
|
||||
|
||||
// An error thrown by any native Node modules.
|
||||
export declare interface NodeError extends Error {
|
||||
code: string;
|
||||
}
|
||||
|
||||
// A path to a file/remote resource.
|
||||
export declare type Path = string;
|
||||
// The port to bind the server on.
|
||||
export declare type Port = number;
|
||||
// The name of the host.
|
||||
export declare type Host = string;
|
||||
// The address of the server.
|
||||
export declare interface ServerAddress {
|
||||
local?: string;
|
||||
network?: string;
|
||||
previous?: number;
|
||||
}
|
||||
|
||||
// The endpoint the server should listen on.
|
||||
export declare type ListenEndpoint =
|
||||
| number
|
||||
| `tcp://${Host}:${Port}`
|
||||
| `unix:${Path}`
|
||||
| `pipe:\\\\.\\pipe\\${Host}`;
|
||||
|
||||
// The parsed endpoints.
|
||||
export declare type ParsedEndpoint = [Port] | [Host] | [Port, Host];
|
||||
|
||||
// An entry for URL rewrites.
|
||||
export declare interface Rewrite {
|
||||
source: string;
|
||||
destination: string;
|
||||
}
|
||||
|
||||
// An entry for redirecting a URL.
|
||||
export declare type Redirect = Rewrite & {
|
||||
type: number;
|
||||
};
|
||||
|
||||
// An entry to send headers for.
|
||||
export declare interface Header {
|
||||
source: string;
|
||||
headers: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// The configuration for the CLI.
|
||||
export declare interface Configuration {
|
||||
public: Path;
|
||||
cleanUrls: boolean | Path[];
|
||||
rewrites: Rewrite[];
|
||||
redirects: Redirect[];
|
||||
headers: Header[];
|
||||
directoryListing: boolean | Path[];
|
||||
unlisted: Path[];
|
||||
trailingSlash: boolean;
|
||||
renderSingle: boolean;
|
||||
symlinks: boolean;
|
||||
etag: boolean;
|
||||
}
|
||||
|
||||
// The options you can pass to the CLI.
|
||||
export declare interface Options {
|
||||
'--help': boolean;
|
||||
'--version': boolean;
|
||||
'--listen': ParsedEndpoint[];
|
||||
'--single': boolean;
|
||||
'--debug': boolean;
|
||||
'--config': Path;
|
||||
'--no-clipboard': boolean;
|
||||
'--no-compression': boolean;
|
||||
'--no-etag': boolean;
|
||||
'--symlinks': boolean;
|
||||
'--cors': boolean;
|
||||
'--no-port-switching': boolean;
|
||||
'--ssl-cert': Path;
|
||||
'--ssl-key': Path;
|
||||
'--ssl-pass': string;
|
||||
}
|
||||
|
||||
// The arguments passed to the CLI (the options + the positional arguments)
|
||||
export declare type Arguments = Partial<Options> & {
|
||||
_: string[];
|
||||
};
|
|
@ -0,0 +1,127 @@
|
|||
// source/utilities/cli.ts
|
||||
// CLI-related utility functions.
|
||||
|
||||
import chalk from 'chalk';
|
||||
import parseArgv from 'arg';
|
||||
import { parseEndpoint } from './http.js';
|
||||
import type { Arguments } from '../types.js';
|
||||
|
||||
// The options the CLI accepts, and how to parse them.
|
||||
const options = {
|
||||
'--help': Boolean,
|
||||
'--version': Boolean,
|
||||
'--listen': [parseEndpoint] as [typeof parseEndpoint],
|
||||
'--single': Boolean,
|
||||
'--debug': Boolean,
|
||||
'--config': String,
|
||||
'--no-clipboard': Boolean,
|
||||
'--no-compression': Boolean,
|
||||
'--no-etag': Boolean,
|
||||
'--symlinks': Boolean,
|
||||
'--cors': Boolean,
|
||||
'--no-port-switching': Boolean,
|
||||
'--ssl-cert': String,
|
||||
'--ssl-key': String,
|
||||
'--ssl-pass': String,
|
||||
// A list of aliases for the above options.
|
||||
'-h': '--help',
|
||||
'-v': '--version',
|
||||
'-l': '--listen',
|
||||
'-s': '--single',
|
||||
'-d': '--debug',
|
||||
'-c': '--config',
|
||||
'-n': '--no-clipboard',
|
||||
'-u': '--no-compression',
|
||||
'-S': '--symlinks',
|
||||
'-C': '--cors',
|
||||
|
||||
// The `-p` option is deprecated and is kept only for backwards-compatibility.
|
||||
'-p': '--listen',
|
||||
};
|
||||
|
||||
// The help text for the CLI.
|
||||
const helpText = chalk`
|
||||
{bold.cyan serve} - Static file serving and directory listing
|
||||
|
||||
{bold USAGE}
|
||||
|
||||
{bold $} {cyan serve} --help
|
||||
{bold $} {cyan serve} --version
|
||||
{bold $} {cyan serve} folder_name
|
||||
{bold $} {cyan serve} [-l {underline listen_uri} [-l ...]] [{underline directory}]
|
||||
|
||||
By default, {cyan serve} will listen on {bold 0.0.0.0:3000} and serve the
|
||||
current working directory on that address.
|
||||
|
||||
Specifying a single {bold --listen} argument will overwrite the default, not supplement it.
|
||||
|
||||
{bold OPTIONS}
|
||||
|
||||
--help Shows this help message
|
||||
|
||||
-v, --version Displays the current version of serve
|
||||
|
||||
-l, --listen {underline listen_uri} Specify a URI endpoint on which to listen (see below) -
|
||||
more than one may be specified to listen in multiple places
|
||||
|
||||
-p Specify custom port
|
||||
|
||||
-d, --debug Show debugging information
|
||||
|
||||
-s, --single Rewrite all not-found requests to \`index.html\`
|
||||
|
||||
-c, --config Specify custom path to \`serve.json\`
|
||||
|
||||
-C, --cors Enable CORS, sets \`Access-Control-Allow-Origin\` to \`*\`
|
||||
|
||||
-n, --no-clipboard Do not copy the local address to the clipboard
|
||||
|
||||
-u, --no-compression Do not compress files
|
||||
|
||||
--no-etag Send \`Last-Modified\` header instead of \`ETag\`
|
||||
|
||||
-S, --symlinks Resolve symlinks instead of showing 404 errors
|
||||
|
||||
--ssl-cert Optional path to an SSL/TLS certificate to serve with HTTPS
|
||||
|
||||
--ssl-key Optional path to the SSL/TLS certificate\'s private key
|
||||
|
||||
--ssl-pass Optional path to the SSL/TLS certificate\'s passphrase
|
||||
|
||||
--no-port-switching Do not open a port other than the one specified when it\'s taken.
|
||||
|
||||
{bold ENDPOINTS}
|
||||
|
||||
Listen endpoints (specified by the {bold --listen} or {bold -l} options above) instruct {cyan serve}
|
||||
to listen on one or more interfaces/ports, UNIX domain sockets, or Windows named pipes.
|
||||
|
||||
For TCP ports on hostname "localhost":
|
||||
|
||||
{bold $} {cyan serve} -l {underline 1234}
|
||||
|
||||
For TCP (traditional host/port) endpoints:
|
||||
|
||||
{bold $} {cyan serve} -l tcp://{underline hostname}:{underline 1234}
|
||||
|
||||
For UNIX domain socket endpoints:
|
||||
|
||||
{bold $} {cyan serve} -l unix:{underline /path/to/socket.sock}
|
||||
|
||||
For Windows named pipe endpoints:
|
||||
|
||||
{bold $} {cyan serve} -l pipe:\\\\.\\pipe\\{underline PipeName}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Parses the program's `process.argv` and returns the options and arguments.
|
||||
*
|
||||
* @returns The parsed options and arguments.
|
||||
*/
|
||||
export const parseArguments = (): Arguments => parseArgv(options);
|
||||
|
||||
/**
|
||||
* Returns the help text.
|
||||
*
|
||||
* @returns The help text shown when the `--help` option is used.
|
||||
*/
|
||||
export const getHelpText = (): string => helpText;
|
|
@ -0,0 +1,134 @@
|
|||
// source/utilities/config.ts
|
||||
// Parse and return the configuration for the CLI.
|
||||
|
||||
import {
|
||||
resolve as resolvePath,
|
||||
relative as resolveRelativePath,
|
||||
} from 'node:path';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import Ajv from 'ajv';
|
||||
// @ts-expect-error No type definitions.
|
||||
import schema from '@zeit/schemas/deployment/config-static.js';
|
||||
import { resolve } from './promise.js';
|
||||
import type { ErrorObject } from 'ajv';
|
||||
import type { Configuration, Options, NodeError } from '../types.js';
|
||||
|
||||
/**
|
||||
* Parses and returns a configuration object from the designated locations.
|
||||
*
|
||||
* @param cwd - The current working directory.
|
||||
* @param entry - The directory to serve.
|
||||
* @param args - The arguments passed to the CLI.
|
||||
*
|
||||
* @returns The parsed configuration.
|
||||
*/
|
||||
export const loadConfiguration = async (
|
||||
cwd: string,
|
||||
entry: string,
|
||||
args: Partial<Options>,
|
||||
): Promise<Configuration> => {
|
||||
const files = ['serve.json', 'now.json', 'package.json'];
|
||||
if (args['--config']) files.unshift(args['--config']);
|
||||
|
||||
const config: Partial<Configuration> = {};
|
||||
for (const file of files) {
|
||||
// Resolve the path to the configuration file relative to the directory
|
||||
// with the content in it.
|
||||
const location = resolvePath(entry, file);
|
||||
|
||||
// Disabling the lint rule as we don't want to read all the files at once;
|
||||
// if we can retrieve the configuration from the first file itself, we
|
||||
// shouldn't waste time and resources fetching the other files too.
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const [error, rawContents] = await resolve<string, NodeError>(
|
||||
readFile(location, 'utf8'),
|
||||
);
|
||||
if (error) {
|
||||
if (error.code === 'ENOENT' && file !== args['--config']) continue;
|
||||
else
|
||||
throw new Error(
|
||||
`Could not read configuration from file ${location}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
// The configuration can come from three files in different forms:
|
||||
// - now.json: `/now/static`
|
||||
// - package.json: `/static`
|
||||
// - serve.json: `/`
|
||||
type ServeConfigurationFile = Partial<Configuration>;
|
||||
interface ManifestConfigurationFile {
|
||||
static?: Partial<Configuration>;
|
||||
}
|
||||
interface NowConfigurationFile {
|
||||
now: { static?: Partial<Configuration> };
|
||||
}
|
||||
type ParseableConfiguration =
|
||||
| ServeConfigurationFile
|
||||
| ManifestConfigurationFile
|
||||
| NowConfigurationFile
|
||||
| undefined;
|
||||
|
||||
// Parse the JSON in the file. If the parsed JSON is not an object, or the
|
||||
// file does not contain valid JSON, throw an error.
|
||||
let parsedJson: ParseableConfiguration;
|
||||
try {
|
||||
parsedJson = JSON.parse(rawContents) as ParseableConfiguration;
|
||||
if (typeof parsedJson !== 'object')
|
||||
throw new Error('configuration is not an object');
|
||||
} catch (parserError: unknown) {
|
||||
throw new Error(
|
||||
`Could not parse ${location} as JSON: ${
|
||||
(parserError as Error).message
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if any of these files have a serve specific section.
|
||||
if (file === 'now.json') {
|
||||
parsedJson = parsedJson as NowConfigurationFile;
|
||||
parsedJson = parsedJson.now.static;
|
||||
} else if (file === 'package.json') {
|
||||
parsedJson = parsedJson as ManifestConfigurationFile;
|
||||
parsedJson = parsedJson.static;
|
||||
}
|
||||
if (!parsedJson) continue;
|
||||
|
||||
// Once we have found a valid configuration, assign it and stop looking
|
||||
// through more configuration files.
|
||||
Object.assign(config, parsedJson);
|
||||
break;
|
||||
}
|
||||
|
||||
// Make sure the directory with the content is relative to the entry path
|
||||
// provided by the user.
|
||||
if (entry) {
|
||||
const staticDirectory = config.public;
|
||||
config.public = resolveRelativePath(
|
||||
cwd,
|
||||
staticDirectory ? resolvePath(entry, staticDirectory) : entry,
|
||||
);
|
||||
}
|
||||
|
||||
// If the configuration isn't empty, validate it against the AJV schema.
|
||||
if (Object.keys(config).length !== 0) {
|
||||
const ajv = new Ajv({ allowUnionTypes: true });
|
||||
const validate = ajv.compile(schema as object);
|
||||
|
||||
if (!validate(config) && validate.errors) {
|
||||
const defaultMessage = 'The configuration you provided is invalid:';
|
||||
const error = validate.errors[0] as ErrorObject;
|
||||
|
||||
throw new Error(
|
||||
`${defaultMessage}\n${error.message ?? ''}\n${JSON.stringify(
|
||||
error.params,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure defaults based on the options the user has passed.
|
||||
config.etag = !args['--no-etag'];
|
||||
config.symlinks = args['--symlinks'] || config.symlinks;
|
||||
|
||||
return config as Configuration;
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
// source/utilities/http.ts
|
||||
// Helper functions for the server.
|
||||
|
||||
import { parse } from 'node:url';
|
||||
import { networkInterfaces as getNetworkInterfaces } from 'node:os';
|
||||
import type { ParsedEndpoint } from '../types.js';
|
||||
|
||||
const networkInterfaces = getNetworkInterfaces();
|
||||
|
||||
/**
|
||||
* Parse and return the endpoints from the given string.
|
||||
*
|
||||
* @param uriOrPort - The endpoint to listen on.
|
||||
* @returns A list of parsed endpoints.
|
||||
*/
|
||||
export const parseEndpoint = (uriOrPort: string): ParsedEndpoint => {
|
||||
// If the endpoint is a port number, return it as is.
|
||||
if (!isNaN(Number(uriOrPort))) return [uriOrPort];
|
||||
|
||||
// Cast it as a string, since we know for sure it is not a number.
|
||||
const endpoint = uriOrPort;
|
||||
|
||||
// We cannot use `new URL` here, otherwise it will not
|
||||
// parse the host properly and it would drop support for IPv6.
|
||||
const url = parse(endpoint);
|
||||
|
||||
switch (url.protocol) {
|
||||
case 'pipe:': {
|
||||
const pipe = endpoint.replace(/^pipe:/, '');
|
||||
if (!pipe.startsWith('\\\\.\\'))
|
||||
throw new Error(`Invalid Windows named pipe endpoint: ${endpoint}`);
|
||||
|
||||
return [pipe];
|
||||
}
|
||||
case 'unix:':
|
||||
if (!url.pathname)
|
||||
throw new Error(`Invalid UNIX domain socket endpoint: ${endpoint}`);
|
||||
|
||||
return [url.pathname];
|
||||
case 'tcp:':
|
||||
url.port = url.port ?? '3000';
|
||||
url.hostname = url.hostname ?? 'localhost';
|
||||
|
||||
return [parseInt(url.port, 10), url.hostname];
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown --listen endpoint scheme (protocol): ${
|
||||
url.protocol ?? 'undefined'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers a function that runs on server shutdown.
|
||||
*
|
||||
* @param fn - The function to run on server shutdown
|
||||
*/
|
||||
export const registerCloseListener = (fn: () => void) => {
|
||||
let run = false;
|
||||
|
||||
const wrapper = () => {
|
||||
if (!run) {
|
||||
run = true;
|
||||
fn();
|
||||
}
|
||||
};
|
||||
|
||||
process.on('SIGINT', wrapper);
|
||||
process.on('SIGTERM', wrapper);
|
||||
process.on('exit', wrapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the IP address of the host.
|
||||
*
|
||||
* @returns The address of the host.
|
||||
*/
|
||||
export const getNetworkAddress = () => {
|
||||
for (const interfaceDetails of Object.values(networkInterfaces)) {
|
||||
if (!interfaceDetails) continue;
|
||||
|
||||
for (const details of interfaceDetails) {
|
||||
const { address, family, internal } = details;
|
||||
|
||||
if (family === 'IPv4' && !internal) return address;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
// source/utilities/logger.ts
|
||||
// A simple colorized console logger.
|
||||
|
||||
/* eslint no-console: 0 */
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
const info = (...message: string[]) =>
|
||||
console.error(chalk.magenta('INFO:', ...message));
|
||||
const warn = (...message: string[]) =>
|
||||
console.error(chalk.yellow('WARNING:', ...message));
|
||||
const error = (...message: string[]) =>
|
||||
console.error(chalk.red('ERROR:', ...message));
|
||||
const log = console.log;
|
||||
|
||||
export const logger = { info, warn, error, log };
|
|
@ -0,0 +1,30 @@
|
|||
// source/utilities/promise.ts
|
||||
// Exports Promise-related utilities.
|
||||
|
||||
/**
|
||||
* Waits for the passed promise to resolve, then returns the data and error
|
||||
* in an array, similar to Go.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* ```
|
||||
* const [error, data] = await resolve(dance())
|
||||
* if (error) console.error(error)
|
||||
* else console.log(data)
|
||||
* ```
|
||||
*
|
||||
* @param promise - The promise to resolve.
|
||||
* @returns An array containing the error as the first element, and the resolved
|
||||
* data as the second element.
|
||||
*/
|
||||
export const resolve = <T = unknown, E = Error>(
|
||||
promise: Promise<T>,
|
||||
): Promise<[E, undefined] | [undefined, T]> =>
|
||||
promise
|
||||
.then<[undefined, T]>((data) => [undefined, data])
|
||||
.catch<[E, undefined]>((error) => [error, undefined]);
|
||||
|
||||
/**
|
||||
* Promisifies the passed function.
|
||||
*/
|
||||
export { promisify } from 'node:util';
|
|
@ -0,0 +1,158 @@
|
|||
// source/utilities/server.ts
|
||||
// Run the server with the given configuration.
|
||||
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import handler from 'serve-handler';
|
||||
import compression from 'compression';
|
||||
import isPortReachable from 'is-port-reachable';
|
||||
import { getNetworkAddress, registerCloseListener } from './http.js';
|
||||
import { promisify } from './promise.js';
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
import type {
|
||||
Configuration,
|
||||
Options,
|
||||
ParsedEndpoint,
|
||||
Port,
|
||||
ServerAddress,
|
||||
} from '../types.js';
|
||||
|
||||
const compress = promisify(compression());
|
||||
|
||||
/**
|
||||
* Starts the server and makes it listen on the given endpoint.
|
||||
*
|
||||
* @param endpoint - The endpoint to listen on.
|
||||
* @param config - The configuration for the `serve-handler` middleware.
|
||||
* @param args - The arguments passed to the CLI.
|
||||
* @returns The address of the server.
|
||||
*/
|
||||
export const startServer = async (
|
||||
endpoint: ParsedEndpoint,
|
||||
config: Partial<Configuration>,
|
||||
args: Partial<Options>,
|
||||
previous?: Port,
|
||||
): Promise<ServerAddress> => {
|
||||
// Define the request handler for the server.
|
||||
const serverHandler = (
|
||||
request: IncomingMessage,
|
||||
response: ServerResponse,
|
||||
): void => {
|
||||
// We can't return a promise in a HTTP request handler, so we run our code
|
||||
// inside an async function instead.
|
||||
const run = async () => {
|
||||
type ExpressRequest = Parameters<typeof compress>[0];
|
||||
type ExpressResponse = Parameters<typeof compress>[1];
|
||||
|
||||
if (args['--cors'])
|
||||
response.setHeader('Access-Control-Allow-Origin', '*');
|
||||
if (!args['--no-compression'])
|
||||
await compress(request as ExpressRequest, response as ExpressResponse);
|
||||
|
||||
// Let the `serve-handler` module do the rest.
|
||||
await handler(request, response, config);
|
||||
};
|
||||
|
||||
// Then we run the async function, and re-throw any errors.
|
||||
run().catch((error: Error) => {
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// Create the server.
|
||||
const useSsl = args['--ssl-cert'] && args['--ssl-key'];
|
||||
const httpMode = useSsl ? 'https' : 'http';
|
||||
const sslPass = args['--ssl-pass'];
|
||||
const serverConfig =
|
||||
httpMode === 'https' && args['--ssl-cert'] && args['--ssl-key']
|
||||
? {
|
||||
key: await readFile(args['--ssl-key']),
|
||||
cert: await readFile(args['--ssl-cert']),
|
||||
passphrase: sslPass ? await readFile(sslPass, 'utf8') : '',
|
||||
}
|
||||
: {};
|
||||
const server =
|
||||
httpMode === 'https'
|
||||
? https.createServer(serverConfig, serverHandler)
|
||||
: http.createServer(serverHandler);
|
||||
|
||||
// Once the server starts, return the address it is running on so the CLI
|
||||
// can tell the user.
|
||||
const getServerDetails = () => {
|
||||
// Make sure to close the server once the process ends.
|
||||
registerCloseListener(() => server.close());
|
||||
|
||||
// Once the server has started, get the address the server is running on
|
||||
// and return it.
|
||||
const details = server.address() as string | AddressInfo;
|
||||
let local: string | undefined;
|
||||
let network: string | undefined;
|
||||
if (typeof details === 'string') {
|
||||
local = details;
|
||||
} else if (typeof details === 'object' && details.port) {
|
||||
// According to https://www.ietf.org/rfc/rfc2732.txt, IPv6 addresses
|
||||
// should be surrounded by square brackets (only the address, not the
|
||||
// port).
|
||||
let address;
|
||||
if (details.address === '::') address = 'localhost';
|
||||
else if (details.family === 'IPv6') address = `[${details.address}]`;
|
||||
else address = details.address;
|
||||
const ip = getNetworkAddress();
|
||||
|
||||
local = `${httpMode}://${address}:${details.port}`;
|
||||
network = ip ? `${httpMode}://${ip}:${details.port}` : undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
local,
|
||||
network,
|
||||
previous,
|
||||
};
|
||||
};
|
||||
|
||||
// Listen for any error that occurs while serving, and throw an error
|
||||
// if any errors are received.
|
||||
server.on('error', (error) => {
|
||||
throw new Error(
|
||||
`Failed to serve: ${error.stack?.toString() ?? error.message}`,
|
||||
);
|
||||
});
|
||||
|
||||
// If the endpoint is a non-zero port, make sure it is not occupied.
|
||||
if (
|
||||
typeof endpoint[0] === 'number' &&
|
||||
!isNaN(endpoint[0]) &&
|
||||
endpoint[0] !== 0
|
||||
) {
|
||||
const port = endpoint[0];
|
||||
const isClosed = await isPortReachable(port, {
|
||||
host: endpoint[1] ?? 'localhost',
|
||||
});
|
||||
// If the port is already taken, then start the server on a random port
|
||||
// instead.
|
||||
if (isClosed) return startServer([0], config, args, port);
|
||||
|
||||
// Otherwise continue on to starting the server.
|
||||
}
|
||||
|
||||
// Finally, start the server.
|
||||
return new Promise((resolve, _reject) => {
|
||||
// If only a port is specified, listen on the given port on localhost.
|
||||
if (endpoint.length === 1 && typeof endpoint[0] === 'number')
|
||||
server.listen(endpoint[0], () => resolve(getServerDetails()));
|
||||
// If the path to a socket or a pipe is given, listen on it.
|
||||
else if (endpoint.length === 1 && typeof endpoint[0] === 'string')
|
||||
server.listen(endpoint[0], () => resolve(getServerDetails()));
|
||||
// If a port number and hostname are given, listen on `host:port`.
|
||||
else if (
|
||||
endpoint.length === 2 &&
|
||||
typeof endpoint[0] === 'number' &&
|
||||
typeof endpoint[1] === 'string'
|
||||
)
|
||||
server.listen(endpoint[0], endpoint[1], () =>
|
||||
resolve(getServerDetails()),
|
||||
);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "@vercel/style-guide/typescript",
|
||||
"compilerOptions": {
|
||||
"lib": ["es2020"],
|
||||
"target": "es2020",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["source/"]
|
||||
}
|
Loading…
Reference in New Issue