Skip to content

Commit

Permalink
feat(dev-server): first implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
LarsDenBakker committed Aug 30, 2020
1 parent 749fa47 commit 0cc6a82
Show file tree
Hide file tree
Showing 88 changed files with 1,470 additions and 73 deletions.
5 changes: 5 additions & 0 deletions .changeset/proud-kangaroos-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@web/dev-server-core': patch
---

expose ErrorWithLocation class
6 changes: 6 additions & 0 deletions .changeset/rich-buckets-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@web/dev-server': patch
'@web/dev-server-cli': patch
---

first implementation
5 changes: 5 additions & 0 deletions .changeset/tasty-crabs-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@web/test-runner-cli': patch
---

add test options to startTestRunner
5 changes: 5 additions & 0 deletions .changeset/weak-zebras-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@web/test-runner': patch
---

expose a startTestRunner function
2 changes: 1 addition & 1 deletion .github/workflows/ci-postinstall.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const fs = require('fs');
const path = require('path');

const testRunnerBin = path.resolve('packages', 'test-runner', 'dist', 'test-runner.js');
const testRunnerBin = path.resolve('packages', 'test-runner', 'dist', 'bin.js');
const projectsDir = path.resolve('demo', 'projects');
const projects = fs.readdirSync(projectsDir);

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"start": "rocket-start",
"start:build": "es-dev-server --root-dir _site --compatibility none --open",
"test": "yarn test:node && yarn test:browser && node scripts/workspaces-scripts-bin.mjs test:ci",
"test:browser": "node packages/test-runner/dist/test-runner.js \"packages/*/test-browser/**/*.test.{js,ts}\"",
"test:browser": "node packages/test-runner/dist/bin.js \"packages/*/test-browser/**/*.test.{js,ts}\"",
"test:node": "mocha \"packages/*/test/!(fixtures|browser)/**/*.test.{ts,js,mjs,cjs}\" --require ts-node/register --parallel --reporter dot --exit --retries 3",
"update-dependency": "node scripts/update-dependency.js",
"update-esm-entrypoints": "node scripts/update-esm-entrypoints.mjs && yarn format",
Expand Down
4 changes: 2 additions & 2 deletions packages/browser-logs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
},
"scripts": {
"build": "tsc",
"test": "node ../test-runner/dist/test-runner.js test-browser/**/*.test.ts",
"test:watch": "node ../test-runner/dist/test-runner.js test-browser/**/*.test.ts --watch"
"test": "node ../test-runner/dist/bin.js test-browser/**/*.test.ts",
"test:watch": "node ../test-runner/dist/bin.js test-browser/**/*.test.ts --watch"
},
"files": [
"*.d.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/dev-server-cli/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dist/index.js';
6 changes: 6 additions & 0 deletions packages/dev-server-cli/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// this file is autogenerated with the update-esm-entrypoints script
import cjsEntrypoint from './dist/index.js';

const { startDevServer, readConfig, validateCoreConfig, readCliArgsConfig } = cjsEntrypoint;

export { startDevServer, readConfig, validateCoreConfig, readCliArgsConfig };
51 changes: 51 additions & 0 deletions packages/dev-server-cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@web/dev-server-cli",
"version": "0.0.0",
"publishConfig": {
"access": "public"
},
"description": "Dev server for web applications",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/modernweb-dev/web.git",
"directory": "packages/dev-server-cli"
},
"author": "modern-web",
"homepage": "https://github.com/modernweb-dev/web/tree/master/packages/dev-server-cli",
"main": "dist/index.js",
"engines": {
"node": ">=10.0.0"
},
"scripts": {
"build": "tsc",
"test": "mocha \"test/**/*.test.ts\" --require ts-node/register --reporter dot",
"test:watch": "mocha \"test/**/*.test.ts\" --require ts-node/register --watch --watch-files src,test --reporter dot"
},
"files": [
"dist"
],
"keywords": [
"web",
"dev",
"server",
"implementation",
"cli"
],
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@types/command-line-args": "^5.0.0",
"@web/config-loader": "^0.1.1",
"@web/dev-server-core": "^0.2.2",
"camelcase": "^6.0.0",
"chalk": "^4.1.0",
"command-line-args": "^5.1.1",
"command-line-usage": "^6.1.0",
"ip": "^1.1.5",
"open": "^7.1.0",
"portfinder": "^1.0.27"
},
"devDependencies": {
"node-fetch": "3.0.0-beta.7"
}
}
6 changes: 6 additions & 0 deletions packages/dev-server-cli/src/config/DevServerCliConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { DevServerCoreConfig } from '@web/dev-server-core';

export interface DevServerCliConfig extends DevServerCoreConfig {
open?: 'string' | boolean;
appIndex?: string;
}
75 changes: 75 additions & 0 deletions packages/dev-server-cli/src/config/readCliArgsConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import commandLineArgs from 'command-line-args';
import commandLineUsage, { OptionDefinition } from 'command-line-usage';
import camelCase from 'camelcase';

const defaultOptions: (OptionDefinition & { description: string })[] = [
{
name: 'config',
alias: 'c',
type: String,
description: 'The file to read configuration from. Config entries are camelCases flags.',
},
{
name: 'root-dir',
alias: 'r',
type: String,
description:
'The root directory to serve files from. Defaults to the current working directory.',
},
{
name: 'open',
alias: 'o',
type: String,
description: 'Opens the browser on app-index, root dir or a custom path.',
},
{
name: 'app-index',
alias: 'a',
type: String,
description:
"The app's index.html file. When set, serves the index.html for non-file requests. Use this to enable SPA routing.",
},
{
name: 'help',
type: Boolean,
description: 'Print help commands',
},
];

export function readCliArgsConfig<T>(
extraOptions: OptionDefinition[] = [],
argv = process.argv,
): Partial<T> {
const options = [...defaultOptions, ...extraOptions];
const cliArgs = commandLineArgs(options, { argv });
// when the open flag is used without arguments, it defaults to null. treat this as "true"
if ('open' in cliArgs && typeof cliArgs.open !== 'string') {
cliArgs.open = true;
}

if ('help' in cliArgs) {
/* eslint-disable-next-line no-console */
console.log(
commandLineUsage([
{
header: 'Web Dev Server',
content: 'Dev Server for web development.',
},
{
header: 'Usage',
content: 'web-dev-server [options...]' + '\nwds [options...]',
},
{ header: 'Options', optionList: options },
]),
);
process.exit();
}

const cliArgsConfig: Partial<T> = {};

for (const [key, value] of Object.entries(cliArgs)) {
cliArgsConfig[camelCase(key) as keyof T] = value;
}

return cliArgsConfig;
}
65 changes: 65 additions & 0 deletions packages/dev-server-cli/src/config/readConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { getPortPromise } from 'portfinder';
import { readConfig as readFileConfig, ConfigLoaderError } from '@web/config-loader';
import chalk from 'chalk';
import path from 'path';
import { DevServerCliConfig } from './DevServerCliConfig';

const defaultBaseConfig: Partial<DevServerCliConfig> = {
rootDir: process.cwd(),
hostname: 'localhost',
middleware: [],
plugins: [],
};

export function validateCoreConfig<T extends DevServerCliConfig>(config: Partial<T>): T {
if (typeof config.hostname !== 'string') {
throw new Error('No hostname specified.');
}
if (typeof config.port !== 'number') {
throw new Error('No port specified.');
}
if (typeof config.rootDir !== 'string') {
throw new Error('No rootDir specified.');
}
if (
config.open != null &&
!(typeof config.open === 'string' || typeof config.open === 'boolean')
) {
throw new Error('The open option should be a boolean or string.');
}

return config as T;
}

export async function readConfig<T extends DevServerCliConfig & { config?: string }>(
cliArgsConfig: Partial<T> = {},
): Promise<Partial<T>> {
try {
const fileConfig = await readFileConfig(
'web-dev-server.config',
typeof cliArgsConfig.config === 'string' ? cliArgsConfig.config : undefined,
);
const config: Partial<T> = {
...defaultBaseConfig,
...fileConfig,
...cliArgsConfig,
};

if (typeof config.rootDir === 'string') {
config.rootDir = path.resolve(config.rootDir);
}

if (typeof config.port !== 'number') {
const port = 9000 + Math.floor(Math.random() * 1000);
config.port = await getPortPromise({ port });
}

return config;
} catch (error) {
if (error instanceof ConfigLoaderError) {
console.error(chalk.red(`\n${error.message}\n`));
process.exit(1);
}
throw error;
}
}
3 changes: 3 additions & 0 deletions packages/dev-server-cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { startDevServer } from './startDevServer';
export { readConfig, validateCoreConfig } from './config/readConfig';
export { readCliArgsConfig } from './config/readCliArgsConfig';
41 changes: 41 additions & 0 deletions packages/dev-server-cli/src/logStartMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { DevServerCliConfig } from './config/DevServerCliConfig';
import { DevServerLogger } from './logger/DevServerLogger';
import ip from 'ip';
import chalk from 'chalk';

const createAddress = (config: DevServerCliConfig, host: string, path: string) =>
`http${config.http2 ? 's' : ''}://${host}:${config.port}${path}`;

function logNetworkAddress(config: DevServerCliConfig, logger: DevServerLogger, openPath: string) {
try {
const address = ip.address();
if (typeof address === 'string') {
logger.log(
`${chalk.white('Network:')} ${chalk.cyanBright(createAddress(config, address, openPath))}`,
);
}
} catch {
//
}
}

export function logStartMessage(config: DevServerCliConfig, logger: DevServerLogger) {
const prettyHost = config.hostname ?? 'localhost';
let openPath = typeof config.open === 'string' ? config.open : '/';
if (!openPath.startsWith('/')) {
openPath = `/${openPath}`;
}

logger.log('');
logger.log(chalk.bold('Web Dev Server started...'));
logger.log('');

logger.group();
logger.log(`${chalk.white('Root dir:')} ${chalk.cyanBright(config.rootDir)}`);
logger.log(
`${chalk.white('Local:')} ${chalk.cyanBright(createAddress(config, prettyHost, openPath))}`,
);
logNetworkAddress(config, logger, openPath);
logger.groupEnd();
logger.log('');
}
45 changes: 45 additions & 0 deletions packages/dev-server-cli/src/logger/DevServerLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Logger, PluginSyntaxError } from '@web/dev-server-core';
import { codeFrameColumns } from '@babel/code-frame';
import path from 'path';
import chalk from 'chalk';

export class DevServerLogger implements Logger {
constructor(private debugLogging: boolean = false) {}

log(...messages: unknown[]) {
console.log(...messages);
}

debug(...messages: unknown[]) {
if (this.debugLogging) {
console.debug(...messages);
}
}

error(...messages: unknown[]) {
console.error(...messages);
}

warn(...messages: unknown[]) {
console.warn(...messages);
}

group() {
console.group();
}

groupEnd() {
console.groupEnd();
}

logSyntaxError(error: PluginSyntaxError) {
const { message, code, filePath, column, line } = error;
const result = codeFrameColumns(code, { start: { line, column } }, { highlightCode: true });
const relativePath = path.relative(process.cwd(), filePath);
console.error(
chalk.red(`Error while transforming ${chalk.cyanBright(relativePath)}: ${message}`),
);
console.error(result);
console.error('');
}
}
34 changes: 34 additions & 0 deletions packages/dev-server-cli/src/openBrowser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import openBrowserWindow from 'open';
import path from 'path';
import { DevServerCliConfig } from './config/DevServerCliConfig';

function isValidURL(str: string) {
try {
return !!new URL(str);
} catch (error) {
return false;
}
}

export async function openBrowser(config: DevServerCliConfig) {
let openPath: string;
if (typeof config.open === 'string') {
// user-provided open path
openPath = (config.open as string) === '' ? '/' : config.open;
} else if (config.appIndex) {
// if an appIndex was provided, use it's directory as open path
openPath = `${config.basePath ?? ''}${path.dirname(config.appIndex)}/`;
} else {
openPath = config.basePath ? `${config.basePath}/` : '/';
}

if (!isValidURL(openPath)) {
// construct a full URL to open if the user didn't provide a full URL
openPath = new URL(
openPath,
`http${config.http2 ? 's' : ''}://${config.hostname}:${config.port}`,
).href;
}

await openBrowserWindow(openPath);
}
Loading

0 comments on commit 0cc6a82

Please sign in to comment.