From 0cc6a8299b526e7b139122da8590562b1c156c97 Mon Sep 17 00:00:00 2001 From: Lars den Bakker Date: Thu, 23 Jul 2020 11:12:41 +0200 Subject: [PATCH] feat(dev-server): first implementation --- .changeset/proud-kangaroos-invite.md | 5 + .changeset/rich-buckets-prove.md | 6 ++ .changeset/tasty-crabs-relax.md | 5 + .changeset/weak-zebras-relax.md | 5 + .github/workflows/ci-postinstall.js | 2 +- package.json | 2 +- packages/browser-logs/package.json | 4 +- packages/dev-server-cli/index.d.ts | 1 + packages/dev-server-cli/index.mjs | 6 ++ packages/dev-server-cli/package.json | 51 ++++++++++ .../src/config/DevServerCliConfig.ts | 6 ++ .../src/config/readCliArgsConfig.ts | 75 +++++++++++++++ .../dev-server-cli/src/config/readConfig.ts | 65 +++++++++++++ packages/dev-server-cli/src/index.ts | 3 + .../dev-server-cli/src/logStartMessage.ts | 41 +++++++++ .../src/logger/DevServerLogger.ts | 45 +++++++++ packages/dev-server-cli/src/openBrowser.ts | 34 +++++++ packages/dev-server-cli/src/startDevServer.ts | 50 ++++++++++ .../test/fixtures/hello-world.txt | 1 + .../test/startDevServer.test.ts | 26 ++++++ packages/dev-server-cli/tsconfig.json | 28 ++++++ packages/dev-server-core/package.json | 1 - packages/dev-server-core/src/index.ts | 2 +- packages/dev-server-core/src/logger/Logger.ts | 2 + packages/dev-server-core/src/test-helpers.ts | 16 +--- .../test/resolving.test.ts | 2 + .../dev-server-rollup/src/rollupAdapter.ts | 8 +- .../dev-server-rollup/test/node/unit.test.ts | 2 + packages/dev-server/demo/base-path/config.mjs | 4 + packages/dev-server/demo/base-path/index.html | 42 +++++++++ .../dev-server/demo/base-path/module-a.js | 6 ++ .../dev-server/demo/base-path/module-b.js | 5 + .../certs/.self-signed-dev-server-ssl.cert | 16 ++++ .../certs/.self-signed-dev-server-ssl.key | 15 +++ packages/dev-server/demo/http2/config.mjs | 5 + packages/dev-server/demo/http2/index.html | 32 +++++++ packages/dev-server/demo/http2/module.js | 1 + packages/dev-server/demo/http2/server.js | 3 + .../dev-server/demo/index-rewrite/config.mjs | 11 +++ .../dev-server/demo/index-rewrite/index.html | 34 +++++++ .../dev-server/demo/index-rewrite/module-a.js | 6 ++ .../dev-server/demo/index-rewrite/module-b.js | 7 ++ packages/dev-server/demo/logo.png | Bin 0 -> 70148 bytes .../dev-server/demo/node-resolve/config.mjs | 8 ++ .../demo/node-resolve/extension-priority.js | 3 + .../demo/node-resolve/extension-priority.mjs | 3 + .../dev-server/demo/node-resolve/index.html | 41 +++++++++ .../dev-server/demo/node-resolve/module.js | 25 +++++ .../demo/node-resolve/no-extension.js | 3 + .../dev-server/demo/plugin-serve/config.mjs | 25 +++++ .../plugin-serve/virtual-files/index.html | 22 +++++ packages/dev-server/demo/static/config.mjs | 1 + packages/dev-server/demo/static/index.html | 32 +++++++ packages/dev-server/demo/static/module.js | 1 + packages/dev-server/demo/syntax/config.mjs | 2 + .../dev-server/demo/syntax/empty-module.js | 0 packages/dev-server/demo/syntax/index.html | 87 ++++++++++++++++++ .../demo/syntax/module-features-a.js | 1 + .../demo/syntax/module-features-b.js | 1 + .../dev-server/demo/syntax/module-features.js | 10 ++ .../demo/syntax/stage-3-class-fields.js | 5 + .../demo/syntax/stage-3-features.js | 6 ++ .../syntax/stage-3-private-class-fields.js | 11 +++ .../demo/syntax/stage-4-features.js | 31 +++++++ packages/dev-server/index.d.ts | 1 + packages/dev-server/index.mjs | 6 ++ packages/dev-server/package.json | 60 ++++++++++++ packages/dev-server/src/bin.ts | 4 + packages/dev-server/src/index.ts | 1 + packages/dev-server/src/nodeResolvePlugin.ts | 26 ++++++ packages/dev-server/src/startDevServer.ts | 73 +++++++++++++++ packages/dev-server/test/integration.test.mjs | 76 +++++++++++++++ packages/dev-server/tsconfig.json | 49 ++++++++++ packages/test-runner-cli/package.json | 5 +- .../test-runner-cli/src/config/readConfig.ts | 2 +- .../test-runner-cli/src/startTestRunner.ts | 38 +++++--- packages/test-runner-cli/test/fixtures/a.js | 3 + .../test/startTestRunner.test.ts | 36 ++++++++ packages/test-runner/index.mjs | 2 + packages/test-runner/package.json | 30 +++--- packages/test-runner/src/bin.ts | 4 + packages/test-runner/src/index.ts | 2 +- .../{test-runner.ts => startTestRunner.ts} | 26 ++++-- packages/tsconfig.project.json | 54 +++++++++++ scripts/update-esm-entrypoints.mjs | 5 +- tsconfig.json | 12 ++- workspace-packages.mjs | 2 + yarn.lock | 27 ++++-- 88 files changed, 1470 insertions(+), 73 deletions(-) create mode 100644 .changeset/proud-kangaroos-invite.md create mode 100644 .changeset/rich-buckets-prove.md create mode 100644 .changeset/tasty-crabs-relax.md create mode 100644 .changeset/weak-zebras-relax.md create mode 100644 packages/dev-server-cli/index.d.ts create mode 100644 packages/dev-server-cli/index.mjs create mode 100644 packages/dev-server-cli/package.json create mode 100644 packages/dev-server-cli/src/config/DevServerCliConfig.ts create mode 100644 packages/dev-server-cli/src/config/readCliArgsConfig.ts create mode 100644 packages/dev-server-cli/src/config/readConfig.ts create mode 100644 packages/dev-server-cli/src/index.ts create mode 100644 packages/dev-server-cli/src/logStartMessage.ts create mode 100644 packages/dev-server-cli/src/logger/DevServerLogger.ts create mode 100644 packages/dev-server-cli/src/openBrowser.ts create mode 100644 packages/dev-server-cli/src/startDevServer.ts create mode 100644 packages/dev-server-cli/test/fixtures/hello-world.txt create mode 100644 packages/dev-server-cli/test/startDevServer.test.ts create mode 100644 packages/dev-server-cli/tsconfig.json create mode 100644 packages/dev-server/demo/base-path/config.mjs create mode 100644 packages/dev-server/demo/base-path/index.html create mode 100644 packages/dev-server/demo/base-path/module-a.js create mode 100644 packages/dev-server/demo/base-path/module-b.js create mode 100644 packages/dev-server/demo/http2/certs/.self-signed-dev-server-ssl.cert create mode 100644 packages/dev-server/demo/http2/certs/.self-signed-dev-server-ssl.key create mode 100644 packages/dev-server/demo/http2/config.mjs create mode 100644 packages/dev-server/demo/http2/index.html create mode 100644 packages/dev-server/demo/http2/module.js create mode 100644 packages/dev-server/demo/http2/server.js create mode 100644 packages/dev-server/demo/index-rewrite/config.mjs create mode 100644 packages/dev-server/demo/index-rewrite/index.html create mode 100644 packages/dev-server/demo/index-rewrite/module-a.js create mode 100644 packages/dev-server/demo/index-rewrite/module-b.js create mode 100644 packages/dev-server/demo/logo.png create mode 100644 packages/dev-server/demo/node-resolve/config.mjs create mode 100644 packages/dev-server/demo/node-resolve/extension-priority.js create mode 100644 packages/dev-server/demo/node-resolve/extension-priority.mjs create mode 100644 packages/dev-server/demo/node-resolve/index.html create mode 100644 packages/dev-server/demo/node-resolve/module.js create mode 100644 packages/dev-server/demo/node-resolve/no-extension.js create mode 100644 packages/dev-server/demo/plugin-serve/config.mjs create mode 100644 packages/dev-server/demo/plugin-serve/virtual-files/index.html create mode 100644 packages/dev-server/demo/static/config.mjs create mode 100644 packages/dev-server/demo/static/index.html create mode 100644 packages/dev-server/demo/static/module.js create mode 100644 packages/dev-server/demo/syntax/config.mjs create mode 100644 packages/dev-server/demo/syntax/empty-module.js create mode 100644 packages/dev-server/demo/syntax/index.html create mode 100644 packages/dev-server/demo/syntax/module-features-a.js create mode 100644 packages/dev-server/demo/syntax/module-features-b.js create mode 100644 packages/dev-server/demo/syntax/module-features.js create mode 100644 packages/dev-server/demo/syntax/stage-3-class-fields.js create mode 100644 packages/dev-server/demo/syntax/stage-3-features.js create mode 100644 packages/dev-server/demo/syntax/stage-3-private-class-fields.js create mode 100644 packages/dev-server/demo/syntax/stage-4-features.js create mode 100644 packages/dev-server/index.d.ts create mode 100644 packages/dev-server/index.mjs create mode 100644 packages/dev-server/package.json create mode 100644 packages/dev-server/src/bin.ts create mode 100644 packages/dev-server/src/index.ts create mode 100644 packages/dev-server/src/nodeResolvePlugin.ts create mode 100644 packages/dev-server/src/startDevServer.ts create mode 100644 packages/dev-server/test/integration.test.mjs create mode 100644 packages/dev-server/tsconfig.json create mode 100644 packages/test-runner-cli/test/fixtures/a.js create mode 100644 packages/test-runner-cli/test/startTestRunner.test.ts create mode 100644 packages/test-runner/src/bin.ts rename packages/test-runner/src/{test-runner.ts => startTestRunner.ts} (86%) create mode 100644 packages/tsconfig.project.json diff --git a/.changeset/proud-kangaroos-invite.md b/.changeset/proud-kangaroos-invite.md new file mode 100644 index 000000000..5347c4ce1 --- /dev/null +++ b/.changeset/proud-kangaroos-invite.md @@ -0,0 +1,5 @@ +--- +'@web/dev-server-core': patch +--- + +expose ErrorWithLocation class diff --git a/.changeset/rich-buckets-prove.md b/.changeset/rich-buckets-prove.md new file mode 100644 index 000000000..9c64278eb --- /dev/null +++ b/.changeset/rich-buckets-prove.md @@ -0,0 +1,6 @@ +--- +'@web/dev-server': patch +'@web/dev-server-cli': patch +--- + +first implementation diff --git a/.changeset/tasty-crabs-relax.md b/.changeset/tasty-crabs-relax.md new file mode 100644 index 000000000..019817ae7 --- /dev/null +++ b/.changeset/tasty-crabs-relax.md @@ -0,0 +1,5 @@ +--- +'@web/test-runner-cli': patch +--- + +add test options to startTestRunner diff --git a/.changeset/weak-zebras-relax.md b/.changeset/weak-zebras-relax.md new file mode 100644 index 000000000..07035ba69 --- /dev/null +++ b/.changeset/weak-zebras-relax.md @@ -0,0 +1,5 @@ +--- +'@web/test-runner': patch +--- + +expose a startTestRunner function diff --git a/.github/workflows/ci-postinstall.js b/.github/workflows/ci-postinstall.js index 4f3614dc6..9ffffa385 100644 --- a/.github/workflows/ci-postinstall.js +++ b/.github/workflows/ci-postinstall.js @@ -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); diff --git a/package.json b/package.json index e02604d6c..71d270bee 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/browser-logs/package.json b/packages/browser-logs/package.json index 636a5b51f..e3c881bfd 100644 --- a/packages/browser-logs/package.json +++ b/packages/browser-logs/package.json @@ -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", diff --git a/packages/dev-server-cli/index.d.ts b/packages/dev-server-cli/index.d.ts new file mode 100644 index 000000000..2b8395cdb --- /dev/null +++ b/packages/dev-server-cli/index.d.ts @@ -0,0 +1 @@ +export * from './dist/index.js'; diff --git a/packages/dev-server-cli/index.mjs b/packages/dev-server-cli/index.mjs new file mode 100644 index 000000000..80ac618c4 --- /dev/null +++ b/packages/dev-server-cli/index.mjs @@ -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 }; diff --git a/packages/dev-server-cli/package.json b/packages/dev-server-cli/package.json new file mode 100644 index 000000000..a16399ee7 --- /dev/null +++ b/packages/dev-server-cli/package.json @@ -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" + } +} diff --git a/packages/dev-server-cli/src/config/DevServerCliConfig.ts b/packages/dev-server-cli/src/config/DevServerCliConfig.ts new file mode 100644 index 000000000..4a6ec502e --- /dev/null +++ b/packages/dev-server-cli/src/config/DevServerCliConfig.ts @@ -0,0 +1,6 @@ +import { DevServerCoreConfig } from '@web/dev-server-core'; + +export interface DevServerCliConfig extends DevServerCoreConfig { + open?: 'string' | boolean; + appIndex?: string; +} diff --git a/packages/dev-server-cli/src/config/readCliArgsConfig.ts b/packages/dev-server-cli/src/config/readCliArgsConfig.ts new file mode 100644 index 000000000..2334dce3c --- /dev/null +++ b/packages/dev-server-cli/src/config/readCliArgsConfig.ts @@ -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( + extraOptions: OptionDefinition[] = [], + argv = process.argv, +): Partial { + 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 = {}; + + for (const [key, value] of Object.entries(cliArgs)) { + cliArgsConfig[camelCase(key) as keyof T] = value; + } + + return cliArgsConfig; +} diff --git a/packages/dev-server-cli/src/config/readConfig.ts b/packages/dev-server-cli/src/config/readConfig.ts new file mode 100644 index 000000000..ce7e855a2 --- /dev/null +++ b/packages/dev-server-cli/src/config/readConfig.ts @@ -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 = { + rootDir: process.cwd(), + hostname: 'localhost', + middleware: [], + plugins: [], +}; + +export function validateCoreConfig(config: Partial): 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( + cliArgsConfig: Partial = {}, +): Promise> { + try { + const fileConfig = await readFileConfig( + 'web-dev-server.config', + typeof cliArgsConfig.config === 'string' ? cliArgsConfig.config : undefined, + ); + const config: Partial = { + ...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; + } +} diff --git a/packages/dev-server-cli/src/index.ts b/packages/dev-server-cli/src/index.ts new file mode 100644 index 000000000..8a0826630 --- /dev/null +++ b/packages/dev-server-cli/src/index.ts @@ -0,0 +1,3 @@ +export { startDevServer } from './startDevServer'; +export { readConfig, validateCoreConfig } from './config/readConfig'; +export { readCliArgsConfig } from './config/readCliArgsConfig'; diff --git a/packages/dev-server-cli/src/logStartMessage.ts b/packages/dev-server-cli/src/logStartMessage.ts new file mode 100644 index 000000000..7d8b37815 --- /dev/null +++ b/packages/dev-server-cli/src/logStartMessage.ts @@ -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(''); +} diff --git a/packages/dev-server-cli/src/logger/DevServerLogger.ts b/packages/dev-server-cli/src/logger/DevServerLogger.ts new file mode 100644 index 000000000..275183237 --- /dev/null +++ b/packages/dev-server-cli/src/logger/DevServerLogger.ts @@ -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(''); + } +} diff --git a/packages/dev-server-cli/src/openBrowser.ts b/packages/dev-server-cli/src/openBrowser.ts new file mode 100644 index 000000000..bcd361e4f --- /dev/null +++ b/packages/dev-server-cli/src/openBrowser.ts @@ -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); +} diff --git a/packages/dev-server-cli/src/startDevServer.ts b/packages/dev-server-cli/src/startDevServer.ts new file mode 100644 index 000000000..41d118a63 --- /dev/null +++ b/packages/dev-server-cli/src/startDevServer.ts @@ -0,0 +1,50 @@ +import { DevServer } from '@web/dev-server-core'; + +import { DevServerLogger } from './logger/DevServerLogger'; +import { DevServerCliConfig } from './config/DevServerCliConfig'; +import { openBrowser } from './openBrowser'; +import { logStartMessage as logStartMessageFunction } from './logStartMessage'; + +export interface StartDevServerOptions { + autoExitProcess?: boolean; + logStartMessage?: boolean; +} + +export async function startDevServer( + config: DevServerCliConfig, + options: StartDevServerOptions = {}, +) { + const { autoExitProcess = true, logStartMessage = true } = options; + const logger = new DevServerLogger(); + const server = new DevServer(config, logger); + + function stop() { + server.stop(); + } + + if (autoExitProcess) { + (['exit', 'SIGINT'] as NodeJS.Signals[]).forEach(event => { + process.on(event, stop); + }); + } + + if (autoExitProcess) { + process.on('uncaughtException', error => { + /* eslint-disable-next-line no-console */ + console.error(error); + stop(); + }); + } + + if (logStartMessage) { + logStartMessageFunction(config, logger); + } + + await server.start(); + + if (config.open != null) { + await openBrowser(config); + } + + return server; +} diff --git a/packages/dev-server-cli/test/fixtures/hello-world.txt b/packages/dev-server-cli/test/fixtures/hello-world.txt new file mode 100644 index 000000000..6769dd60b --- /dev/null +++ b/packages/dev-server-cli/test/fixtures/hello-world.txt @@ -0,0 +1 @@ +Hello world! \ No newline at end of file diff --git a/packages/dev-server-cli/test/startDevServer.test.ts b/packages/dev-server-cli/test/startDevServer.test.ts new file mode 100644 index 000000000..b2506b8cd --- /dev/null +++ b/packages/dev-server-cli/test/startDevServer.test.ts @@ -0,0 +1,26 @@ +import { DevServerCoreConfig } from '@web/dev-server-core'; +import { expect } from 'chai'; +import { join } from 'path'; +import fetch from 'node-fetch'; + +import { startDevServer } from '../src/startDevServer'; +import { readConfig } from '../src/config/readConfig'; + +describe('startDevServer', () => { + it('starts a functioning dev server', async () => { + const config = await readConfig({ + rootDir: join(__dirname, 'fixtures'), + }); + const devServer = await startDevServer(config as DevServerCoreConfig, { + autoExitProcess: false, + logStartMessage: false, + }); + const basePath = `http${config.http2 ? 's' : ''}://${config.hostname}:${config.port}`; + const response = await fetch(`${basePath}/hello-world.txt`); + + expect(response.status).to.equal(200); + expect(await response.text()).to.equal('Hello world!'); + + await devServer.stop(); + }); +}); diff --git a/packages/dev-server-cli/tsconfig.json b/packages/dev-server-cli/tsconfig.json new file mode 100644 index 000000000..ed9f7250e --- /dev/null +++ b/packages/dev-server-cli/tsconfig.json @@ -0,0 +1,28 @@ +// Don't edit this file directly. It is generated by /scripts/update-package-configs.ts + +{ + "extends": "../../tsconfig.node-base.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "composite": true, + "allowJs": true + }, + "references": [ + { + "path": "../config-loader/tsconfig.json" + }, + { + "path": "../dev-server-core/tsconfig.json" + } + ], + "include": [ + "src" + ], + "exclude": [ + "src/browser", + "tests", + "dist" + ] +} \ No newline at end of file diff --git a/packages/dev-server-core/package.json b/packages/dev-server-core/package.json index 2cea70d03..8afa65ea1 100644 --- a/packages/dev-server-core/package.json +++ b/packages/dev-server-core/package.json @@ -62,7 +62,6 @@ "@types/uuid": "^8.0.0", "abort-controller": "^3.0.0", "node-fetch": "3.0.0-beta.7", - "open": "^7.0.4", "portfinder": "^1.0.26", "uuid": "^8.2.0" }, diff --git a/packages/dev-server-core/src/index.ts b/packages/dev-server-core/src/index.ts index 5e098ed25..263da4923 100644 --- a/packages/dev-server-core/src/index.ts +++ b/packages/dev-server-core/src/index.ts @@ -8,10 +8,10 @@ export { getHtmlPath, isInlineScriptRequest, } from './utils'; -export { Logger } from './logger/Logger'; export { EventStreamManager } from './event-stream/EventStreamManager'; export { FSWatcher } from 'chokidar'; export { default as Koa, Context, Middleware } from 'koa'; export { Server } from 'net'; +export { Logger, ErrorWithLocation } from './logger/Logger'; export { PluginSyntaxError } from './logger/PluginSyntaxError'; export { PluginError } from './logger/PluginError'; diff --git a/packages/dev-server-core/src/logger/Logger.ts b/packages/dev-server-core/src/logger/Logger.ts index 6ee741a94..a896f399e 100644 --- a/packages/dev-server-core/src/logger/Logger.ts +++ b/packages/dev-server-core/src/logger/Logger.ts @@ -12,5 +12,7 @@ export interface Logger { debug(...messages: unknown[]): void; error(...messages: unknown[]): void; warn(...messages: unknown[]): void; + group(): void; + groupEnd(): void; logSyntaxError(error: ErrorWithLocation): void; } diff --git a/packages/dev-server-core/src/test-helpers.ts b/packages/dev-server-core/src/test-helpers.ts index af132526c..7f30e03ad 100644 --- a/packages/dev-server-core/src/test-helpers.ts +++ b/packages/dev-server-core/src/test-helpers.ts @@ -15,20 +15,12 @@ const defaultConfig: Omit = { }; const mockLogger: Logger = { - log() { - // - }, + ...console, debug() { - // - }, - error() { - // - }, - warn() { - // + // no debug }, - logSyntaxError() { - // + logSyntaxError(error) { + console.error(error); }, }; diff --git a/packages/dev-server-import-maps/test/resolving.test.ts b/packages/dev-server-import-maps/test/resolving.test.ts index 3bd7f0c2c..16093b4fa 100644 --- a/packages/dev-server-import-maps/test/resolving.test.ts +++ b/packages/dev-server-import-maps/test/resolving.test.ts @@ -339,6 +339,8 @@ describe('resolving imports', () => { debug: stub(), error: stub(), warn: stub(), + group: stub(), + groupEnd: stub(), logSyntaxError: stub(), }; const { server, host } = await createTestServer( diff --git a/packages/dev-server-rollup/src/rollupAdapter.ts b/packages/dev-server-rollup/src/rollupAdapter.ts index 6c365ca61..12b3718ab 100644 --- a/packages/dev-server-rollup/src/rollupAdapter.ts +++ b/packages/dev-server-rollup/src/rollupAdapter.ts @@ -148,11 +148,11 @@ export function rollupAdapter( if (!path.normalize(resolvedImportPath).startsWith(rootDir)) { throw new PluginError( - red(`Resolved an import to ${yellow(resolvedImportPath)}`) + - red('. This path is not reachable from the browser because') + - red(` it is outside root directory ${yellow(rootDir)}`) + + red(`Resolved an import to ${yellow(resolvedImportPath)}.\n`) + + red('This path is not reachable from the browser because') + + red(` it is outside root directory ${yellow(rootDir)}.`) + red( - `. Configure the root directory using the ${yellow('--root-dir')} or ${yellow( + ` Configure the root directory using the ${yellow('--root-dir')} or ${yellow( 'rootDir', )} option.`, ), diff --git a/packages/dev-server-rollup/test/node/unit.test.ts b/packages/dev-server-rollup/test/node/unit.test.ts index f90653971..b30e66c95 100644 --- a/packages/dev-server-rollup/test/node/unit.test.ts +++ b/packages/dev-server-rollup/test/node/unit.test.ts @@ -98,6 +98,8 @@ describe('@web/dev-server-rollup', () => { debug: stub(), error: stub(), warn: stub(), + group: stub(), + groupEnd: stub(), logSyntaxError: stub(), }; const { server, host } = await createTestServer( diff --git a/packages/dev-server/demo/base-path/config.mjs b/packages/dev-server/demo/base-path/config.mjs new file mode 100644 index 000000000..d6a233a37 --- /dev/null +++ b/packages/dev-server/demo/base-path/config.mjs @@ -0,0 +1,4 @@ +export default { + appIndex: 'demo/base-path/index.html', + basePath: '/my-base-path', +}; diff --git a/packages/dev-server/demo/base-path/index.html b/packages/dev-server/demo/base-path/index.html new file mode 100644 index 000000000..c62c96be7 --- /dev/null +++ b/packages/dev-server/demo/base-path/index.html @@ -0,0 +1,42 @@ + + + + + + + + +

Demo app

+ +

+ Foo +

+ +

+ Bar +

+ +
+
+
+ + + + + + diff --git a/packages/dev-server/demo/base-path/module-a.js b/packages/dev-server/demo/base-path/module-a.js new file mode 100644 index 000000000..d8ce4affb --- /dev/null +++ b/packages/dev-server/demo/base-path/module-a.js @@ -0,0 +1,6 @@ +/* eslint-disable */ +import { foo } from './module-b.js'; + +console.log('module a'); +console.log(foo()); +window.__moduleALoaded = true; diff --git a/packages/dev-server/demo/base-path/module-b.js b/packages/dev-server/demo/base-path/module-b.js new file mode 100644 index 000000000..d9dfc4cbd --- /dev/null +++ b/packages/dev-server/demo/base-path/module-b.js @@ -0,0 +1,5 @@ +/* eslint-disable */ +export const foo = () => 'module b foo'; + +console.log('module b'); +window.__moduleBLoaded = true; diff --git a/packages/dev-server/demo/http2/certs/.self-signed-dev-server-ssl.cert b/packages/dev-server/demo/http2/certs/.self-signed-dev-server-ssl.cert new file mode 100644 index 000000000..47b660afc --- /dev/null +++ b/packages/dev-server/demo/http2/certs/.self-signed-dev-server-ssl.cert @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIIClTCCAf6gAwIBAgIJRhJ5Yf7uQ2ZGMA0GCSqGSIb3DQEBBQUAMGkxFDASBgNV +BAMTC2V4YW1wbGUub3JnMQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWEx +EzARBgNVBAcTCkJsYWNrc2J1cmcxDTALBgNVBAoTBFRlc3QxDTALBgNVBAsTBFRl +c3QwHhcNMTkwNjI1MTcwMzExWhcNMjAwNjI0MTcwMzExWjBpMRQwEgYDVQQDEwtl +eGFtcGxlLm9yZzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYD +VQQHEwpCbGFja3NidXJnMQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQLEwRUZXN0MIGf +MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOeTymomQwK//9598FeiNppQC+n+2S +Fa66YtzX2PQgXuVGVJvSinDsbo6ZY6CzzxiWSomauC281ZXC7S6sGwplUKvGlZkG +fG+WiZO37PCkatb7VQGQ77gL0iJSZJnq2lW/o4gJ4y3331VIevdx4bcFLR6fZZYO +wAUMVO1l5bMZUQIDAQABo0UwQzAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIC9DAm +BgNVHREEHzAdhhtodHRwOi8vZXhhbXBsZS5vcmcvd2ViaWQjbWUwDQYJKoZIhvcN +AQEFBQADgYEAZX7IaqDmpzIvMGx+1B+304QWAxi4+fO6oggskrCOoXL1BcoxXox4 +7cNxYGYsniteuTt/u9OYxmX2J/gWaRVo5dlTM5sBoQUBpnvrowDCRpnQNPJpaj+S +s86T0GqVx3Mn/fzNXBBHGBgWmU8OYfwDQDS7tE5c5OHtXNeNLOAA3PE= +-----END CERTIFICATE----- diff --git a/packages/dev-server/demo/http2/certs/.self-signed-dev-server-ssl.key b/packages/dev-server/demo/http2/certs/.self-signed-dev-server-ssl.key new file mode 100644 index 000000000..8d4977c96 --- /dev/null +++ b/packages/dev-server/demo/http2/certs/.self-signed-dev-server-ssl.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDOeTymomQwK//9598FeiNppQC+n+2SFa66YtzX2PQgXuVGVJvS +inDsbo6ZY6CzzxiWSomauC281ZXC7S6sGwplUKvGlZkGfG+WiZO37PCkatb7VQGQ +77gL0iJSZJnq2lW/o4gJ4y3331VIevdx4bcFLR6fZZYOwAUMVO1l5bMZUQIDAQAB +AoGBAMESR2++nJcbHebswvSZMLIiNF8Mp5eaJOhvd/lzC12VvymUBp3LDStZeoje +y7A4MNKg4qnGHibdRoyfQ6x2ILHMVfAhA5mD96iI9EIxOlcrvg+j2E0dDi0ps2BV +LiUinue1sVkrz+4VgFnt9oUBXYXSUJWhvm0qEI1JLQIdUCYBAkEA8SYdz8Br4fHp +EJOGICfHyXHpsscQzsX41tmhombLN+1faXXGURhsjE+KGqV+KbSLLhcYHP2uL5e3 +5wDTJ84OcQJBANswcIeBLaXPkF7o8m+den3oRf0calvsnQsQTfhy5uLeO+gS9k5N +XL/T7EJENn+M+s0QAjiqsfp+wITsXxmi6OECQQCm+dCcoM1SpxHNY4j0zPa+mrzK +CKKvuk8iXZFZBTpjoF53hJBsaLAIu66R4tOoBxZ0NJOCx3kMBU7WijCrmDdRAkBa +k3fM5xq/7DgSoFyetwrJQNS7NaTV+78htUkjJAg5M/C5hCx4SwvK0X5OG/YRW2bA +mp7wX8lQZFSWGT9rTqDBAkA3wsS5Sjkbvq/u0BQBItUfHDjM2ehjv3cAiyNIjlsF +pbwMFaAHP0ev2NjCtsd6TaeFeZ3nclf31U9Klfl45GmH +-----END RSA PRIVATE KEY----- diff --git a/packages/dev-server/demo/http2/config.mjs b/packages/dev-server/demo/http2/config.mjs new file mode 100644 index 000000000..d1fc18648 --- /dev/null +++ b/packages/dev-server/demo/http2/config.mjs @@ -0,0 +1,5 @@ +export default { + http2: true, + sslKey: './demo/http2/certs/.self-signed-dev-server-ssl.key', + sslCert: './demo/http2/certs/.self-signed-dev-server-ssl.cert', +}; diff --git a/packages/dev-server/demo/http2/index.html b/packages/dev-server/demo/http2/index.html new file mode 100644 index 000000000..2fd7a6513 --- /dev/null +++ b/packages/dev-server/demo/http2/index.html @@ -0,0 +1,32 @@ + + + + + + +

Static demo

+

A demo using https.

+
+ + + + + + + + diff --git a/packages/dev-server/demo/http2/module.js b/packages/dev-server/demo/http2/module.js new file mode 100644 index 000000000..e1b955c51 --- /dev/null +++ b/packages/dev-server/demo/http2/module.js @@ -0,0 +1 @@ +window.__moduleLoaded = true; \ No newline at end of file diff --git a/packages/dev-server/demo/http2/server.js b/packages/dev-server/demo/http2/server.js new file mode 100644 index 000000000..b386b8292 --- /dev/null +++ b/packages/dev-server/demo/http2/server.js @@ -0,0 +1,3 @@ +module.exports = { + http2: true, +}; diff --git a/packages/dev-server/demo/index-rewrite/config.mjs b/packages/dev-server/demo/index-rewrite/config.mjs new file mode 100644 index 000000000..fd8cec75d --- /dev/null +++ b/packages/dev-server/demo/index-rewrite/config.mjs @@ -0,0 +1,11 @@ +export default { + middleware: [ + function rewriteIndex(ctx, next) { + if (ctx.url === '/' || ctx.url === '/index.html') { + ctx.url = '/demo/index-rewrite/index.html'; + } + + return next(); + }, + ], +}; diff --git a/packages/dev-server/demo/index-rewrite/index.html b/packages/dev-server/demo/index-rewrite/index.html new file mode 100644 index 000000000..47ad0be35 --- /dev/null +++ b/packages/dev-server/demo/index-rewrite/index.html @@ -0,0 +1,34 @@ + + + + + + + + +

Demo app

+ +

+ Foo +

+ +

+ Bar +

+ +
+ + + + diff --git a/packages/dev-server/demo/index-rewrite/module-a.js b/packages/dev-server/demo/index-rewrite/module-a.js new file mode 100644 index 000000000..d8ce4affb --- /dev/null +++ b/packages/dev-server/demo/index-rewrite/module-a.js @@ -0,0 +1,6 @@ +/* eslint-disable */ +import { foo } from './module-b.js'; + +console.log('module a'); +console.log(foo()); +window.__moduleALoaded = true; diff --git a/packages/dev-server/demo/index-rewrite/module-b.js b/packages/dev-server/demo/index-rewrite/module-b.js new file mode 100644 index 000000000..87afa125c --- /dev/null +++ b/packages/dev-server/demo/index-rewrite/module-b.js @@ -0,0 +1,7 @@ +/* eslint-disable */ + +export const foo = () => 'module b foo'; + +console.log('module b'); + +window.__moduleBLoaded = true; diff --git a/packages/dev-server/demo/logo.png b/packages/dev-server/demo/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c9de9c4e5453ff8a33d4c1d06e9ae7bc843c2733 GIT binary patch literal 70148 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 z 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 uAkeiC^-!4lz_3H)jCo82SSt0%@;Qs(FK<-cg literal 0 HcmV?d00001 diff --git a/packages/dev-server/demo/node-resolve/config.mjs b/packages/dev-server/demo/node-resolve/config.mjs new file mode 100644 index 000000000..f5b04adc5 --- /dev/null +++ b/packages/dev-server/demo/node-resolve/config.mjs @@ -0,0 +1,8 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +export default { + rootDir: fileURLToPath(path.join(import.meta.url, '../../../../..')), + appIndex: 'packages/dev-server/demo/node-resolve/index.html', + nodeResolve: true, +}; diff --git a/packages/dev-server/demo/node-resolve/extension-priority.js b/packages/dev-server/demo/node-resolve/extension-priority.js new file mode 100644 index 000000000..be1eae85b --- /dev/null +++ b/packages/dev-server/demo/node-resolve/extension-priority.js @@ -0,0 +1,3 @@ +import { html, render } from 'lit-html'; + +window.__extensionPriority = false; diff --git a/packages/dev-server/demo/node-resolve/extension-priority.mjs b/packages/dev-server/demo/node-resolve/extension-priority.mjs new file mode 100644 index 000000000..1e689b4f1 --- /dev/null +++ b/packages/dev-server/demo/node-resolve/extension-priority.mjs @@ -0,0 +1,3 @@ +import { html, render } from 'lit-html'; + +window.__extensionPriority = true; \ No newline at end of file diff --git a/packages/dev-server/demo/node-resolve/index.html b/packages/dev-server/demo/node-resolve/index.html new file mode 100644 index 000000000..83ceb086a --- /dev/null +++ b/packages/dev-server/demo/node-resolve/index.html @@ -0,0 +1,41 @@ + + + + + + + + +

Node resolve demo

+

A demo which resolves bare module imports

+ +
+ + + + + + diff --git a/packages/dev-server/demo/node-resolve/module.js b/packages/dev-server/demo/node-resolve/module.js new file mode 100644 index 000000000..8ce074c4a --- /dev/null +++ b/packages/dev-server/demo/node-resolve/module.js @@ -0,0 +1,25 @@ +import { html, render } from 'lit-html'; +import { LitElement } from 'lit-element'; + +class MyElement extends LitElement { + connectedCallback() { + super.connectedCallback(); + + render( + html` +

Web component instantiated ✓

+ `, + document.getElementById('web-component'), + ); + } + + render() { + return html` +

Element Shadow DOM content ✓

+ `; + } +} + +customElements.define('my-element', MyElement); + +window.__nodeResolve = !!html && !!render && !!LitElement; \ No newline at end of file diff --git a/packages/dev-server/demo/node-resolve/no-extension.js b/packages/dev-server/demo/node-resolve/no-extension.js new file mode 100644 index 000000000..c45cecc33 --- /dev/null +++ b/packages/dev-server/demo/node-resolve/no-extension.js @@ -0,0 +1,3 @@ +import { html, render } from 'lit-html'; + +window.__noExtension = !!html && !!render; diff --git a/packages/dev-server/demo/plugin-serve/config.mjs b/packages/dev-server/demo/plugin-serve/config.mjs new file mode 100644 index 000000000..da9e1f81b --- /dev/null +++ b/packages/dev-server/demo/plugin-serve/config.mjs @@ -0,0 +1,25 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const indexPath = path.resolve(__dirname, 'virtual-files', 'index.html'); + +function createServeHtmlPlugin() { + return { + serverStart({ fileWatcher }) { + fileWatcher.add(indexPath); + }, + + serve(context) { + if (['/', '/index.html'].includes(context.path)) { + return { body: fs.readFileSync(indexPath, 'utf-8'), type: 'html' }; + } + }, + }; +} + +export default { + plugins: [createServeHtmlPlugin()], +}; diff --git a/packages/dev-server/demo/plugin-serve/virtual-files/index.html b/packages/dev-server/demo/plugin-serve/virtual-files/index.html new file mode 100644 index 000000000..22cd43d3c --- /dev/null +++ b/packages/dev-server/demo/plugin-serve/virtual-files/index.html @@ -0,0 +1,22 @@ + + + + +

Plugin serve demo

+ +
+ + + + diff --git a/packages/dev-server/demo/static/config.mjs b/packages/dev-server/demo/static/config.mjs new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/dev-server/demo/static/config.mjs @@ -0,0 +1 @@ +export default {}; diff --git a/packages/dev-server/demo/static/index.html b/packages/dev-server/demo/static/index.html new file mode 100644 index 000000000..4fcbb3022 --- /dev/null +++ b/packages/dev-server/demo/static/index.html @@ -0,0 +1,32 @@ + + + + + + +

Static demo

+

A demo without any custom flags.

+
+ + + + + + + + diff --git a/packages/dev-server/demo/static/module.js b/packages/dev-server/demo/static/module.js new file mode 100644 index 000000000..e1b955c51 --- /dev/null +++ b/packages/dev-server/demo/static/module.js @@ -0,0 +1 @@ +window.__moduleLoaded = true; \ No newline at end of file diff --git a/packages/dev-server/demo/syntax/config.mjs b/packages/dev-server/demo/syntax/config.mjs new file mode 100644 index 000000000..6ccd98e28 --- /dev/null +++ b/packages/dev-server/demo/syntax/config.mjs @@ -0,0 +1,2 @@ +// empty config so that integration test can assume a config is present +export default {}; diff --git a/packages/dev-server/demo/syntax/empty-module.js b/packages/dev-server/demo/syntax/empty-module.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/dev-server/demo/syntax/index.html b/packages/dev-server/demo/syntax/index.html new file mode 100644 index 000000000..5d54a919b --- /dev/null +++ b/packages/dev-server/demo/syntax/index.html @@ -0,0 +1,87 @@ + + + + +

Syntax demo

+

A demo which showcases different types of syntax being handled by es-dev-server

+ +
+ + + + + + diff --git a/packages/dev-server/demo/syntax/module-features-a.js b/packages/dev-server/demo/syntax/module-features-a.js new file mode 100644 index 000000000..d1d0f07c2 --- /dev/null +++ b/packages/dev-server/demo/syntax/module-features-a.js @@ -0,0 +1 @@ +export default 'moduleFeaturesA'; diff --git a/packages/dev-server/demo/syntax/module-features-b.js b/packages/dev-server/demo/syntax/module-features-b.js new file mode 100644 index 000000000..55b6e6747 --- /dev/null +++ b/packages/dev-server/demo/syntax/module-features-b.js @@ -0,0 +1 @@ +export default 'moduleFeaturesB'; diff --git a/packages/dev-server/demo/syntax/module-features.js b/packages/dev-server/demo/syntax/module-features.js new file mode 100644 index 000000000..1244bdcb4 --- /dev/null +++ b/packages/dev-server/demo/syntax/module-features.js @@ -0,0 +1,10 @@ +import module from './module-features-a.js'; + +const featuresB = 'features-b'; + +window.__importMeta = import.meta.url.indexOf('syntax/module-features.js') > 0; +window.__staticImports = module === 'moduleFeaturesA'; +window.__dynamicImports = (async () => + (await import('./module-features-b.js')).default === 'moduleFeaturesB')(); +window.__dynamicImportsString = (async () => + (await import(`./module-${featuresB}.js`)).default === 'moduleFeaturesB')(); diff --git a/packages/dev-server/demo/syntax/stage-3-class-fields.js b/packages/dev-server/demo/syntax/stage-3-class-fields.js new file mode 100644 index 000000000..97f7ebbac --- /dev/null +++ b/packages/dev-server/demo/syntax/stage-3-class-fields.js @@ -0,0 +1,5 @@ +class ClassFields { + myField = 'foo'; +} + +console.log(new ClassFields().myField); diff --git a/packages/dev-server/demo/syntax/stage-3-features.js b/packages/dev-server/demo/syntax/stage-3-features.js new file mode 100644 index 000000000..abbc27e12 --- /dev/null +++ b/packages/dev-server/demo/syntax/stage-3-features.js @@ -0,0 +1,6 @@ +console.log( + 'dynamically importing stage 3 features, these should throw errors on browsers that dont support them but it should not crash babel parsing', +); + +import('./stage-3-class-fields.js'); +import('./stage-3-private-class-fields.js'); diff --git a/packages/dev-server/demo/syntax/stage-3-private-class-fields.js b/packages/dev-server/demo/syntax/stage-3-private-class-fields.js new file mode 100644 index 000000000..ec35a1abd --- /dev/null +++ b/packages/dev-server/demo/syntax/stage-3-private-class-fields.js @@ -0,0 +1,11 @@ +class PrivateClassFields { + #foo = 'bar'; + + #bar() { + return this.#foo; + } + + bar() { + return this.#bar(); + } +} diff --git a/packages/dev-server/demo/syntax/stage-4-features.js b/packages/dev-server/demo/syntax/stage-4-features.js new file mode 100644 index 000000000..0c3b8b1bb --- /dev/null +++ b/packages/dev-server/demo/syntax/stage-4-features.js @@ -0,0 +1,31 @@ +const foo = { a: 1 }; +const bar = { ...foo }; +const objectSpread = bar.a === 1; + +async function asyncFunction() {} +const asyncFunctions = asyncFunction() instanceof Promise; + +const exponentation = 2 ** 4 === 16; + +class Foo { + constructor() { + this.foo = 'bar'; + } +} +const classes = new Foo().foo === 'bar'; + +const templateLiterals = `template ${'literal'}` === 'template literal'; + +const lorem = { ipsum: 'lorem ipsum' }; +const optionalChaining = lorem?.ipsum === 'lorem ipsum' && lorem?.ipsum?.foo === undefined; +const buz = null; +const nullishCoalescing = (buz ?? 'nullish colaesced') === 'nullish colaesced'; + +window.__stage4 = + objectSpread && + asyncFunctions && + exponentation && + classes && + templateLiterals && + optionalChaining && + nullishCoalescing; diff --git a/packages/dev-server/index.d.ts b/packages/dev-server/index.d.ts new file mode 100644 index 000000000..2b8395cdb --- /dev/null +++ b/packages/dev-server/index.d.ts @@ -0,0 +1 @@ +export * from './dist/index.js'; diff --git a/packages/dev-server/index.mjs b/packages/dev-server/index.mjs new file mode 100644 index 000000000..728494e59 --- /dev/null +++ b/packages/dev-server/index.mjs @@ -0,0 +1,6 @@ +// this file is autogenerated with the update-esm-entrypoints script +import cjsEntrypoint from './dist/index.js'; + +const { startDevServer } = cjsEntrypoint; + +export { startDevServer }; diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json new file mode 100644 index 000000000..7e544d440 --- /dev/null +++ b/packages/dev-server/package.json @@ -0,0 +1,60 @@ +{ + "name": "@web/dev-server", + "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" + }, + "author": "modern-web", + "homepage": "https://github.com/modernweb-dev/web/tree/master/packages/dev-server", + "main": "dist/index.js", + "bin": { + "web-dev-server": "./dist/bin.js", + "wds": "./dist/bin.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "scripts": { + "start": "yarn start:syntax", + "start:base-path": "node dist/dev-server.js --config demo/base-path/config.mjs --open", + "start:http2": "node dist/dev-server.js --config demo/http2/config.mjs --open demo/http2/", + "start:index-rewrite": "node dist/dev-server.js --config demo/index-rewrite/config.mjs --open", + "start:node-resolve": "node dist/dev-server.js --config demo/node-resolve/config.mjs --open", + "start:plugin-serve": "node dist/dev-server.js --config demo/plugin-serve/config.mjs --open", + "start:static": "node dist/dev-server.js --config demo/static/config.mjs --open demo/static/", + "start:syntax": "node dist/dev-server.js --config demo/syntax/config.mjs --open demo/syntax/", + "test": "mocha \"test/**/*.test.mjs\" --reporter dot", + "test:watch": "mocha \"test/**/*.test.mjs\" --watch --watch-files src,test --reporter dot" + }, + "files": [ + "dist" + ], + "keywords": [ + "web", + "dev", + "server", + "default", + "implementation", + "cli" + ], + "dependencies": { + "@rollup/plugin-node-resolve": "^8.4.0", + "@types/command-line-args": "^5.0.0", + "@web/dev-server-cli": "^0.0.0", + "@web/dev-server-rollup": "^0.2.3", + "chalk": "^4.1.0", + "command-line-args": "^5.1.1", + "deepmerge": "^4.2.2" + }, + "devDependencies": { + "lit-html": "^1.2.1", + "puppeteer": "^5.2.1" + } +} diff --git a/packages/dev-server/src/bin.ts b/packages/dev-server/src/bin.ts new file mode 100644 index 000000000..b7619d63f --- /dev/null +++ b/packages/dev-server/src/bin.ts @@ -0,0 +1,4 @@ +#!/usr/bin/env node +import { startDevServer } from './startDevServer'; + +startDevServer(); diff --git a/packages/dev-server/src/index.ts b/packages/dev-server/src/index.ts new file mode 100644 index 000000000..f080e9e67 --- /dev/null +++ b/packages/dev-server/src/index.ts @@ -0,0 +1 @@ +export { DevServerConfig, startDevServer } from './startDevServer'; diff --git a/packages/dev-server/src/nodeResolvePlugin.ts b/packages/dev-server/src/nodeResolvePlugin.ts new file mode 100644 index 000000000..daa8e1cc6 --- /dev/null +++ b/packages/dev-server/src/nodeResolvePlugin.ts @@ -0,0 +1,26 @@ +import { rollupAdapter } from '@web/dev-server-rollup'; +import { Plugin } from '@web/dev-server-core'; +import { nodeResolve, RollupNodeResolveOptions } from '@rollup/plugin-node-resolve'; +import deepmerge from 'deepmerge'; + +export function nodeResolvePlugin( + rootDir: string, + preserveSymlinks?: boolean, + userOptions?: RollupNodeResolveOptions, +): Plugin { + const userOptionsObject = typeof userOptions === 'object' ? userOptions : {}; + const options: RollupNodeResolveOptions = deepmerge( + { + rootDir, + extensions: ['.mjs', '.js', '.cjs', '.jsx', '.json', '.ts', '.tsx'], + customResolveOptions: { + moduleDirectory: ['node_modules', 'web_modules'], + }, + // allow resolving polyfills for nodejs libs + preferBuiltins: false, + }, + userOptionsObject, + ); + + return rollupAdapter(nodeResolve(options), { preserveSymlinks }); +} diff --git a/packages/dev-server/src/startDevServer.ts b/packages/dev-server/src/startDevServer.ts new file mode 100644 index 000000000..75c176ceb --- /dev/null +++ b/packages/dev-server/src/startDevServer.ts @@ -0,0 +1,73 @@ +import { + readCliArgsConfig, + readConfig, + startDevServer as originalStartDevServer, + validateCoreConfig, +} from '@web/dev-server-cli'; +import { RollupNodeResolveOptions } from '@rollup/plugin-node-resolve'; +import commandLineArgs from 'command-line-args'; +import chalk from 'chalk'; + +import { nodeResolvePlugin } from './nodeResolvePlugin'; +import { DevServerCliConfig } from '@web/dev-server-cli/dist/config/DevServerCliConfig'; + +export interface DevServerConfig extends DevServerCliConfig { + nodeResolve?: boolean | RollupNodeResolveOptions; + preserveSymlinks?: boolean; +} + +export interface DevServerCliArgsConfig extends DevServerConfig { + // CLI-only options go here +} + +export interface StartDevServerOptions { + autoExitProcess?: boolean; + logStartMessage?: boolean; + argv?: string[]; +} + +const cliOptions: commandLineArgs.OptionDefinition[] = [ + { + name: 'preserve-symlinks', + type: Boolean, + }, + { + name: 'node-resolve', + type: Boolean, + }, + { + name: 'debug', + type: Boolean, + }, +]; + +export async function startDevServer(options: StartDevServerOptions = {}) { + const { autoExitProcess = true, argv = process.argv, logStartMessage = true } = options; + + try { + const cliArgs = readCliArgsConfig(cliOptions, argv); + const cliArgsConfig: Partial = {}; + for (const [key, value] of Object.entries(cliArgs)) { + // cli args are read from a file, they are validated by cli-options and later on as well + (cliArgsConfig as any)[key] = value; + } + + const config = await readConfig(cliArgsConfig); + const { rootDir } = config; + + if (typeof rootDir !== 'string') { + throw new Error('No rootDir specified.'); + } + + if (config.nodeResolve) { + const userOptions = typeof config.nodeResolve === 'object' ? config.nodeResolve : undefined; + config.plugins!.push(nodeResolvePlugin(rootDir, config.preserveSymlinks, userOptions)); + } + + const validatedConfig = validateCoreConfig(config); + return originalStartDevServer(validatedConfig, { autoExitProcess, logStartMessage }); + } catch (error) { + console.error(chalk.red(`\nFailed to start dev server: ${error.message}\n`)); + process.exit(1); + } +} diff --git a/packages/dev-server/test/integration.test.mjs b/packages/dev-server/test/integration.test.mjs new file mode 100644 index 000000000..1a79a76b8 --- /dev/null +++ b/packages/dev-server/test/integration.test.mjs @@ -0,0 +1,76 @@ +import puppeteer from 'puppeteer'; +import { startDevServer } from '../index.mjs'; + +const testCases = [ + { + name: 'base-path', + tests: ['moduleLoaded'], + }, + { + name: 'index-rewrite', + tests: ['moduleLoaded'], + }, + { + name: 'node-resolve', + tests: ['inlineNodeResolve', 'nodeResolve', 'noExtension', 'extensionPriority'], + }, + { + name: 'static', + tests: ['moduleLoaded'], + }, + { + name: 'syntax', + tests: ['stage4', 'inlineStage4', 'importMeta', 'staticImports', 'dynamicImports'], + }, +]; + +describe('integration tests', () => { + let browser; + + before(async () => { + browser = await puppeteer.launch({ + // devtools: true, + }); + }); + + after(() => { + browser.close(); + }); + + for (const testCase of testCases) { + describe(`testcase ${testCase.name}`, function test() { + this.timeout(30000); + let server; + + beforeEach(async () => { + server = await startDevServer({ + autoExitProcess: false, + logStartMessage: false, + argv: ['--config', `demo/${testCase.name}/config.mjs`], + }); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('passes the in-browser tests', async function it() { + const openPath = server.config.appIndex ?? `demo/${testCase.name}/`; + const browserPath = `http://${server.config.hostname}:${server.config.port}/${openPath}`; + const page = await browser.newPage(); + await page.goto(browserPath, { + waitUntil: 'networkidle2', + }); + + const browserTests = await page.evaluate(() => window.__tests); + + // the demos run "tests", which we check here + for (const test of testCase.tests) { + if (!browserTests[test]) { + throw new Error(`Expected test ${test} to have passed in the browser.`); + } + } + }); + }); + } +}); diff --git a/packages/dev-server/tsconfig.json b/packages/dev-server/tsconfig.json new file mode 100644 index 000000000..5ae369a86 --- /dev/null +++ b/packages/dev-server/tsconfig.json @@ -0,0 +1,49 @@ +// Don't edit this file directly. It is generated by /scripts/update-package-configs.ts + +{ + "extends": "../../tsconfig.node-base.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "composite": true, + "allowJs": true + }, + "references": [ + { + "path": "../config-loader/tsconfig.json" + }, + { + "path": "../dev-server-core/tsconfig.json" + }, + { + "path": "../test-runner-core/tsconfig.json" + }, + { + "path": "../browser-logs/tsconfig.json" + }, + { + "path": "../test-runner-coverage-v8/tsconfig.json" + }, + { + "path": "../test-runner-mocha/tsconfig.json" + }, + { + "path": "../test-runner-chrome/tsconfig.json" + }, + { + "path": "../dev-server-cli/tsconfig.json" + }, + { + "path": "../dev-server-rollup/tsconfig.json" + } + ], + "include": [ + "src" + ], + "exclude": [ + "src/browser", + "tests", + "dist" + ] +} \ No newline at end of file diff --git a/packages/test-runner-cli/package.json b/packages/test-runner-cli/package.json index 7d5471d08..7edc56f8d 100644 --- a/packages/test-runner-cli/package.json +++ b/packages/test-runner-cli/package.json @@ -19,8 +19,8 @@ }, "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" + "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": [ "*.d.ts", @@ -43,6 +43,7 @@ "@types/babel__code-frame": "^7.0.1", "@web/browser-logs": "^0.1.2", "@web/config-loader": "^0.1.1", + "@web/test-runner-chrome": "^0.6.4", "@web/test-runner-core": "^0.7.4", "camelcase": "^6.0.0", "chalk": "^4.1.0", diff --git a/packages/test-runner-cli/src/config/readConfig.ts b/packages/test-runner-cli/src/config/readConfig.ts index 1d440cde7..b9b479dca 100644 --- a/packages/test-runner-cli/src/config/readConfig.ts +++ b/packages/test-runner-cli/src/config/readConfig.ts @@ -53,7 +53,7 @@ export function validateCoreConfig(config: Parti } export async function readConfig( - cliArgsConfig: Partial, + cliArgsConfig: Partial = {}, ): Promise> { try { const fileConfig = await readFileConfig( diff --git a/packages/test-runner-cli/src/startTestRunner.ts b/packages/test-runner-cli/src/startTestRunner.ts index 3810b2956..5ab202ed4 100644 --- a/packages/test-runner-cli/src/startTestRunner.ts +++ b/packages/test-runner-cli/src/startTestRunner.ts @@ -1,8 +1,17 @@ +/* eslint-disable no-async-promise-executor */ import { TestRunner, TestRunnerCoreConfig } from '@web/test-runner-core'; import { TestRunnerCli } from './cli/TestRunnerCli'; import { collectTestFiles } from './config/collectTestFiles'; -export async function startTestRunner(config: TestRunnerCoreConfig) { +export interface StartTestRunnerOptions { + autoExitProcess?: boolean; +} + +export async function startTestRunner( + config: TestRunnerCoreConfig, + options: StartTestRunnerOptions = {}, +) { + const { autoExitProcess = true } = options; const testFiles = await collectTestFiles( Array.isArray(config.files) ? config.files : [config.files], ); @@ -18,20 +27,27 @@ export async function startTestRunner(config: TestRunnerCoreConfig) { runner.stop(); } - (['exit', 'SIGINT'] as NodeJS.Signals[]).forEach(event => { - process.on(event, stop); - }); + if (autoExitProcess) { + (['exit', 'SIGINT'] as NodeJS.Signals[]).forEach(event => { + process.on(event, stop); + }); + } - process.on('uncaughtException', error => { - /* eslint-disable-next-line no-console */ - console.error(error); - stop(); - }); + if (autoExitProcess) { + process.on('uncaughtException', error => { + /* eslint-disable-next-line no-console */ + console.error(error); + stop(); + }); + } runner.on('stopped', passed => { - process.exit(passed ? 0 : 1); + if (autoExitProcess) { + process.exit(passed ? 0 : 1); + } }); await runner.start(); - await cli.start(); + cli.start(); + return runner; } diff --git a/packages/test-runner-cli/test/fixtures/a.js b/packages/test-runner-cli/test/fixtures/a.js new file mode 100644 index 000000000..dcf540f16 --- /dev/null +++ b/packages/test-runner-cli/test/fixtures/a.js @@ -0,0 +1,3 @@ +describe('my test', () => { + it('works', () => {}); +}); diff --git a/packages/test-runner-cli/test/startTestRunner.test.ts b/packages/test-runner-cli/test/startTestRunner.test.ts new file mode 100644 index 000000000..4fd2876f0 --- /dev/null +++ b/packages/test-runner-cli/test/startTestRunner.test.ts @@ -0,0 +1,36 @@ +import { TestRunnerCoreConfig } from '@web/test-runner-core'; +import { chromeLauncher } from '@web/test-runner-chrome'; +import { resolve } from 'path'; +import { startTestRunner } from '../src/startTestRunner'; +import { readConfig } from '../src/config/readConfig'; + +describe('startTestRunner', () => { + it('starts the test runner', async () => { + let resolveTest: () => void; + const config = await readConfig(); + const testRunner = await startTestRunner( + { + ...config, + files: [resolve(__dirname, 'fixtures', 'a.js')], + testFramework: { + path: require.resolve('@web/test-runner-mocha/dist/autorun.js'), + }, + reporters: [], + browsers: [chromeLauncher()], + concurrency: 3, + } as TestRunnerCoreConfig, + { autoExitProcess: false }, + ); + + testRunner.on('stopped', passed => { + if (!passed) { + throw new Error('Tests run did not pas'); + } + resolveTest(); + }); + + return new Promise(resolve => { + resolveTest = resolve; + }); + }); +}); diff --git a/packages/test-runner/index.mjs b/packages/test-runner/index.mjs index 29de55aab..8b746f032 100644 --- a/packages/test-runner/index.mjs +++ b/packages/test-runner/index.mjs @@ -4,6 +4,7 @@ import cjsEntrypoint from './dist/index.js'; const { chromeLauncher, defaultReporter, + startTestRunner, constants, TestRunner, TestSessionManager, @@ -14,6 +15,7 @@ const { export { chromeLauncher, defaultReporter, + startTestRunner, constants, TestRunner, TestSessionManager, diff --git a/packages/test-runner/package.json b/packages/test-runner/package.json index 3d36cbdd0..1a552a6a4 100644 --- a/packages/test-runner/package.json +++ b/packages/test-runner/package.json @@ -15,28 +15,28 @@ "homepage": "https://github.com/modernweb-dev/web/tree/master/packages/test-runner", "main": "dist/index.js", "bin": { - "web-test-runner": "./dist/test-runner.js", - "wtr": "./dist/test-runner.js" + "web-test-runner": "./dist/bin.js", + "wtr": "./dist/bin.js" }, "engines": { "node": ">=10.0.0" }, "scripts": { "build": "tsc", - "test": "node dist/test-runner.js \"demo/test/pass-*.test.{js,html}\"", - "test:babel-coverage": "node dist/test-runner.js \"demo/test/pass-*.test.{js,html}\" --config demo/babel-coverage.config.js", - "test:bare": "node dist/test-runner.js", + "test": "node dist/bin.js \"demo/test/pass-*.test.{js,html}\"", + "test:babel-coverage": "node dist/bin.js \"demo/test/pass-*.test.{js,html}\" --config demo/babel-coverage.config.js", + "test:bare": "node dist/bin.js", "test:ci": "yarn test", - "test:custom-html": "node dist/test-runner.js \"demo/test/pass-*.test.{js,html}\" --config demo/customhtml.config.js", - "test:legacy": "node dist/test-runner.js \"demo/test/pass-*.test.{js,html}\" --config legacy.config.js", - "test:logging": "node dist/test-runner.js \"demo/test/logging.test.js\"", - "test:many": "node dist/test-runner.js \"demo/test/many/**/*.test.js\"", - "test:mixed": "node dist/test-runner.js \"demo/**/*.test.js\"", - "test:mocha-options": "node dist/test-runner.js --config \"demo/test/mocha-options/config.js\"", - "test:playwright": "node dist/test-runner.js \"demo/test/pass-*.test.{js,html}\" --playwright --browsers chromium firefox webkit", - "test:puppeteer-firefox": "node dist/test-runner.js \"demo/test/pass-*.test.{js,html}\" --puppeteer --browsers firefox", - "test:source-maps": "node dist/test-runner.js \"demo/test/source-maps/**/*/*.test.js\"", - "test:watch": "node dist/test-runner.js \"demo/test/pass-*.test.{js,html}\" --watch" + "test:custom-html": "node dist/bin.js \"demo/test/pass-*.test.{js,html}\" --config demo/customhtml.config.js", + "test:legacy": "node dist/bin.js \"demo/test/pass-*.test.{js,html}\" --config legacy.config.js", + "test:logging": "node dist/bin.js \"demo/test/logging.test.js\"", + "test:many": "node dist/bin.js \"demo/test/many/**/*.test.js\"", + "test:mixed": "node dist/bin.js \"demo/**/*.test.js\"", + "test:mocha-options": "node dist/bin.js --config \"demo/test/mocha-options/config.js\"", + "test:playwright": "node dist/bin.js \"demo/test/pass-*.test.{js,html}\" --playwright --browsers chromium firefox webkit", + "test:puppeteer-firefox": "node dist/bin.js \"demo/test/pass-*.test.{js,html}\" --puppeteer --browsers firefox", + "test:source-maps": "node dist/bin.js \"demo/test/source-maps/**/*/*.test.js\"", + "test:watch": "node dist/bin.js \"demo/test/pass-*.test.{js,html}\" --watch" }, "files": [ "*.d.ts", diff --git a/packages/test-runner/src/bin.ts b/packages/test-runner/src/bin.ts new file mode 100644 index 000000000..67f9e9e00 --- /dev/null +++ b/packages/test-runner/src/bin.ts @@ -0,0 +1,4 @@ +#!/usr/bin/env node +import { startTestRunner } from './startTestRunner'; + +startTestRunner(); diff --git a/packages/test-runner/src/index.ts b/packages/test-runner/src/index.ts index 26be53427..994ba3a0d 100644 --- a/packages/test-runner/src/index.ts +++ b/packages/test-runner/src/index.ts @@ -1,4 +1,4 @@ export * from '@web/test-runner-core'; export { chromeLauncher } from '@web/test-runner-chrome'; export { defaultReporter } from '@web/test-runner-cli'; -export { TestRunnerConfig } from './test-runner'; +export { TestRunnerConfig, startTestRunner } from './startTestRunner'; diff --git a/packages/test-runner/src/test-runner.ts b/packages/test-runner/src/startTestRunner.ts similarity index 86% rename from packages/test-runner/src/test-runner.ts rename to packages/test-runner/src/startTestRunner.ts index 51abe917c..77f602f83 100644 --- a/packages/test-runner/src/test-runner.ts +++ b/packages/test-runner/src/startTestRunner.ts @@ -1,9 +1,8 @@ -#!/usr/bin/env node import { TestRunnerCoreConfig } from '@web/test-runner-core'; import { readCliArgsConfig, readConfig, - startTestRunner, + startTestRunner as defaultStartTestRunner, validateCoreConfig, defaultReporter, } from '@web/test-runner-cli'; @@ -31,6 +30,11 @@ export interface TestRunnerCliArgsConfig extends Omit { +export async function startTestRunner(options: StartTestRunnerOptions = {}) { + const { autoExitProcess = true, argv = process.argv } = options; try { - const cliArgs = readCliArgsConfig(cliOptions); + const cliArgs = readCliArgsConfig(cliOptions, argv); const cliArgsConfig: Partial = {}; + for (const [key, value] of Object.entries(cliArgs)) { if (key !== 'browsers') { // cli args are read from a file, they are validated by cli-options and later on as well @@ -121,9 +127,13 @@ const cliOptions: (commandLineArgs.OptionDefinition & { description: string })[] config.plugins!.push(setViewportPlugin(), emulateMediaPlugin(), setUserAgentPlugin()); const validatedConfig = validateCoreConfig(config); - startTestRunner(validatedConfig); + return defaultStartTestRunner(validatedConfig, { autoExitProcess }); } catch (error) { - console.error(chalk.red(`\nFailed to start test runner: ${error.message}\n`)); - process.exit(1); + if (autoExitProcess) { + console.error(chalk.red(`\nFailed to start test runner: ${error.message}\n`)); + process.exit(1); + } else { + throw error; + } } -})(); +} diff --git a/packages/tsconfig.project.json b/packages/tsconfig.project.json new file mode 100644 index 000000000..1ce046af2 --- /dev/null +++ b/packages/tsconfig.project.json @@ -0,0 +1,54 @@ +// GENERATED by update-package-tsconfig +{ + "files": [], + "references": [ + { + "path": "./dev-server-core/tsconfig.json" + }, + { + "path": "./test-runner-core/tsconfig.json" + }, + { + "path": "./test-runner-coverage-v8/tsconfig.json" + }, + { + "path": "./test-runner-server/tsconfig.json" + }, + { + "path": "./dev-server-rollup/tsconfig.json" + }, + { + "path": "./test-runner-chrome/tsconfig.json" + }, + { + "path": "./test-runner-cli/tsconfig.json" + }, + { + "path": "./dev-server-legacy/tsconfig.json" + }, + { + "path": "./test-runner-selenium/tsconfig.json" + }, + { + "path": "./dev-server/tsconfig.json" + }, + { + "path": "./dev-server-cli/tsconfig.json" + }, + { + "path": "./dev-server-esbuild/tsconfig.json" + }, + { + "path": "./test-runner/tsconfig.json" + }, + { + "path": "./test-runner-puppeteer/tsconfig.json" + }, + { + "path": "./test-runner-playwright/tsconfig.json" + }, + { + "path": "./test-runner-browserstack/tsconfig.json" + } + ] +} \ No newline at end of file diff --git a/scripts/update-esm-entrypoints.mjs b/scripts/update-esm-entrypoints.mjs index c45a8e6f1..423ee1354 100644 --- a/scripts/update-esm-entrypoints.mjs +++ b/scripts/update-esm-entrypoints.mjs @@ -12,7 +12,10 @@ for (const pkg of packages) { const namedExports = Object.keys(cjsModule) .filter(name => name !== 'default' && !name.startsWith('_')) .join(', '); - const esmEntrypoint = `// this file is autogenerated with the update-esm-entrypoints script + const esmEntrypoint = + namedExports.length === 0 + ? '' + : `// this file is autogenerated with the update-esm-entrypoints script import cjsEntrypoint from './dist/index.js'; const { ${namedExports} } = cjsEntrypoint; diff --git a/tsconfig.json b/tsconfig.json index 1e2be2a7c..dffe22772 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,9 @@ "extends": "./tsconfig.node-base.json", "files": [], "references": [ + { + "path": "./packages/config-loader/tsconfig.json" + }, { "path": "./packages/dev-server-core/tsconfig.json" }, @@ -23,13 +26,13 @@ "path": "./packages/test-runner-chrome/tsconfig.json" }, { - "path": "./packages/config-loader/tsconfig.json" + "path": "./packages/dev-server-cli/tsconfig.json" }, { - "path": "./packages/test-runner-playwright/tsconfig.json" + "path": "./packages/dev-server-rollup/tsconfig.json" }, { - "path": "./packages/dev-server-rollup/tsconfig.json" + "path": "./packages/test-runner-playwright/tsconfig.json" }, { "path": "./packages/test-runner-cli/tsconfig.json" @@ -52,6 +55,9 @@ { "path": "./packages/rollup-plugin-workbox/tsconfig.json" }, + { + "path": "./packages/dev-server/tsconfig.json" + }, { "path": "./packages/dev-server-esbuild/tsconfig.json" }, diff --git a/workspace-packages.mjs b/workspace-packages.mjs index d81ba0730..34ee033ab 100644 --- a/workspace-packages.mjs +++ b/workspace-packages.mjs @@ -3,6 +3,8 @@ const packages = [ { name: 'browser-logs', type: 'ts', environment: 'node' }, { name: 'rollup-plugin-copy', type: 'js', environment: 'node' }, { name: 'rollup-plugin-workbox', type: 'ts', environment: 'node' }, + { name: 'dev-server', type: 'ts', environment: 'node' }, + { name: 'dev-server-cli', type: 'ts', environment: 'node' }, { name: 'dev-server-core', type: 'ts', environment: 'node' }, { name: 'dev-server-esbuild', type: 'ts', environment: 'node' }, { name: 'dev-server-rollup', type: 'ts', environment: 'node' }, diff --git a/yarn.lock b/yarn.lock index e078de55b..f3aa7b520 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6889,9 +6889,9 @@ lit-element@^2.2.1, lit-element@^2.3.1: lit-html "^1.1.1" lit-html@^1.0.0, lit-html@^1.1.1, lit-html@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-1.3.0.tgz#c80f3cc5793a6dea6c07172be90a70ab20e56034" - integrity sha512-0Q1bwmaFH9O14vycPHw8C/IeHMk/uSDldVLIefu/kfbTBGIc44KGH6A8p1bDfxUfHdc8q6Ct7kQklWoHgr4t1Q== + version "1.2.1" + resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-1.2.1.tgz#1fb933dc1e2ddc095f60b8086277d4fcd9d62cc8" + integrity sha512-GSJHHXMGLZDzTRq59IUfL9FCdAlGfqNp/dEa7k7aBaaWD+JKaCjsAk9KYm2V12ItonVaYx2dprN66Zdm1AuBTQ== load-json-file@^4.0.0: version "4.0.0" @@ -7823,10 +7823,10 @@ only@~0.0.2: resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q= -open@^7.0.3, open@^7.0.4: - version "7.2.0" - resolved "https://registry.yarnpkg.com/open/-/open-7.2.0.tgz#212959bd7b0ce2e8e3676adc76e3cf2f0a2498b4" - integrity sha512-4HeyhxCvBTI5uBePsAdi55C5fmqnWZ2e2MlmvWi5KW5tdH5rxoiv/aMtbeVxKZc3eWkT1GymMnLG8XC4Rq4TDQ== +open@^7.0.3, open@^7.0.4, open@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/open/-/open-7.1.0.tgz#68865f7d3cb238520fa1225a63cf28bcf8368a1c" + integrity sha512-lLPI5KgOwEYCDKXf4np7y1PBEkj7HYIyP2DY8mVDRnx0VIIu6bNrRB0R66TuO7Mack6EnTNLm4uvcl1UoklTpA== dependencies: is-docker "^2.0.0" is-wsl "^2.1.1" @@ -8330,7 +8330,16 @@ popper.js@^1.15.0: resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== -portfinder@^1.0.21, portfinder@^1.0.26, portfinder@^1.0.28: +portfinder@^1.0.21, portfinder@^1.0.26, portfinder@^1.0.27: + version "1.0.27" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.27.tgz#a41333c116b5e5f3d380f9745ac2f35084c4c758" + integrity sha512-bJ3U3MThKnyJ9Dx1Idtm5pQmxXqw08+XOHhi/Lie8OF1OlhVaBFhsntAIhkZYjfDcCzszSr0w1yCbccThhzgxQ== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.1" + +portfinder@^1.0.28: version "1.0.28" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== @@ -8999,7 +9008,7 @@ puppeteer-core@^5.0.0: unbzip2-stream "^1.3.3" ws "^7.2.3" -puppeteer@^5.1.0: +puppeteer@^5.1.0, puppeteer@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.2.1.tgz#7f0564f0a5384f352a38c8cc42af875cd87f4ea6" integrity sha512-PZoZG7u+T6N1GFWBQmGVG162Ak5MAy8nYSVpeeQrwJK2oYUlDWpHEJPcd/zopyuEMTv7DiztS1blgny1txR2qw==