From cf335e312a25335fd3c2cec55278ff74dcdb82e1 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Thu, 20 Aug 2020 10:54:26 -0500 Subject: [PATCH] feat(dev-server): single-threaded dev-server for debugging Allow the dev server to run on the same process so it's easier to debug and step through http requests and responses. Added for the on-demand dev mode prerendering and dev node module feature. --- scripts/bundles/dev-server.ts | 138 ++++--- scripts/bundles/plugins/alias-plugin.ts | 1 + .../bundles/plugins/content-types-plugin.ts | 54 +++ scripts/test/validate-build.ts | 4 +- src/compiler/bundle/dev-module.ts | 20 +- src/compiler/config/validate-dev-server.ts | 21 +- src/compiler/config/validate-workers.ts | 4 + src/declarations/stencil-private.ts | 94 +++-- src/declarations/stencil-public-compiler.ts | 47 ++- src/dev-server/content-types-db.json | 4 + src/dev-server/dev-server-client/index.ts | 8 +- src/dev-server/dev-server-utils.ts | 71 ++-- src/dev-server/find-closest-port.ts | 34 -- src/dev-server/index.ts | 355 ++++++++---------- src/dev-server/open-in-browser.ts | 6 + src/dev-server/open-in-editor.ts | 132 ++++--- src/dev-server/public.ts | 6 - src/dev-server/request-handler.ts | 103 ++--- src/dev-server/serve-404.ts | 40 +- src/dev-server/serve-500.ts | 25 +- src/dev-server/serve-compiler-request.ts | 68 ++-- src/dev-server/serve-dev-client.ts | 91 +++-- src/dev-server/serve-directory-index.ts | 62 ++- src/dev-server/serve-file.ts | 56 ++- src/dev-server/server-http.ts | 53 ++- src/dev-server/server-process.ts | 98 +++++ src/dev-server/server-web-socket.ts | 64 ++-- src/dev-server/server-worker-main.ts | 48 +++ src/dev-server/server-worker-thread.js | 10 + src/dev-server/server-worker.ts | 44 --- src/dev-server/start-server-worker.ts | 87 ----- test/hello-vdom/package.json | 2 +- test/hello-vdom/src/index.ts | 2 +- tsconfig.json | 3 +- 34 files changed, 996 insertions(+), 859 deletions(-) create mode 100644 scripts/bundles/plugins/content-types-plugin.ts create mode 100644 src/dev-server/content-types-db.json delete mode 100644 src/dev-server/find-closest-port.ts create mode 100644 src/dev-server/open-in-browser.ts delete mode 100644 src/dev-server/public.ts create mode 100644 src/dev-server/server-process.ts create mode 100644 src/dev-server/server-worker-main.ts create mode 100644 src/dev-server/server-worker-thread.js delete mode 100644 src/dev-server/server-worker.ts delete mode 100644 src/dev-server/start-server-worker.ts diff --git a/scripts/bundles/dev-server.ts b/scripts/bundles/dev-server.ts index cbcd1d8ab12..7162edd8772 100644 --- a/scripts/bundles/dev-server.ts +++ b/scripts/bundles/dev-server.ts @@ -11,15 +11,15 @@ import type { BuildOptions } from '../utils/options'; import type { RollupOptions, OutputChunk, Plugin } from 'rollup'; import { minify } from 'terser'; import ts from 'typescript'; -import { prettyMinifyPlugin } from './plugins/pretty-minify'; import { getBanner } from '../utils/banner'; +import { contentTypesPlugin } from './plugins/content-types-plugin'; export async function devServer(opts: BuildOptions) { const inputDir = join(opts.buildDir, 'dev-server'); // create public d.ts - let dts = await fs.readFile(join(inputDir, 'public.d.ts'), 'utf8'); - dts = dts.replace('@stencil/core/internal', '../internal/index'); + let dts = await fs.readFile(join(inputDir, 'index.d.ts'), 'utf8'); + dts = dts.replace('../declarations', '../internal/index'); await fs.writeFile(join(opts.output.devServerDir, 'index.d.ts'), dts); // write package.json @@ -33,71 +33,86 @@ export async function devServer(opts: BuildOptions) { // copy static files await fs.copy(join(opts.srcDir, 'dev-server', 'static'), join(opts.output.devServerDir, 'static')); + // copy server-worker-thread.js + await fs.copy(join(opts.srcDir, 'dev-server', 'server-worker-thread.js'), join(opts.output.devServerDir, 'server-worker-thread.js')); + // copy template files await fs.copy(join(opts.srcDir, 'dev-server', 'templates'), join(opts.output.devServerDir, 'templates')); - // create content-type-db.json - await createContentTypeData(opts); - - const devServerBundle: RollupOptions = { + const external = [ + 'assert', + 'buffer', + 'child_process', + 'crypto', + 'events', + 'fs', + 'http', + 'https', + 'net', + 'os', + 'path', + 'stream', + 'url', + 'util', + 'zlib', + ]; + + const plugins = [ + contentTypesPlugin(opts), + { + name: 'devServerWorkerResolverPlugin', + resolveId(importee) { + if (importee.includes('open-in-editor-api')) { + return { + id: './open-in-editor-api.js', + external: true, + }; + } + return null; + }, + }, + relativePathPlugin('@sys-api-node', '../sys/node/index.js'), + relativePathPlugin('glob', '../sys/node/glob.js'), + relativePathPlugin('graceful-fs', '../sys/node/graceful-fs.js'), + relativePathPlugin('ws', './ws.js'), + relativePathPlugin('../sys/node/node-sys.js', '../sys/node/node-sys.js'), + aliasPlugin(opts), + rollupResolve({ + preferBuiltins: true, + }), + rollupCommonjs(), + replacePlugin(opts), + ]; + + const devServerIndexBundle: RollupOptions = { input: join(inputDir, 'index.js'), output: { format: 'cjs', file: join(opts.output.devServerDir, 'index.js'), + hoistTransitiveImports: false, esModule: false, preferConst: true, + banner: getBanner(opts, `Stencil Dev Server`, true), }, - external: ['assert', 'child_process', 'fs', 'os', 'path', 'url', 'util'], - plugins: [ - relativePathPlugin('glob', '../sys/node/glob.js'), - relativePathPlugin('graceful-fs', '../sys/node/graceful-fs.js'), - relativePathPlugin('../sys/node/node-sys.js', '../sys/node/node-sys.js'), - aliasPlugin(opts), - rollupResolve({ - preferBuiltins: true, - }), - rollupCommonjs(), - prettyMinifyPlugin(opts, getBanner(opts, `Stencil Dev Server`, true)), - ], + external, + plugins, treeshake: { moduleSideEffects: false, }, }; - const devServerWorkerBundle: RollupOptions = { - input: join(inputDir, 'server-worker.js'), + const devServerProcessBundle: RollupOptions = { + input: join(inputDir, 'server-process.js'), output: { format: 'cjs', - file: join(opts.output.devServerDir, 'server-worker.js'), + file: join(opts.output.devServerDir, 'server-process.js'), + hoistTransitiveImports: false, esModule: false, preferConst: true, + banner: getBanner(opts, `Stencil Dev Server Process`, true), }, - external: ['assert', 'buffer', 'child_process', 'crypto', 'events', 'fs', 'http', 'https', 'net', 'os', 'path', 'querystring', 'stream', 'url', 'util', 'zlib'], - plugins: [ - { - name: 'devServerWorkerResolverPlugin', - resolveId(importee) { - if (importee.includes('open-in-editor-api')) { - return { - id: './open-in-editor-api.js', - external: true, - }; - } - return null; - }, - }, - relativePathPlugin('ws', './ws.js'), - relativePathPlugin('graceful-fs', '../sys/node/graceful-fs.js'), - relativePathPlugin('glob', '../sys/node/glob.js'), - relativePathPlugin('../sys/node/node-sys.js', '../sys/node/node-sys.js'), - aliasPlugin(opts), - rollupResolve({ - preferBuiltins: true, - }), - rollupCommonjs(), - replacePlugin(opts), - prettyMinifyPlugin(opts, getBanner(opts, `Stencil Dev Server`, true)), - ], + external, + plugins, treeshake: { moduleSideEffects: false, }, @@ -219,32 +234,10 @@ export async function devServer(opts: BuildOptions) { plugins: [appErrorCssPlugin(), replacePlugin(opts), rollupResolve()], }; - return [devServerBundle, devServerWorkerBundle, connectorBundle, devServerClientBundle]; -} - -async function createContentTypeData(opts: BuildOptions) { - // create a focused content-type lookup object from - // the mime db json file - const mimeDbSrcPath = join(opts.nodeModulesDir, 'mime-db', 'db.json'); - const mimeDbJson = await fs.readJson(mimeDbSrcPath); - - const contentTypeDestPath = join(opts.output.devServerDir, 'content-type-db.json'); - - const exts = {}; - - Object.keys(mimeDbJson).forEach(mimeType => { - const mimeTypeData = mimeDbJson[mimeType]; - if (Array.isArray(mimeTypeData.extensions)) { - mimeTypeData.extensions.forEach(ext => { - exts[ext] = mimeType; - }); - } - }); - - await fs.writeJson(contentTypeDestPath, exts); + return [devServerIndexBundle, devServerProcessBundle, connectorBundle, devServerClientBundle]; } -const banner = ` +const banner = `Stencil Dev Server Connector __VERSION:STENCIL__ ⚡ Stencil Dev Server Connector __VERSION:STENCIL__ ⚡ @@ -255,7 +248,6 @@ const intro = `(function(iframeWindow, appWindow, config, exports) { `; const outro = ` -document.title = document.body.innerText; })(window, window.parent, window.__DEV_CLIENT_CONFIG__, {}); `; diff --git a/scripts/bundles/plugins/alias-plugin.ts b/scripts/bundles/plugins/alias-plugin.ts index 052642f8d87..1d552f153bc 100644 --- a/scripts/bundles/plugins/alias-plugin.ts +++ b/scripts/bundles/plugins/alias-plugin.ts @@ -12,6 +12,7 @@ export function aliasPlugin(opts: BuildOptions): Plugin { ['@sys-api-deno', './index.js'], ['@sys-api-node', './index.js'], ['@deno-node-compat', './node-compat.js'], + ['@dev-server-process', './server-process.js'], ]); // ensure we use the same one diff --git a/scripts/bundles/plugins/content-types-plugin.ts b/scripts/bundles/plugins/content-types-plugin.ts new file mode 100644 index 00000000000..76cd12448b9 --- /dev/null +++ b/scripts/bundles/plugins/content-types-plugin.ts @@ -0,0 +1,54 @@ +import type { Plugin } from 'rollup'; +import type { BuildOptions } from '../../utils/options'; +import fs from 'fs-extra'; +import { join } from 'path'; + +export function contentTypesPlugin(opts: BuildOptions): Plugin { + return { + name: 'contentTypesPlugin', + resolveId(id) { + if (id.endsWith('content-types-db.json')) { + return id; + } + return null; + }, + load(id) { + if (id.endsWith('content-types-db.json')) { + return createContentTypeData(opts); + } + return null; + }, + }; +} + +async function createContentTypeData(opts: BuildOptions) { + // create a focused content-type lookup object from + // the mime db json file + const mimeDbSrcPath = join(opts.nodeModulesDir, 'mime-db', 'db.json'); + const mimeDbJson = await fs.readJson(mimeDbSrcPath); + + const extData: { ext: string; mimeType: string }[] = []; + + Object.keys(mimeDbJson).forEach(mimeType => { + const mimeTypeData = mimeDbJson[mimeType]; + if (Array.isArray(mimeTypeData.extensions)) { + mimeTypeData.extensions.forEach(ext => { + extData.push({ + ext, + mimeType, + }); + }); + } + }); + + const exts = {}; + extData + .sort((a, b) => { + if (a.ext < b.ext) return -1; + if (a.ext > b.ext) return 1; + return 0; + }) + .forEach(x => (exts[x.ext] = x.mimeType)); + + return `export default ${JSON.stringify(exts)}`; +} diff --git a/scripts/test/validate-build.ts b/scripts/test/validate-build.ts index 0f8a8db2577..046bcb3080d 100644 --- a/scripts/test/validate-build.ts +++ b/scripts/test/validate-build.ts @@ -28,9 +28,9 @@ const pkgs: TestPackage[] = [ 'dev-server/templates/directory-index.html', 'dev-server/templates/initial-load.html', 'dev-server/connector.html', - 'dev-server/content-type-db.json', 'dev-server/open-in-editor-api.js', - 'dev-server/server-worker.js', + 'dev-server/server-process.js', + 'dev-server/server-worker-thread.js', 'dev-server/visualstudio.vbs', 'dev-server/ws.js', 'dev-server/xdg-open', diff --git a/src/compiler/bundle/dev-module.ts b/src/compiler/bundle/dev-module.ts index f200724bb93..0b5a9ed2250 100644 --- a/src/compiler/bundle/dev-module.ts +++ b/src/compiler/bundle/dev-module.ts @@ -4,7 +4,12 @@ import { BuildContext } from '../build/build-ctx'; import { getRollupOptions } from './bundle-output'; import { OutputOptions, PartialResolvedId, rollup } from 'rollup'; -export const devNodeModuleResolveId = async (config: d.Config, inMemoryFs: d.InMemoryFileSystem, resolvedId: PartialResolvedId, importee: string) => { +export const devNodeModuleResolveId = async ( + config: d.Config, + inMemoryFs: d.InMemoryFileSystem, + resolvedId: PartialResolvedId, + importee: string, +) => { if (!shouldCheckDevModule(resolvedId, importee)) { return resolvedId; } @@ -52,6 +57,7 @@ const getPackageJsonPath = (resolvedPath: string, importee: string): string => { export const compilerRequest = async (config: d.Config, compilerCtx: d.CompilerCtx, data: d.CompilerRequest) => { const results: d.CompilerRequestResponse = { + path: data.path, nodeModuleId: null, nodeModuleVersion: null, nodeResolvedPath: null, @@ -118,7 +124,12 @@ export const compilerRequest = async (config: d.Config, compilerCtx: d.CompilerC return results; }; -const bundleDevModule = async (config: d.Config, compilerCtx: d.CompilerCtx, parsedUrl: ParsedDevModuleUrl, results: d.CompilerRequestResponse) => { +const bundleDevModule = async ( + config: d.Config, + compilerCtx: d.CompilerCtx, + parsedUrl: ParsedDevModuleUrl, + results: d.CompilerRequestResponse, +) => { const buildCtx = new BuildContext(config, compilerCtx); try { @@ -221,7 +232,10 @@ const parseDevModuleUrl = (config: d.Config, u: string) => { }; const getDevModuleCachePath = (config: d.Config, parsedUrl: ParsedDevModuleUrl) => { - return join(config.cacheDir, `dev_module_${parsedUrl.nodeModuleId}_${parsedUrl.nodeModuleVersion}_${DEV_MODULE_CACHE_BUSTER}.log`); + return join( + config.cacheDir, + `dev_module_${parsedUrl.nodeModuleId}_${parsedUrl.nodeModuleVersion}_${DEV_MODULE_CACHE_BUSTER}.log`, + ); }; const DEV_MODULE_CACHE_BUSTER = 0; diff --git a/src/compiler/config/validate-dev-server.ts b/src/compiler/config/validate-dev-server.ts index 90429f9aaf5..6d42aa09bbb 100644 --- a/src/compiler/config/validate-dev-server.ts +++ b/src/compiler/config/validate-dev-server.ts @@ -4,7 +4,7 @@ import { isAbsolute, join } from 'path'; import { isOutputTargetWww } from '../output-targets/output-utils'; export const validateDevServer = (config: d.Config, diagnostics: d.Diagnostic[]) => { - if (config.devServer === false || config.devServer === null) { + if ((config.devServer === null || (config.devServer as any)) === false) { return null; } @@ -49,18 +49,15 @@ export const validateDevServer = (config: d.Config, diagnostics: d.Diagnostic[]) } } - if ((devServer as any).hotReplacement === true) { - // DEPRECATED: 2019-05-20 + if (devServer.reloadStrategy === undefined) { devServer.reloadStrategy = 'hmr'; - } else if ((devServer as any).hotReplacement === false || (devServer as any).hotReplacement === null) { - // DEPRECATED: 2019-05-20 - devServer.reloadStrategy = null; - } else { - if (devServer.reloadStrategy === undefined) { - devServer.reloadStrategy = 'hmr'; - } else if (devServer.reloadStrategy !== 'hmr' && devServer.reloadStrategy !== 'pageReload' && devServer.reloadStrategy !== null) { - throw new Error(`Invalid devServer reloadStrategy "${devServer.reloadStrategy}". Valid configs include "hmr", "pageReload" and null.`); - } + } else if ( + devServer.reloadStrategy !== 'hmr' && + devServer.reloadStrategy !== 'pageReload' && + devServer.reloadStrategy !== null + ) { + const err = buildError(diagnostics); + err.messageText = `Invalid devServer reloadStrategy "${devServer.reloadStrategy}". Valid configs include "hmr", "pageReload" and null.`; } if (!isBoolean(devServer.gzip)) { diff --git a/src/compiler/config/validate-workers.ts b/src/compiler/config/validate-workers.ts index 6c4dc4f0f6e..799cc7cb7d0 100644 --- a/src/compiler/config/validate-workers.ts +++ b/src/compiler/config/validate-workers.ts @@ -14,4 +14,8 @@ export const validateWorkers = (config: d.Config) => { } config.maxConcurrentWorkers = Math.max(Math.min(config.maxConcurrentWorkers, 16), 0); + + if (config.devServer) { + config.devServer.worker = config.maxConcurrentWorkers > 0; + } }; diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts index 2ee97d4e9a9..18a2608c648 100644 --- a/src/declarations/stencil-private.ts +++ b/src/declarations/stencil-private.ts @@ -5,8 +5,8 @@ import type { CompilerBuildResults, CompilerBuildStart, CompilerFsStats, - CompilerSystem, CompilerRequestResponse, + CompilerSystem, Config, CopyResults, DevServerConfig, @@ -25,7 +25,13 @@ import type { LoggerLineUpdater, } from './stencil-public-compiler'; -import type { ComponentInterface, ListenOptions, ListenTargetOptions, VNode, VNodeData } from './stencil-public-runtime'; +import type { + ComponentInterface, + ListenOptions, + ListenTargetOptions, + VNode, + VNodeData, +} from './stencil-public-runtime'; export interface PrintLine { lineIndex: number; @@ -170,7 +176,17 @@ export interface BuildConditionals extends Partial { attachStyles?: boolean; } -export type ModuleFormat = 'amd' | 'cjs' | 'es' | 'iife' | 'system' | 'umd' | 'commonjs' | 'esm' | 'module' | 'systemjs'; +export type ModuleFormat = + | 'amd' + | 'cjs' + | 'es' + | 'iife' + | 'system' + | 'umd' + | 'commonjs' + | 'esm' + | 'module' + | 'systemjs'; export interface RollupResultModule { id: string; @@ -632,7 +648,13 @@ export interface CompilerCtx { export type NodeMap = WeakMap; -export type TsService = (compilerCtx: CompilerCtx, buildCtx: BuildCtx, tsFilePaths: string[], checkCacheKey: boolean, useFsCache: boolean) => Promise; +export type TsService = ( + compilerCtx: CompilerCtx, + buildCtx: BuildCtx, + tsFilePaths: string[], + checkCacheKey: boolean, + useFsCache: boolean, +) => Promise; /** Must be serializable to JSON!! */ export interface ComponentCompilerFeatures { @@ -912,7 +934,13 @@ export interface ComponentConstructorProperty { watchCallbacks?: string[]; } -export type ComponentConstructorPropertyType = StringConstructor | BooleanConstructor | NumberConstructor | 'string' | 'boolean' | 'number'; +export type ComponentConstructorPropertyType = + | StringConstructor + | BooleanConstructor + | NumberConstructor + | 'string' + | 'boolean' + | 'number'; export interface ComponentConstructorEvent { name: string; @@ -957,17 +985,6 @@ export interface CssVarShim { updateGlobal(): void; } -export interface DevServerStartResponse { - address: string; - basePath: string; - browserUrl: string; - initialLoadUrl: string; - protocol: string; - port: number; - root: string; - error: string; -} - export interface DevClientWindow extends Window { ['s-dev-server']: boolean; ['s-initial-load']: boolean; @@ -985,7 +1002,8 @@ export interface DevClientConfig { export interface HttpRequest { method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS'; acceptHeader: string; - url: string; + url: URL; + searchParams: URLSearchParams; pathname?: string; filePath?: string; stats?: CompilerFsStats; @@ -994,9 +1012,11 @@ export interface HttpRequest { } export interface DevServerMessage { - resolveId?: number; startServer?: DevServerConfig; - serverStarted?: DevServerStartResponse; + closeServer?: boolean; + serverStarted?: DevServerConfig; + serverClosed?: boolean; + buildStart?: boolean; buildLog?: BuildLog; buildResults?: CompilerBuildResults; requestBuildResults?: boolean; @@ -1011,7 +1031,9 @@ export interface DevServerMessage { }; } -export type DevServerDestroy = () => void; +export type DevServerSendMessage = (msg: DevServerMessage) => void; + +export type InitServerProcess = (sendMsg: (msg: DevServerMessage) => void) => (msg: DevServerMessage) => void; export interface DevResponseHeaders { 'cache-control'?: string; @@ -1390,7 +1412,11 @@ export interface Plugin { pluginType?: string; load?: (id: string, context: PluginCtx) => Promise | string; resolveId?: (importee: string, importer: string, context: PluginCtx) => Promise | string; - transform?: (sourceText: string, id: string, context: PluginCtx) => Promise | PluginTransformResults | string; + transform?: ( + sourceText: string, + id: string, + context: PluginCtx, + ) => Promise | PluginTransformResults | string; } export interface PluginTransformResults { @@ -1624,8 +1650,18 @@ export interface PlatformRuntime { $resourcesUrl$: string; jmp: (c: Function) => any; raf: (c: FrameRequestCallback) => number; - ael: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void; - rel: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void; + ael: ( + el: EventTarget, + eventName: string, + listener: EventListenerOrEventListenerObject, + options: boolean | AddEventListenerOptions, + ) => void; + rel: ( + el: EventTarget, + eventName: string, + listener: EventListenerOrEventListenerObject, + options: boolean | AddEventListenerOptions, + ) => void; ce: (eventName: string, opts?: any) => CustomEvent; } @@ -1646,7 +1682,10 @@ export interface ScreenshotConnector { pullMasterBuild(): Promise; publishBuild(buildResults: ScreenshotBuildResults): Promise; getScreenshotCache(): Promise; - updateScreenshotCache(screenshotCache: ScreenshotCache, buildResults: ScreenshotBuildResults): Promise; + updateScreenshotCache( + screenshotCache: ScreenshotCache, + buildResults: ScreenshotBuildResults, + ): Promise; generateJsonpDataUris(build: ScreenshotBuild): Promise; sortScreenshots(screenshots: Screenshot[]): Screenshot[]; toJson(masterBuild: ScreenshotBuild, screenshotCache: ScreenshotCache): string; @@ -2364,7 +2403,12 @@ export interface VNodeProdData { export interface CompilerWorkerContext { optimizeCss(inputOpts: OptimizeCssInput): Promise; - prepareModule(input: string, minifyOpts: any, transpile: boolean, inlineHelpers: boolean): Promise<{ output: string; diagnostics: Diagnostic[] }>; + prepareModule( + input: string, + minifyOpts: any, + transpile: boolean, + inlineHelpers: boolean, + ): Promise<{ output: string; diagnostics: Diagnostic[] }>; prerenderWorker(prerenderRequest: PrerenderUrlRequest): Promise; transformCssToEsm(input: TransformCssToEsmInput): Promise; } diff --git a/src/declarations/stencil-public-compiler.ts b/src/declarations/stencil-public-compiler.ts index 4b7577194f7..9090633437e 100644 --- a/src/declarations/stencil-public-compiler.ts +++ b/src/declarations/stencil-public-compiler.ts @@ -395,13 +395,12 @@ export interface StencilDevServerConfig { reloadStrategy?: PageReloadStrategy; root?: string; websocket?: boolean; + worker?: boolean; } export interface DevServerConfig extends StencilDevServerConfig { browserUrl?: string; - contentTypes?: { [ext: string]: string }; devServerDir?: string; - editors?: DevServerEditor[]; excludeHmr?: string[]; historyApiFallback?: HistoryApiFallback; openBrowser?: boolean; @@ -464,7 +463,17 @@ export interface ConfigFlags { devtools?: boolean; } -export type TaskCommand = 'build' | 'docs' | 'generate' | 'g' | 'help' | 'info' | 'prerender' | 'serve' | 'test' | 'version'; +export type TaskCommand = + | 'build' + | 'docs' + | 'generate' + | 'g' + | 'help' + | 'info' + | 'prerender' + | 'serve' + | 'test' + | 'version'; export type PageReloadStrategy = 'hmr' | 'pageReload' | null; @@ -822,7 +831,11 @@ export interface CompilerSystem { */ createWorkerController?(maxConcurrentWorkers: number): WorkerMainController; encodeToBase64(str: string): string; - ensureDependencies?(opts: { rootDir: string; logger: Logger; dependencies: CompilerDependency[] }): Promise<{ stencilPath: string; diagnostics: Diagnostic[] }>; + ensureDependencies?(opts: { + rootDir: string; + logger: Logger; + dependencies: CompilerDependency[]; + }): Promise<{ stencilPath: string; diagnostics: Diagnostic[] }>; ensureResources?(opts: { rootDir: string; logger: Logger; dependencies: CompilerDependency[] }): Promise; /** * process.exit() @@ -1143,7 +1156,12 @@ export interface CompilerBuildStart { export type CompilerFileWatcherCallback = (fileName: string, eventKind: CompilerFileWatcherEvent) => void; -export type CompilerFileWatcherEvent = CompilerEventFileAdd | CompilerEventFileDelete | CompilerEventFileUpdate | CompilerEventDirAdd | CompilerEventDirDelete; +export type CompilerFileWatcherEvent = + | CompilerEventFileAdd + | CompilerEventFileDelete + | CompilerEventFileUpdate + | CompilerEventDirAdd + | CompilerEventDirDelete; export type CompilerEventName = | CompilerEventFsChange @@ -2144,9 +2162,9 @@ export interface Compiler { } export interface CompilerWatcher extends BuildOnEvents { - start(): Promise; - close(): Promise; - request(data: CompilerRequest): Promise; + start: () => Promise; + close: () => Promise; + request: (data: CompilerRequest) => Promise; } export interface CompilerRequest { @@ -2158,6 +2176,7 @@ export interface WatcherCloseResults { } export interface CompilerRequestResponse { + path: string; nodeModuleId: string; nodeModuleVersion: string; nodeResolvedPath: string; @@ -2245,7 +2264,17 @@ export interface TranspileOptions { sys?: CompilerSystem; } -export type CompileTarget = 'latest' | 'esnext' | 'es2020' | 'es2019' | 'es2018' | 'es2017' | 'es2015' | 'es5' | string | undefined; +export type CompileTarget = + | 'latest' + | 'esnext' + | 'es2020' + | 'es2019' + | 'es2018' + | 'es2017' + | 'es2015' + | 'es5' + | string + | undefined; export interface TranspileResults { code: string; diff --git a/src/dev-server/content-types-db.json b/src/dev-server/content-types-db.json new file mode 100644 index 00000000000..11b94086a6f --- /dev/null +++ b/src/dev-server/content-types-db.json @@ -0,0 +1,4 @@ +{ + "replaced-at": "build-time", + "html": "text/html" +} \ No newline at end of file diff --git a/src/dev-server/dev-server-client/index.ts b/src/dev-server/dev-server-client/index.ts index f58c2ba5b74..88886db5f1d 100644 --- a/src/dev-server/dev-server-client/index.ts +++ b/src/dev-server/dev-server-client/index.ts @@ -10,11 +10,11 @@ const defaultConfig: d.DevClientConfig = { basePath: appWindow.location.pathname, editors: [], reloadStrategy: 'hmr', - socketUrl: `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.hostname}${location.port !== '' ? ':' + location.port : ''}/`, + socketUrl: `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.hostname}${ + location.port !== '' ? ':' + location.port : '' + }/`, }; applyPolyfills(iframeWindow); -const devClientConfig = Object.assign({}, defaultConfig, appWindow.devServerConfig, config); - -initDevClient(appWindow, devClientConfig); +initDevClient(appWindow, Object.assign({}, defaultConfig, appWindow.devServerConfig, config)); diff --git a/src/dev-server/dev-server-utils.ts b/src/dev-server/dev-server-utils.ts index a3a37d1166e..758cf8ff3b5 100644 --- a/src/dev-server/dev-server-utils.ts +++ b/src/dev-server/dev-server-utils.ts @@ -1,56 +1,10 @@ import type * as d from '../declarations'; import * as c from './dev-server-constants'; +import contentTypes from './content-types-db.json'; import { version } from '../version'; -import util from 'util'; - -const msgResolves = new Map void>(); -let resolveIds = 1; - -export function sendMsg(prcs: NodeJS.Process, msg: d.DevServerMessage) { - prcs.send(msg); -} - -export function sendMsgWithResponse(prcs: NodeJS.Process, msg: d.DevServerMessage) { - return new Promise(resolve => { - msg.resolveId = resolveIds++; - msgResolves.set(msg.resolveId, resolve); - sendMsg(prcs, msg); - }); -} - -export function createMessageReceiver(prcs: NodeJS.Process, cb: (msg: d.DevServerMessage) => void) { - prcs.on('message', (msg: d.DevServerMessage) => { - if (typeof msg.resolveId === 'number') { - const resolve = msgResolves.get(msg.resolveId); - if (resolve) { - msgResolves.delete(msg.resolveId); - resolve(msg); - } - } - cb(msg); - }); -} - -export function sendError(prcs: NodeJS.Process, e: any) { - const msg: d.DevServerMessage = { - error: { - message: e, - }, - }; - - if (typeof e === 'string') { - msg.error.message = e + ''; - } else if (e) { - try { - msg.error.message = util.inspect(e) + ''; - } catch (e) {} - } - - sendMsg(prcs, msg); -} export function responseHeaders(headers: d.DevResponseHeaders): any { - return Object.assign({}, DEFAULT_HEADERS, headers); + return { ...DEFAULT_HEADERS, ...headers }; } const DEFAULT_HEADERS: d.DevResponseHeaders = { @@ -89,14 +43,14 @@ export function getDevServerClientUrl(devServerConfig: d.DevServerConfig, host: return getBrowserUrl(protocol ?? devServerConfig.protocol, address, port, devServerConfig.basePath, c.DEV_SERVER_URL); } -export function getContentType(devServerConfig: d.DevServerConfig, filePath: string) { +export function getContentType(filePath: string) { const last = filePath.replace(/^.*[/\\]/, '').toLowerCase(); const ext = last.replace(/^.*\./, '').toLowerCase(); const hasPath = last.length < filePath.length; const hasDot = ext.length < last.length - 1; - return ((hasDot || !hasPath) && devServerConfig.contentTypes[ext]) || 'application/octet-stream'; + return ((hasDot || !hasPath) && (contentTypes as any)[ext]) || 'application/octet-stream'; } export function isHtmlFile(filePath: string) { @@ -156,3 +110,20 @@ export function shouldCompress(devServerConfig: d.DevServerConfig, req: d.HttpRe return true; } + +export function sendLogRequest( + devServerConfig: d.DevServerConfig, + req: d.HttpRequest, + status: number, + sendMsg: d.DevServerSendMessage, +) { + if (devServerConfig.logRequests) { + sendMsg({ + requestLog: { + method: req.method, + url: req.pathname, + status, + }, + }); + } +} diff --git a/src/dev-server/find-closest-port.ts b/src/dev-server/find-closest-port.ts deleted file mode 100644 index 64600e08eff..00000000000 --- a/src/dev-server/find-closest-port.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as net from 'net'; - -export async function findClosestOpenPort(host: string, port: number): Promise { - async function t(portToCheck: number): Promise { - const isTaken = await isPortTaken(host, portToCheck); - if (!isTaken) { - return portToCheck; - } - return t(portToCheck + 1); - } - - return t(port); -} - -export function isPortTaken(host: string, port: number): Promise { - return new Promise((resolve, reject) => { - const tester = net - .createServer() - .once('error', () => { - resolve(true); - }) - .once('listening', () => { - tester - .once('close', () => { - resolve(false); - }) - .close(); - }) - .on('error', (err: any) => { - reject(err); - }) - .listen(port, host); - }); -} diff --git a/src/dev-server/index.ts b/src/dev-server/index.ts index 939f82121f8..40e8aa87a73 100644 --- a/src/dev-server/index.ts +++ b/src/dev-server/index.ts @@ -1,240 +1,191 @@ import type { BuildOnEventRemove, - CompilerBuildResults, - CompilerEventName, CompilerWatcher, - DevServer, DevServerConfig, - DevServerMessage, - DevServerStartResponse, Logger, StencilDevServerConfig, + DevServer, + CompilerBuildResults, + InitServerProcess, + DevServerMessage, } from '../declarations'; -import { normalizePath } from '@utils'; -import { ChildProcess, fork } from 'child_process'; +import { initServerProcessWorkerProxy } from './server-worker-main'; import path from 'path'; -import open from 'open'; -export async function start(stencilDevServerConfig: StencilDevServerConfig, logger: Logger, watcher?: CompilerWatcher) { - let devServer: DevServer = null; - const devServerConfig = { ...stencilDevServerConfig } as DevServerConfig; - const timespan = logger.createTimeSpan(`starting dev server`, true); +export function start(stencilDevServerConfig: StencilDevServerConfig, logger: Logger, watcher?: CompilerWatcher) { + return new Promise(async (resolve, reject) => { + try { + const devServerConfig: DevServerConfig = { + devServerDir: __dirname, + ...stencilDevServerConfig, + }; - try { - // using the path stuff below because after the the bundles are created - // then these files are no longer relative to how they are in the src directory - devServerConfig.devServerDir = __dirname; + if (!path.isAbsolute(devServerConfig.root)) { + devServerConfig.root = path.join(process.cwd(), devServerConfig.root); + } - // get the path of the dev server module - const workerPath = require.resolve(path.join(devServerConfig.devServerDir, 'server-worker.js')); + let initServerProcess: InitServerProcess; - const filteredExecArgs = process.execArgv.filter(v => !/^--(debug|inspect)/.test(v)); + if (stencilDevServerConfig.worker === true || stencilDevServerConfig.worker === undefined) { + // fork a worker process + initServerProcess = initServerProcessWorkerProxy; + } else { + // same process + const devServerProcess = await import('@dev-server-process'); + initServerProcess = devServerProcess.initServerProcess; + } - const forkOpts: any = { - execArgv: filteredExecArgs, - env: process.env, - cwd: process.cwd(), - stdio: ['pipe', 'pipe', 'pipe', 'ipc'], - }; + startServer(devServerConfig, logger, watcher, initServerProcess, resolve, reject); + } catch (e) { + reject(e); + } + }); +} - // start a new child process of the CLI process - // for the http and web socket server - const serverProcess = fork(workerPath, [], forkOpts); +function startServer( + devServerConfig: DevServerConfig, + logger: Logger, + watcher: CompilerWatcher, + initServerProcess: InitServerProcess, + resolve: (devServer: DevServer) => void, + reject: (err: any) => void, +) { + const timespan = logger.createTimeSpan(`starting dev server`, true); - const devServerContext: DevServerMainContext = { - isActivelyBuilding: false, - lastBuildResults: null, - }; + const startupTimeout = + logger.getLevel() !== 'debug' + ? setTimeout(() => { + reject(`dev server startup timeout`); + }, 15000) + : null; + + let isActivelyBuilding = false; + let lastBuildResults: CompilerBuildResults = null; + let devServer: DevServer = null; + let removeWatcher: BuildOnEventRemove = null; + let closeResolve: () => void = null; + let hasStarted = false; + let browserUrl = ''; - const starupDevServerConfig = await startWorkerServer(devServerConfig, logger, watcher, serverProcess, devServerContext); + let sendToWorker: (msg: DevServerMessage) => void = null; - let removeWatcher: BuildOnEventRemove = null; - if (watcher) { - removeWatcher = watcher.on((eventName, data) => { - emitMessageToClient(serverProcess, devServerContext, eventName, data); + const closePromise = new Promise(resolve => (closeResolve = resolve)); + + const close = async () => { + clearTimeout(startupTimeout); + isActivelyBuilding = false; + + if (removeWatcher) { + removeWatcher(); + } + if (devServer) { + devServer = null; + } + if (sendToWorker) { + sendToWorker({ + closeServer: true, }); + sendToWorker = null; } - - if (!path.isAbsolute(starupDevServerConfig.root)) { - starupDevServerConfig.root = path.join(process.cwd(), starupDevServerConfig.root); + return closePromise; + }; + + const emit = async (eventName: any, data: any) => { + if (sendToWorker) { + if (eventName === 'buildFinish') { + isActivelyBuilding = false; + lastBuildResults = { ...data }; + delete lastBuildResults.hmr; + sendToWorker({ buildResults: { ...lastBuildResults }, isActivelyBuilding }); + } else if (eventName === 'buildLog') { + sendToWorker({ + buildLog: { ...data }, + }); + } else if (eventName === 'buildStart') { + isActivelyBuilding = true; + } } - starupDevServerConfig.root = normalizePath(starupDevServerConfig.root); + }; + + const serverStarted = (msg: DevServerMessage) => { + hasStarted = true; + clearTimeout(startupTimeout); + devServerConfig = msg.serverStarted; devServer = { - address: starupDevServerConfig.address, - basePath: starupDevServerConfig.basePath, - browserUrl: starupDevServerConfig.browserUrl, - port: starupDevServerConfig.port, - protocol: starupDevServerConfig.protocol, - root: starupDevServerConfig.root, - close() { - try { - if (serverProcess) { - serverProcess.kill('SIGINT'); - } - if (removeWatcher) { - removeWatcher(); - removeWatcher = null; - } - } catch (e) {} - logger.debug(`dev server closed, port ${starupDevServerConfig.port}`); - return Promise.resolve(); - }, - emit(eventName: any, data: any) { - emitMessageToClient(serverProcess, devServerContext, eventName, data); - }, + address: devServerConfig.address, + basePath: devServerConfig.basePath, + browserUrl: devServerConfig.browserUrl, + protocol: devServerConfig.protocol, + port: devServerConfig.port, + root: devServerConfig.root, + emit, + close, }; - timespan.finish(`dev server started: ${starupDevServerConfig.browserUrl}`); - } catch (e) { - console.error(`dev server error: ${e}`); - } + browserUrl = devServerConfig.browserUrl; - return devServer; -} + timespan.finish(`dev server started: ${browserUrl}`); -function startWorkerServer(devServerConfig: DevServerConfig, logger: Logger, watcher: CompilerWatcher, serverProcess: ChildProcess, devServerContext: DevServerMainContext) { - let hasStarted = false; + resolve(devServer); + }; - return new Promise((resolve, reject) => { - serverProcess.stdout.on('data', (data: any) => { - // the child server process has console logged data - logger.debug(`dev server: ${data}`); - }); - - serverProcess.stderr.on('data', (data: any) => { - // the child server process has console logged an error - logger.error(`dev server error: ${data}, hasStarted: ${hasStarted}`); - if (!hasStarted) { - reject(`dev server error: ${data}`); + const requestLog = (msg: DevServerMessage) => { + if (devServerConfig.logRequests) { + let statusMsg: any; + if (msg.requestLog.status >= 400) { + statusMsg = logger.red(msg.requestLog.method); + } else if (msg.requestLog.status >= 300) { + statusMsg = logger.magenta(msg.requestLog.method); + } else { + statusMsg = logger.cyan(msg.requestLog.method); } - }); - - serverProcess.on('message', async (msg: DevServerMessage) => { - // main process has received a message from the child server process + logger.info(logger.dim(`${statusMsg} ${msg.requestLog.url}`)); + } + }; + + const serverError = (msg: DevServerMessage) => { + if (hasStarted) { + logger.error(msg.error.message + ' ' + msg.error.stack); + } else { + close(); + reject(msg.error.message); + } + }; + const receiveFromWorker = async (msg: DevServerMessage) => { + try { if (msg.serverStarted) { - if (msg.serverStarted.error) { - // error! - reject(msg.serverStarted.error); - } else { - hasStarted = true; - // received a message from the child process that the server has successfully started - if (devServerConfig.openBrowser && msg.serverStarted.initialLoadUrl) { - openInBrowser({ url: msg.serverStarted.initialLoadUrl }); - } - - // resolve that everything is good to go - resolve(msg.serverStarted); - } - - return; - } - - if (msg.requestBuildResults) { - // we received a request to send up the latest build results - if (devServerContext.lastBuildResults != null) { - // we do have build results, so let's send them to the child process - // but don't send any previous live reload data - const msg: DevServerMessage = { - buildResults: Object.assign({}, devServerContext.lastBuildResults) as any, - isActivelyBuilding: devServerContext.isActivelyBuilding, - }; - delete msg.buildResults.hmr; - - serverProcess.send(msg); - } else { - const msg: DevServerMessage = { - isActivelyBuilding: true, - }; - serverProcess.send(msg); + serverStarted(msg); + } else if (msg.serverClosed) { + logger.debug(`dev server closed: ${browserUrl}`); + closeResolve(); + } else if (msg.requestBuildResults) { + sendToWorker({ buildResults: { ...lastBuildResults }, isActivelyBuilding }); + } else if (msg.compilerRequestPath) { + if (watcher) { + const compilerRequestResults = await watcher.request({ path: msg.compilerRequestPath }); + sendToWorker({ compilerRequestResults }); } - return; + } else if (msg.requestLog) { + requestLog(msg); + } else if (msg.error) { + serverError(msg); } + } catch (e) { + logger.error('receiveFromWorker: ' + e); + } + }; - if (msg.compilerRequestPath && watcher && watcher.request) { - const rspMsg: DevServerMessage = { - resolveId: msg.resolveId, - compilerRequestResults: await watcher.request({ - path: msg.compilerRequestPath, - }), - }; - serverProcess.send(rspMsg); - return; - } - - if (msg.error) { - // received a message from the child process that is an error - if (msg.error.message) { - if (typeof msg.error.message === 'string') { - logger.error(msg.error.message); - } else { - try { - logger.error(JSON.stringify(msg.error.message)); - } catch (e) { - console.error(e); - } - } - } - - logger.debug(msg.error); - return; - } - - if (msg.requestLog) { - const req = msg.requestLog; - - let status: any; - if (req.status >= 400) { - status = logger.red(req.method); - } else if (req.status >= 300) { - status = logger.magenta(req.method); - } else { - status = logger.cyan(req.method); - } - - logger.info(logger.dim(`${status} ${req.url}`)); - return; - } - }); + if (watcher) { + removeWatcher = watcher.on(emit); + } - // have the main process send a message to the child server process - // to start the http and web socket server - serverProcess.send({ - startServer: devServerConfig, - }); + sendToWorker = initServerProcess(receiveFromWorker); - return devServerConfig; + sendToWorker({ + startServer: devServerConfig, }); } -function emitMessageToClient(serverProcess: ChildProcess, devServerContext: DevServerMainContext, eventName: CompilerEventName, data: any) { - if (eventName === 'buildFinish') { - // a compiler build has finished - // send the build results to the child server process - devServerContext.isActivelyBuilding = false; - devServerContext.lastBuildResults = { ...data }; - const msg: DevServerMessage = { - buildResults: { ...data }, - }; - - serverProcess.send(msg); - } else if (eventName === 'buildStart') { - devServerContext.isActivelyBuilding = true; - } else if (eventName === 'buildLog') { - const msg: DevServerMessage = { - buildLog: Object.assign({}, data), - }; - - serverProcess.send(msg); - } -} - -export async function openInBrowser(opts: { url: string }) { - await open(opts.url); -} - -interface DevServerMainContext { - isActivelyBuilding: boolean; - lastBuildResults: CompilerBuildResults; -} +export { DevServer, StencilDevServerConfig as DevServerConfig, Logger }; diff --git a/src/dev-server/open-in-browser.ts b/src/dev-server/open-in-browser.ts new file mode 100644 index 00000000000..06c6c8e9e65 --- /dev/null +++ b/src/dev-server/open-in-browser.ts @@ -0,0 +1,6 @@ +import open from 'open'; + +export async function openInBrowser(opts: { url: string }) { + // await open(opts.url, { app: ['google chrome', '--auto-open-devtools-for-tabs'] }); + await open(opts.url); +} diff --git a/src/dev-server/open-in-editor.ts b/src/dev-server/open-in-editor.ts index 73cb086f82d..e717a364727 100644 --- a/src/dev-server/open-in-editor.ts +++ b/src/dev-server/open-in-editor.ts @@ -1,18 +1,23 @@ import type * as d from '../declarations'; -import * as util from './dev-server-utils'; -import * as http from 'http'; -import * as querystring from 'querystring'; -import * as url from 'url'; +import type { ServerResponse } from 'http'; +import { responseHeaders, sendLogRequest } from './dev-server-utils'; import openInEditorApi from './open-in-editor-api'; -export async function serveOpenInEditor(devServerConfig: d.DevServerConfig, sys: d.CompilerSystem, req: d.HttpRequest, res: http.ServerResponse) { +export async function serveOpenInEditor( + devServerConfig: d.DevServerConfig, + sys: d.CompilerSystem, + req: d.HttpRequest, + res: ServerResponse, + sendMsg: d.DevServerSendMessage, +) { let status = 200; const data: d.OpenInEditorData = {}; try { - if (devServerConfig.editors.length > 0) { - await parseData(devServerConfig, sys, req, data); + const editors = await getEditors(); + if (editors.length > 0) { + await parseData(editors, sys, req, data); await openDataInEditor(data); } else { data.error = `no editors available`; @@ -22,17 +27,11 @@ export async function serveOpenInEditor(devServerConfig: d.DevServerConfig, sys: status = 500; } - util.sendMsg(process, { - requestLog: { - method: req.method, - url: req.url, - status, - }, - }); + sendLogRequest(devServerConfig, req, status, sendMsg); res.writeHead( status, - util.responseHeaders({ + responseHeaders({ 'content-type': 'application/json; charset=utf-8', }), ); @@ -41,41 +40,46 @@ export async function serveOpenInEditor(devServerConfig: d.DevServerConfig, sys: res.end(); } -async function parseData(devServerConfig: d.DevServerConfig, sys: d.CompilerSystem, req: d.HttpRequest, data: d.OpenInEditorData) { - const query = url.parse(req.url).query; - const qs = querystring.parse(query) as any; +async function parseData( + editors: d.DevServerEditor[], + sys: d.CompilerSystem, + req: d.HttpRequest, + data: d.OpenInEditorData, +) { + const qs = req.searchParams; - if (typeof qs.file !== 'string') { + if (!qs.has('file')) { data.error = `missing file`; return; } - data.file = qs.file; + data.file = qs.get('file'); - if (qs.line != null && !isNaN(qs.line)) { - data.line = parseInt(qs.line, 10); + if (qs.has('line') && !isNaN(qs.get('line') as any)) { + data.line = parseInt(qs.get('line'), 10); } if (typeof data.line !== 'number' || data.line < 1) { data.line = 1; } - if (qs.column != null && !isNaN(qs.column)) { - data.column = parseInt(qs.column, 10); + if (qs.has('column') && !isNaN(qs.get('column') as any)) { + data.column = parseInt(qs.get('column'), 10); } if (typeof data.column !== 'number' || data.column < 1) { data.column = 1; } - if (typeof qs.editor === 'string') { - qs.editor = qs.editor.trim().toLowerCase(); - if (devServerConfig.editors.some(e => e.id === qs.editor)) { - data.editor = qs.editor; + let editor = qs.get('editor'); + if (typeof editor === 'string') { + editor = editor.trim().toLowerCase(); + if (editors.some(e => e.id === editor)) { + data.editor = editor; } else { - data.error = `invalid editor: ${qs.editor}`; + data.error = `invalid editor: ${editor}`; return; } } else { - data.editor = devServerConfig.editors[0].id; + data.editor = editors[0].id; } const stat = await sys.stat(data.file); @@ -106,36 +110,46 @@ async function openDataInEditor(data: d.OpenInEditorData) { } } -export async function getEditors() { - const editors: d.DevServerEditor[] = []; - - try { - await Promise.all( - Object.keys(openInEditorApi.editors).map(async editorId => { - const isSupported = await isEditorSupported(editorId); - - editors.push({ - id: editorId, - priority: EDITOR_PRIORITY[editorId], - supported: isSupported, - }); - }), - ); - } catch (e) {} - - return editors - .filter(e => e.supported) - .sort((a, b) => { - if (a.priority < b.priority) return -1; - if (a.priority > b.priority) return 1; - return 0; - }) - .map(e => { - return { - id: e.id, - name: EDITORS[e.id], - } as d.DevServerEditor; +let editors: Promise = null; + +export function getEditors() { + if (!editors) { + editors = new Promise(async resolve => { + const editors: d.DevServerEditor[] = []; + + try { + await Promise.all( + Object.keys(openInEditorApi.editors).map(async editorId => { + const isSupported = await isEditorSupported(editorId); + + editors.push({ + id: editorId, + priority: EDITOR_PRIORITY[editorId], + supported: isSupported, + }); + }), + ); + } catch (e) {} + + resolve( + editors + .filter(e => e.supported) + .sort((a, b) => { + if (a.priority < b.priority) return -1; + if (a.priority > b.priority) return 1; + return 0; + }) + .map(e => { + return { + id: e.id, + name: EDITORS[e.id], + } as d.DevServerEditor; + }), + ); }); + } + + return editors; } async function isEditorSupported(editorId: string) { diff --git a/src/dev-server/public.ts b/src/dev-server/public.ts deleted file mode 100644 index 6f5653c2775..00000000000 --- a/src/dev-server/public.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { DevServer, Logger, StencilDevServerConfig as DevServerConfig } from '@stencil/core/internal'; - -export declare function start(devServerConfig: DevServerConfig, logger: Logger): Promise; -export declare function openInBrowser(opts: { url: string }): Promise; - -export { DevServer, DevServerConfig, Logger }; diff --git a/src/dev-server/request-handler.ts b/src/dev-server/request-handler.ts index 94a2a723d46..6b067decca6 100644 --- a/src/dev-server/request-handler.ts +++ b/src/dev-server/request-handler.ts @@ -1,5 +1,6 @@ import type * as d from '../declarations'; -import { isDevClient, isDevModule, sendMsg } from './dev-server-utils'; +import type { IncomingMessage, ServerResponse } from 'http'; +import { isDevClient, isDevModule, sendLogRequest } from './dev-server-utils'; import { normalizePath } from '@utils'; import { serveDevClient } from './serve-dev-client'; import { serveFile } from './serve-file'; @@ -7,61 +8,52 @@ import { serve404, serve404Content } from './serve-404'; import { serve500 } from './serve-500'; import { serveCompilerRequest } from './serve-compiler-request'; import { serveDirectoryIndex } from './serve-directory-index'; -import * as http from 'http'; import path from 'path'; -import * as url from 'url'; -export function createRequestHandler(devServerConfig: d.DevServerConfig, sys: d.CompilerSystem) { - return async function (incomingReq: http.IncomingMessage, res: http.ServerResponse) { +export function createRequestHandler( + devServerConfig: d.DevServerConfig, + sys: d.CompilerSystem, + sendMsg: d.DevServerSendMessage, +) { + return async function (incomingReq: IncomingMessage, res: ServerResponse) { try { const req = normalizeHttpRequest(devServerConfig, incomingReq); - if (req.url === '') { + if (!req.url) { res.writeHead(302, { location: '/' }); - - if (devServerConfig.logRequests) { - sendMsg(process, { - requestLog: { - method: req.method, - url: req.url, - status: 302, - }, - }); - } - + sendLogRequest(devServerConfig, req, 302, sendMsg); return res.end(); } if (isDevClient(req.pathname) && devServerConfig.websocket) { - return serveDevClient(devServerConfig, sys, req, res); + return serveDevClient(devServerConfig, sys, req, res, sendMsg); } if (isDevModule(req.pathname)) { - return serveCompilerRequest(devServerConfig, req, res); + return serveCompilerRequest(devServerConfig, req, res, sendMsg); } if (!isValidUrlBasePath(devServerConfig.basePath, req.url)) { - if (devServerConfig.logRequests) { - sendMsg(process, { - requestLog: { - method: req.method, - url: req.url, - status: 404, - }, - }); - } - - return serve404Content(devServerConfig, req, res, `404 File Not Found, base path: ${devServerConfig.basePath}`, `invalid basePath`); + sendLogRequest(devServerConfig, req, 404, sendMsg); + + return serve404Content( + devServerConfig, + req, + res, + `404 File Not Found, base path: ${devServerConfig.basePath}`, + `invalid basePath`, + sendMsg, + ); } try { req.stats = await sys.stat(req.filePath); if (req.stats.isFile) { - return serveFile(devServerConfig, sys, req, res); + return serveFile(devServerConfig, sys, req, res, sendMsg); } if (req.stats.isDirectory) { - return serveDirectoryIndex(devServerConfig, sys, req, res); + return serveDirectoryIndex(devServerConfig, sys, req, res, sendMsg); } } catch (e) {} @@ -77,49 +69,62 @@ export function createRequestHandler(devServerConfig: d.DevServerConfig, sys: d. req.stats = await sys.stat(indexFilePath); if (req.stats.isFile) { req.filePath = indexFilePath; - return serveFile(devServerConfig, sys, req, res); + return serveFile(devServerConfig, sys, req, res, sendMsg); } } catch (e) { xSource.push(`notfound error: ${e}`); } } - return serve404(devServerConfig, req, res, xSource.join(', ')); + serve404(devServerConfig, req, res, xSource.join(', '), sendMsg); } catch (e) { - return serve500(devServerConfig, incomingReq as any, res, e, `not found error`); + serve500(devServerConfig, incomingReq as any, res, e, `not found error`, sendMsg); } }; } -export function isValidUrlBasePath(basePath: string, url: string) { +export function isValidUrlBasePath(basePath: string, url: URL) { // normalize the paths to always end with a slash for the check - if (!url.endsWith('/')) { - url += '/'; + if (!url.pathname.endsWith('/')) { + url.pathname += '/'; } if (!basePath.endsWith('/')) { basePath += '/'; } - return url.startsWith(basePath); + return url.pathname.startsWith(basePath); } -function normalizeHttpRequest(devServerConfig: d.DevServerConfig, incomingReq: http.IncomingMessage) { +function normalizeHttpRequest(devServerConfig: d.DevServerConfig, incomingReq: IncomingMessage) { const req: d.HttpRequest = { method: (incomingReq.method || 'GET').toUpperCase() as any, headers: incomingReq.headers as any, - acceptHeader: (incomingReq.headers && typeof incomingReq.headers.accept === 'string' && incomingReq.headers.accept) || '', - url: (incomingReq.url || '').trim() || '', + acceptHeader: + (incomingReq.headers && typeof incomingReq.headers.accept === 'string' && incomingReq.headers.accept) || '', host: (incomingReq.headers && typeof incomingReq.headers.host === 'string' && incomingReq.headers.host) || null, + url: null, + searchParams: null, }; - const parsedUrl = url.parse(req.url); - const parts = (parsedUrl.pathname || '').replace(/\\/g, '/').split('/'); - - req.pathname = parts.map(part => decodeURIComponent(part)).join('/'); - if (req.pathname.length > 0 && !isDevClient(req.pathname)) { - req.pathname = '/' + req.pathname.substring(devServerConfig.basePath.length); + const incomingUrl = (incomingReq.url || '').trim() || null; + if (incomingUrl) { + if (req.host) { + req.url = new URL(incomingReq.url, `http://${req.host}`); + } else { + req.url = new URL(incomingReq.url, `http://dev.stenciljs.com`); + } + req.searchParams = req.url.searchParams; } - req.filePath = normalizePath(path.normalize(path.join(devServerConfig.root, path.relative('/', req.pathname)))); + if (req.url) { + const parts = req.url.pathname.replace(/\\/g, '/').split('/'); + + req.pathname = parts.map(part => decodeURIComponent(part)).join('/'); + if (req.pathname.length > 0 && !isDevClient(req.pathname)) { + req.pathname = '/' + req.pathname.substring(devServerConfig.basePath.length); + } + + req.filePath = normalizePath(path.normalize(path.join(devServerConfig.root, path.relative('/', req.pathname)))); + } return req; } diff --git a/src/dev-server/serve-404.ts b/src/dev-server/serve-404.ts index c7e96362525..11a0d55f39a 100644 --- a/src/dev-server/serve-404.ts +++ b/src/dev-server/serve-404.ts @@ -1,12 +1,18 @@ import type * as d from '../declarations'; -import * as http from 'http'; +import type { ServerResponse } from 'http'; import fs from 'graceful-fs'; import path from 'path'; import util from 'util'; -import { responseHeaders, sendMsg } from './dev-server-utils'; +import { responseHeaders, sendLogRequest } from './dev-server-utils'; import { serve500 } from './serve-500'; -export async function serve404(devServerConfig: d.DevServerConfig, req: d.HttpRequest, res: http.ServerResponse, xSource: string) { +export function serve404( + devServerConfig: d.DevServerConfig, + req: d.HttpRequest, + res: ServerResponse, + xSource: string, + sendMsg: d.DevServerSendMessage, +) { try { if (req.pathname === '/favicon.ico') { try { @@ -33,29 +39,27 @@ export async function serve404(devServerConfig: d.DevServerConfig, req: d.HttpRe rs.pipe(res); return; } catch (e) { - serve500(devServerConfig, req, res, e, xSource); + serve500(devServerConfig, req, res, e, xSource, sendMsg); } } const content = ['404 File Not Found', 'Url: ' + req.pathname, 'File: ' + req.filePath].join('\n'); - serve404Content(devServerConfig, req, res, content, xSource); - - if (devServerConfig.logRequests) { - sendMsg(process, { - requestLog: { - method: req.method, - url: req.url, - status: 404, - }, - }); - } + serve404Content(devServerConfig, req, res, content, xSource, sendMsg); + sendLogRequest(devServerConfig, req, 400, sendMsg); } catch (e) { - serve500(devServerConfig, req, res, e, xSource); + serve500(devServerConfig, req, res, e, xSource, sendMsg); } } -export function serve404Content(devServerConfig: d.DevServerConfig, req: d.HttpRequest, res: http.ServerResponse, content: string, xSource: string) { +export function serve404Content( + devServerConfig: d.DevServerConfig, + req: d.HttpRequest, + res: ServerResponse, + content: string, + xSource: string, + sendMsg: d.DevServerSendMessage, +) { try { const headers = responseHeaders({ 'content-type': 'text/plain; charset=utf-8', @@ -66,6 +70,6 @@ export function serve404Content(devServerConfig: d.DevServerConfig, req: d.HttpR res.write(content); res.end(); } catch (e) { - serve500(devServerConfig, req, res, e, 'serve404Content: ' + xSource); + serve500(devServerConfig, req, res, e, 'serve404Content: ' + xSource, sendMsg); } } diff --git a/src/dev-server/serve-500.ts b/src/dev-server/serve-500.ts index 74379d110a1..5107e57a8ac 100644 --- a/src/dev-server/serve-500.ts +++ b/src/dev-server/serve-500.ts @@ -1,9 +1,16 @@ import type * as d from '../declarations'; -import * as http from 'http'; -import { responseHeaders, sendError, sendMsg } from './dev-server-utils'; +import type { ServerResponse } from 'http'; +import { responseHeaders, sendLogRequest } from './dev-server-utils'; import util from 'util'; -export function serve500(devServerConfig: d.DevServerConfig, req: d.HttpRequest, res: http.ServerResponse, error: any, xSource: string) { +export function serve500( + devServerConfig: d.DevServerConfig, + req: d.HttpRequest, + res: ServerResponse, + error: any, + xSource: string, + sendMsg: d.DevServerSendMessage, +) { try { res.writeHead( 500, @@ -16,16 +23,8 @@ export function serve500(devServerConfig: d.DevServerConfig, req: d.HttpRequest, res.write(util.inspect(error)); res.end(); - if (devServerConfig.logRequests) { - sendMsg(process, { - requestLog: { - method: req.method, - url: req.url, - status: 500, - }, - }); - } + sendLogRequest(devServerConfig, req, 500, sendMsg); } catch (e) { - sendError(process, 'serve500: ' + e); + sendMsg({ error: { message: 'serve500: ' + e } }); } } diff --git a/src/dev-server/serve-compiler-request.ts b/src/dev-server/serve-compiler-request.ts index c8e53fbc651..fb71f752c4f 100644 --- a/src/dev-server/serve-compiler-request.ts +++ b/src/dev-server/serve-compiler-request.ts @@ -1,36 +1,58 @@ import type * as d from '../declarations'; -import * as util from './dev-server-utils'; -import { serve404 } from './serve-404'; +import type { ServerResponse } from 'http'; +import { responseHeaders, sendLogRequest } from './dev-server-utils'; import { serve500 } from './serve-500'; -import * as http from 'http'; -export async function serveCompilerRequest(devServerConfig: d.DevServerConfig, req: d.HttpRequest, res: http.ServerResponse) { - try { - const msgResults = await util.sendMsgWithResponse(process, { - compilerRequestPath: req.url, - }); +const compilerRequests = new Map(); + +export function serveCompilerRequest( + devServerConfig: d.DevServerConfig, + req: d.HttpRequest, + res: ServerResponse, + sendMsg: d.DevServerSendMessage, +) { + const tmr = setTimeout(() => { + serve500(devServerConfig, req, res, 'Timeout exceeded for dev module', 'serveCompilerRequest', sendMsg); + }, 15000); - const results = msgResults.compilerRequestResults; + compilerRequests.set(req.pathname, { + req, + res, + tmr, + }); + sendMsg({ + compilerRequestPath: req.pathname, + }); +} + +export function serveCompilerResponse( + devServerConfig: d.DevServerConfig, + compilerRequestResponse: d.CompilerRequestResponse, + sendMsg: d.DevServerSendMessage, +) { + try { + const data = compilerRequests.get(compilerRequestResponse.path); + if (data) { + compilerRequests.delete(compilerRequestResponse.path); + clearTimeout(data.tmr); - if (results) { const headers = { 'content-type': 'application/javascript; charset=utf-8', - 'content-length': Buffer.byteLength(results.content, 'utf8'), - 'x-dev-node-module-id': results.nodeModuleId, - 'x-dev-node-module-version': results.nodeModuleVersion, - 'x-dev-node-module-resolved-path': results.nodeResolvedPath, - 'x-dev-node-module-cache-path': results.cachePath, - 'x-dev-node-module-cache-hit': results.cacheHit, + 'content-length': Buffer.byteLength(compilerRequestResponse.content, 'utf8'), + 'x-dev-node-module-id': compilerRequestResponse.nodeModuleId, + 'x-dev-node-module-version': compilerRequestResponse.nodeModuleVersion, + 'x-dev-node-module-resolved-path': compilerRequestResponse.nodeResolvedPath, + 'x-dev-node-module-cache-path': compilerRequestResponse.cachePath, + 'x-dev-node-module-cache-hit': compilerRequestResponse.cacheHit, }; - res.writeHead(results.status, util.responseHeaders(headers)); - res.write(results.content); - res.end(); - return; - } + data.res.writeHead(compilerRequestResponse.status, responseHeaders(headers)); + data.res.write(compilerRequestResponse.content); + data.res.end(); - return serve404(devServerConfig, req, res, 'serveCompilerRequest'); + sendLogRequest(devServerConfig, data.req, compilerRequestResponse.status, sendMsg); + } } catch (e) { - return serve500(devServerConfig, req, res, e, 'serveCompilerRequest'); + sendMsg({ error: { message: 'serveCompilerResponse: ' + e } }); } } diff --git a/src/dev-server/serve-dev-client.ts b/src/dev-server/serve-dev-client.ts index 23b4a3c2d36..6b5a6eec7e7 100644 --- a/src/dev-server/serve-dev-client.ts +++ b/src/dev-server/serve-dev-client.ts @@ -1,70 +1,87 @@ import type * as d from '../declarations'; -import * as c from './dev-server-constants'; -import * as util from './dev-server-utils'; +import type { ServerResponse } from 'http'; +import { DEV_SERVER_URL } from './dev-server-constants'; +import { isDevServerClient, isInitialDevServerLoad, isOpenInEditor, responseHeaders } from './dev-server-utils'; import { serve404 } from './serve-404'; import { serve500 } from './serve-500'; import { serveFile } from './serve-file'; -import { serveOpenInEditor } from './open-in-editor'; -import * as http from 'http'; +import { serveOpenInEditor, getEditors } from './open-in-editor'; import path from 'path'; -export async function serveDevClient(devServerConfig: d.DevServerConfig, sys: d.CompilerSystem, req: d.HttpRequest, res: http.ServerResponse) { +export async function serveDevClient( + devServerConfig: d.DevServerConfig, + sys: d.CompilerSystem, + req: d.HttpRequest, + res: ServerResponse, + sendMsg: d.DevServerSendMessage, +) { try { - if (util.isOpenInEditor(req.pathname)) { - return serveOpenInEditor(devServerConfig, sys, req, res); + if (isOpenInEditor(req.pathname)) { + return serveOpenInEditor(devServerConfig, sys, req, res, sendMsg); } - if (util.isDevServerClient(req.pathname)) { - return serveDevClientScript(devServerConfig, sys, req, res); + if (isDevServerClient(req.pathname)) { + return serveDevClientScript(devServerConfig, sys, req, res, sendMsg); } - if (util.isInitialDevServerLoad(req.pathname)) { + if (isInitialDevServerLoad(req.pathname)) { req.filePath = path.join(devServerConfig.devServerDir, 'templates', 'initial-load.html'); } else { - const staticFile = req.pathname.replace(c.DEV_SERVER_URL + '/', ''); + const staticFile = req.pathname.replace(DEV_SERVER_URL + '/', ''); req.filePath = path.join(devServerConfig.devServerDir, 'static', staticFile); } try { req.stats = await sys.stat(req.filePath); if (req.stats.isFile) { - return serveFile(devServerConfig, sys, req, res); + return serveFile(devServerConfig, sys, req, res, sendMsg); } - return serve404(devServerConfig, req, res, 'serveDevClient no stats'); + return serve404(devServerConfig, req, res, 'serveDevClient not file', sendMsg); } catch (e) { - return serve404(devServerConfig, req, res, `serveDevClient stats error ${e}`); + return serve404(devServerConfig, req, res, `serveDevClient stats error ${e}`, sendMsg); } } catch (e) { - return serve500(devServerConfig, req, res, e, 'serveDevClient'); + return serve500(devServerConfig, req, res, e, 'serveDevClient', sendMsg); } } -async function serveDevClientScript(devServerConfig: d.DevServerConfig, sys: d.CompilerSystem, req: d.HttpRequest, res: http.ServerResponse) { - try { - const filePath = path.join(devServerConfig.devServerDir, 'connector.html'); +let connectorHtml: string = null; - let content = await sys.readFile(filePath, 'utf8'); - if (typeof content === 'string') { - const devClientConfig: d.DevClientConfig = { - basePath: devServerConfig.basePath, - editors: devServerConfig.editors, - reloadStrategy: devServerConfig.reloadStrategy, - }; +async function serveDevClientScript( + devServerConfig: d.DevServerConfig, + sys: d.CompilerSystem, + req: d.HttpRequest, + res: ServerResponse, + sendMsg: d.DevServerSendMessage, +) { + try { + if (connectorHtml == null) { + const filePath = path.join(devServerConfig.devServerDir, 'connector.html'); - content = content.replace('window.__DEV_CLIENT_CONFIG__', JSON.stringify(devClientConfig)); + connectorHtml = await sys.readFile(filePath, 'utf8'); + if (typeof connectorHtml === 'string') { + const devClientConfig: d.DevClientConfig = { + basePath: devServerConfig.basePath, + editors: await getEditors(), + reloadStrategy: devServerConfig.reloadStrategy, + }; - res.writeHead( - 200, - util.responseHeaders({ - 'content-type': 'text/html; charset=utf-8', - }), - ); - res.write(content); - res.end(); - } else { - serve404(devServerConfig, req, res, 'serveDevClientScript'); + connectorHtml = connectorHtml.replace('window.__DEV_CLIENT_CONFIG__', JSON.stringify(devClientConfig)); + } else { + serve404(devServerConfig, req, res, 'serveDevClientScript', sendMsg); + return; + } } + + res.writeHead( + 200, + responseHeaders({ + 'content-type': 'text/html; charset=utf-8', + }), + ); + res.write(connectorHtml); + res.end(); } catch (e) { - serve500(devServerConfig, req, res, e, 'serveDevClientScript'); + serve500(devServerConfig, req, res, e, 'serveDevClientScript', sendMsg); } } diff --git a/src/dev-server/serve-directory-index.ts b/src/dev-server/serve-directory-index.ts index 68af5d9dbf9..36ef462919b 100644 --- a/src/dev-server/serve-directory-index.ts +++ b/src/dev-server/serve-directory-index.ts @@ -1,36 +1,32 @@ import type * as d from '../declarations'; -import { responseHeaders, sendMsg } from './dev-server-utils'; +import type { ServerResponse } from 'http'; +import { responseHeaders, sendLogRequest } from './dev-server-utils'; import { serve404 } from './serve-404'; import { serve500 } from './serve-500'; import { serveFile } from './serve-file'; -import * as http from 'http'; import path from 'path'; -import * as url from 'url'; let dirTemplate: string = null; -export async function serveDirectoryIndex(devServerConfig: d.DevServerConfig, sys: d.CompilerSystem, req: d.HttpRequest, res: http.ServerResponse) { +export async function serveDirectoryIndex( + devServerConfig: d.DevServerConfig, + sys: d.CompilerSystem, + req: d.HttpRequest, + res: ServerResponse, + sendMsg: d.DevServerSendMessage, +) { try { const indexFilePath = path.join(req.filePath, 'index.html'); req.stats = await sys.stat(indexFilePath); if (req.stats.isFile) { req.filePath = indexFilePath; - return serveFile(devServerConfig, sys, req, res); + return serveFile(devServerConfig, sys, req, res, sendMsg); } } catch (e) {} if (!req.pathname.endsWith('/')) { - if (devServerConfig.logRequests) { - sendMsg(process, { - requestLog: { - method: req.method, - url: req.url, - status: 302, - }, - }); - } - + sendLogRequest(devServerConfig, req, 302, sendMsg); res.writeHead(302, { location: req.pathname + '/', }); @@ -43,11 +39,16 @@ export async function serveDirectoryIndex(devServerConfig: d.DevServerConfig, sy try { if (dirTemplate == null) { const dirTemplatePath = path.join(devServerConfig.devServerDir, 'templates', 'directory-index.html'); - dirTemplate = await sys.readFile(dirTemplatePath, 'utf8'); + dirTemplate = sys.readFileSync(dirTemplatePath); } - const files = await getFiles(sys, req.pathname, dirFilePaths); + const files = await getFiles(sys, req.url, dirFilePaths); + + const templateHtml = (await dirTemplate) + .replace('{{title}}', getTitle(req.pathname)) + .replace('{{nav}}', getName(req.pathname)) + .replace('{{files}}', files); - const templateHtml = dirTemplate.replace('{{title}}', getTitle(req.pathname)).replace('{{nav}}', getName(req.pathname)).replace('{{files}}', files); + sendLogRequest(devServerConfig, req, 200, sendMsg); res.writeHead( 200, @@ -59,28 +60,18 @@ export async function serveDirectoryIndex(devServerConfig: d.DevServerConfig, sy res.write(templateHtml); res.end(); - - if (devServerConfig.logRequests) { - sendMsg(process, { - requestLog: { - method: req.method, - url: req.url, - status: 200, - }, - }); - } } catch (e) { - serve500(devServerConfig, req, res, e, 'serveDirectoryIndex'); + serve500(devServerConfig, req, res, e, 'serveDirectoryIndex', sendMsg); } } catch (e) { - serve404(devServerConfig, req, res, 'serveDirectoryIndex'); + serve404(devServerConfig, req, res, 'serveDirectoryIndex', sendMsg); } } -async function getFiles(sys: d.CompilerSystem, urlPathName: string, dirItemNames: string[]) { - const items = await getDirectoryItems(sys, urlPathName, dirItemNames); +async function getFiles(sys: d.CompilerSystem, baseUrl: URL, dirItemNames: string[]) { + const items = await getDirectoryItems(sys, baseUrl, dirItemNames); - if (urlPathName !== '/') { + if (baseUrl.pathname !== '/') { items.unshift({ isDirectory: true, pathname: '../', @@ -101,15 +92,16 @@ async function getFiles(sys: d.CompilerSystem, urlPathName: string, dirItemNames .join(''); } -async function getDirectoryItems(sys: d.CompilerSystem, urlPathName: string, dirFilePaths: string[]) { +async function getDirectoryItems(sys: d.CompilerSystem, baseUrl: URL, dirFilePaths: string[]) { const items = await Promise.all( dirFilePaths.map(async dirFilePath => { const fileName = path.basename(dirFilePath); + const url = new URL(fileName, baseUrl); const stats = await sys.stat(dirFilePath); const item: DirectoryItem = { name: fileName, - pathname: url.resolve(urlPathName, fileName), + pathname: url.pathname, isDirectory: stats.isDirectory, }; diff --git a/src/dev-server/serve-file.ts b/src/dev-server/serve-file.ts index ad303e978a8..260ac2afa37 100644 --- a/src/dev-server/serve-file.ts +++ b/src/dev-server/serve-file.ts @@ -1,16 +1,20 @@ import type * as d from '../declarations'; +import type { ServerResponse } from 'http'; import * as util from './dev-server-utils'; import { version } from '../version'; import { serve500 } from './serve-500'; -import * as http from 'http'; import path from 'path'; import fs from 'graceful-fs'; -import * as querystring from 'querystring'; -import * as Url from 'url'; import * as zlib from 'zlib'; import { Buffer } from 'buffer'; -export async function serveFile(devServerConfig: d.DevServerConfig, sys: d.CompilerSystem, req: d.HttpRequest, res: http.ServerResponse) { +export async function serveFile( + devServerConfig: d.DevServerConfig, + sys: d.CompilerSystem, + req: d.HttpRequest, + res: ServerResponse, + sendMsg: d.DevServerSendMessage, +) { try { if (util.isSimpleText(req.filePath)) { // easy text file, use the internal cache @@ -28,7 +32,7 @@ export async function serveFile(devServerConfig: d.DevServerConfig, sys: d.Compi res.writeHead( 200, util.responseHeaders({ - 'content-type': util.getContentType(devServerConfig, req.filePath) + '; charset=utf-8', + 'content-type': util.getContentType(req.filePath) + '; charset=utf-8', 'content-encoding': 'gzip', 'vary': 'Accept-Encoding', }), @@ -42,7 +46,7 @@ export async function serveFile(devServerConfig: d.DevServerConfig, sys: d.Compi res.writeHead( 200, util.responseHeaders({ - 'content-type': util.getContentType(devServerConfig, req.filePath) + '; charset=utf-8', + 'content-type': util.getContentType(req.filePath) + '; charset=utf-8', 'content-length': Buffer.byteLength(content, 'utf8'), }), ); @@ -55,33 +59,22 @@ export async function serveFile(devServerConfig: d.DevServerConfig, sys: d.Compi res.writeHead( 200, util.responseHeaders({ - 'content-type': util.getContentType(devServerConfig, req.filePath), + 'content-type': util.getContentType(req.filePath), 'content-length': req.stats.size, }), ); fs.createReadStream(req.filePath).pipe(res); } - if (devServerConfig.logRequests) { - util.sendMsg(process, { - requestLog: { - method: req.method, - url: req.url, - status: 200, - }, - }); - } + util.sendLogRequest(devServerConfig, req, 200, sendMsg); } catch (e) { - serve500(devServerConfig, req, res, e, 'serveFile'); + serve500(devServerConfig, req, res, e, 'serveFile', sendMsg); } } -function updateStyleUrls(cssUrl: string, oldCss: string) { - const parsedUrl = Url.parse(cssUrl); - const qs = querystring.parse(parsedUrl.query); - - const versionId = qs['s-hmr']; - const hmrUrls = qs['s-hmr-urls']; +function updateStyleUrls(url: URL, oldCss: string) { + const versionId = url.searchParams.get('s-hmr'); + const hmrUrls = url.searchParams.get('s-hmr-urls'); if (versionId && hmrUrls) { (hmrUrls as string).split(',').forEach(hmrUrl => { @@ -96,7 +89,7 @@ function updateStyleUrls(cssUrl: string, oldCss: string) { while ((result = reg.exec(oldCss)) !== null) { const oldUrl = result[2]; - const parsedUrl = Url.parse(oldUrl); + const parsedUrl = new URL(oldUrl, url); const fileName = path.basename(parsedUrl.pathname); const versionId = urlVersionIds.get(fileName); @@ -104,14 +97,9 @@ function updateStyleUrls(cssUrl: string, oldCss: string) { continue; } - const qs = querystring.parse(parsedUrl.query); - qs['s-hmr'] = versionId; - - parsedUrl.search = querystring.stringify(qs); - - const newUrl = Url.format(parsedUrl); + parsedUrl.searchParams.set('s-hmr', versionId); - newCss = newCss.replace(oldUrl, newUrl); + newCss = newCss.replace(oldUrl, parsedUrl.pathname); } return newCss; @@ -120,7 +108,11 @@ function updateStyleUrls(cssUrl: string, oldCss: string) { const urlVersionIds = new Map(); function appendDevServerClientScript(devServerConfig: d.DevServerConfig, req: d.HttpRequest, content: string) { - const devServerClientUrl = util.getDevServerClientUrl(devServerConfig, req.headers?.['x-forwarded-host'] ?? req.host, req.headers?.['x-forwarded-proto']); + const devServerClientUrl = util.getDevServerClientUrl( + devServerConfig, + req.headers?.['x-forwarded-host'] ?? req.host, + req.headers?.['x-forwarded-proto'], + ); const iframe = ``; return appendDevServerClientIframe(content, iframe); } diff --git a/src/dev-server/server-http.ts b/src/dev-server/server-http.ts index be00a1af42a..10addd9dbd9 100644 --- a/src/dev-server/server-http.ts +++ b/src/dev-server/server-http.ts @@ -1,26 +1,51 @@ import type * as d from '../declarations'; import { createRequestHandler } from './request-handler'; -import { findClosestOpenPort } from './find-closest-port'; import * as http from 'http'; import * as https from 'https'; +import * as net from 'net'; -export async function createHttpServer(devServerConfig: d.DevServerConfig, sys: d.CompilerSystem, destroys: d.DevServerDestroy[]) { - // figure out the port to be listening on - // by figuring out the first one available - devServerConfig.port = await findClosestOpenPort(devServerConfig.address, devServerConfig.port); - +export function createHttpServer( + devServerConfig: d.DevServerConfig, + sys: d.CompilerSystem, + sendMsg: d.DevServerSendMessage, +) { // create our request handler - const reqHandler = createRequestHandler(devServerConfig, sys); + const reqHandler = createRequestHandler(devServerConfig, sys, sendMsg); const credentials = devServerConfig.https; - let server = credentials ? https.createServer(credentials, reqHandler) : http.createServer(reqHandler); + return credentials ? https.createServer(credentials, reqHandler) : http.createServer(reqHandler); +} - destroys.push(() => { - // close down the serve on destroy - server.close(); - server = null; - }); +export async function findClosestOpenPort(host: string, port: number): Promise { + async function t(portToCheck: number): Promise { + const isTaken = await isPortTaken(host, portToCheck); + if (!isTaken) { + return portToCheck; + } + return t(portToCheck + 1); + } - return server; + return t(port); +} + +function isPortTaken(host: string, port: number): Promise { + return new Promise((resolve, reject) => { + const tester = net + .createServer() + .once('error', () => { + resolve(true); + }) + .once('listening', () => { + tester + .once('close', () => { + resolve(false); + }) + .close(); + }) + .on('error', (err: any) => { + reject(err); + }) + .listen(port, host); + }); } diff --git a/src/dev-server/server-process.ts b/src/dev-server/server-process.ts new file mode 100644 index 00000000000..c475088fa4b --- /dev/null +++ b/src/dev-server/server-process.ts @@ -0,0 +1,98 @@ +import type * as d from '../declarations'; +import type { Server } from 'http'; +import { createHttpServer, findClosestOpenPort } from './server-http'; +import { createNodeSys } from '@sys-api-node'; +import { createWebSocket, DevWebSocket } from './server-web-socket'; +import { DEV_SERVER_INIT_URL } from './dev-server-constants'; +import { getBrowserUrl } from './dev-server-utils'; +import { normalizePath } from '@utils'; +import { openInBrowser } from './open-in-browser'; +import { serveCompilerResponse } from './serve-compiler-request'; + +export function initServerProcess(sendMsg: (msg: d.DevServerMessage) => void) { + let devServerConfig: d.DevServerConfig = null; + let server: Server = null; + let webSocket: DevWebSocket = null; + + let sys = createNodeSys({ process }); + + const startServer = async (msg: d.DevServerMessage) => { + devServerConfig = msg.startServer; + devServerConfig.port = await findClosestOpenPort(devServerConfig.address, devServerConfig.port); + devServerConfig.browserUrl = getBrowserUrl( + devServerConfig.protocol, + devServerConfig.address, + devServerConfig.port, + devServerConfig.basePath, + '/', + ); + devServerConfig.root = normalizePath(devServerConfig.root); + + server = createHttpServer(devServerConfig, sys, sendMsg); + + webSocket = devServerConfig.websocket ? createWebSocket(server, sendMsg) : null; + + server.listen(devServerConfig.port, devServerConfig.address); + + if (devServerConfig.openBrowser) { + const initialLoadUrl = getBrowserUrl( + devServerConfig.protocol, + devServerConfig.address, + devServerConfig.port, + devServerConfig.basePath, + devServerConfig.initialLoadUrl || DEV_SERVER_INIT_URL, + ); + openInBrowser({ url: initialLoadUrl }); + } + + sendMsg({ serverStarted: devServerConfig }); + }; + + const closeServer = () => { + const promises: Promise[] = []; + devServerConfig = null; + if (sys) { + promises.push(sys.destroy()); + sys = null; + } + if (webSocket) { + promises.push(webSocket.close()); + webSocket = null; + } + if (server) { + promises.push( + new Promise(resolve => { + server.close(resolve); + }), + ); + server = null; + } + Promise.all(promises).finally(() => { + sendMsg({ + serverClosed: true, + }); + }); + }; + + const receiveMessage = (msg: d.DevServerMessage) => { + try { + if (msg.startServer) { + startServer(msg); + } else if (msg.closeServer) { + closeServer(); + } else if (msg.compilerRequestResults) { + serveCompilerResponse(devServerConfig, msg.compilerRequestResults, sendMsg); + } else { + if (webSocket && devServerConfig) { + webSocket.sendToBrowser(msg); + } + } + } catch (e) { + sendMsg({ + error: { message: e + '', stack: e?.stack ? e.stack : null }, + }); + } + }; + + return receiveMessage; +} diff --git a/src/dev-server/server-web-socket.ts b/src/dev-server/server-web-socket.ts index 34a9f7164d7..38014c61952 100644 --- a/src/dev-server/server-web-socket.ts +++ b/src/dev-server/server-web-socket.ts @@ -1,9 +1,12 @@ import type * as d from '../declarations'; +import type { Server } from 'http'; import * as ws from 'ws'; -import * as http from 'http'; import { noop } from '@utils'; -export function createWebSocket(prcs: NodeJS.Process, httpServer: http.Server, destroys: d.DevServerDestroy[]) { +export function createWebSocket( + httpServer: Server, + onMessageFromClient: (msg: d.DevServerMessage) => void, +): DevWebSocket { const wsConfig: ws.ServerOptions = { server: httpServer, }; @@ -18,7 +21,11 @@ export function createWebSocket(prcs: NodeJS.Process, httpServer: http.Server, d ws.on('message', data => { // the server process has received a message from the browser // pass the message received from the browser to the main cli process - prcs.send(JSON.parse(data.toString())); + try { + onMessageFromClient(JSON.parse(data.toString())); + } catch (e) { + console.error(e); + } }); ws.isAlive = true; @@ -31,34 +38,43 @@ export function createWebSocket(prcs: NodeJS.Process, httpServer: http.Server, d if (!ws.isAlive) { return ws.close(1000); } - ws.isAlive = false; ws.ping(noop); }); }, 10000); - function onMessageFromCli(msg: d.DevServerMessage) { - // the server process has received a message from the cli's main thread - // pass the data to each web socket for each browser/tab connected - if (msg) { - const data = JSON.stringify(msg); - wsServer.clients.forEach(ws => { - if (ws.readyState === ws.OPEN) { - ws.send(data); - } + return { + sendToBrowser: (msg: d.DevServerMessage) => { + if (msg && wsServer && wsServer.clients) { + const data = JSON.stringify(msg); + wsServer.clients.forEach(ws => { + if (ws.readyState === ws.OPEN) { + ws.send(data); + } + }); + } + }, + close: () => { + return new Promise((resolve, reject) => { + clearInterval(pingInternval); + wsServer.clients.forEach(ws => { + ws.close(1000); + }); + wsServer.close(err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); }); - } - } - - prcs.addListener('message', onMessageFromCli); - - destroys.push(() => { - clearInterval(pingInternval); + }, + }; +} - wsServer.clients.forEach(ws => { - ws.close(1000); - }); - }); +export interface DevWebSocket { + sendToBrowser: (msg: d.DevServerMessage) => void; + close: () => Promise; } interface DevWS extends ws { diff --git a/src/dev-server/server-worker-main.ts b/src/dev-server/server-worker-main.ts new file mode 100644 index 00000000000..d416641ff19 --- /dev/null +++ b/src/dev-server/server-worker-main.ts @@ -0,0 +1,48 @@ +import type * as d from '../declarations'; +import { fork } from 'child_process'; +import path from 'path'; + +export function initServerProcessWorkerProxy(sendToMain: (msg: d.DevServerMessage) => void) { + const workerPath = require.resolve(path.join(__dirname, 'server-worker-thread.js')); + + const filteredExecArgs = process.execArgv.filter(v => !/^--(debug|inspect)/.test(v)); + + const forkOpts: any = { + execArgv: filteredExecArgs, + env: process.env, + cwd: process.cwd(), + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + }; + + // start a new child process of the CLI process + // for the http and web socket server + let serverProcess = fork(workerPath, [], forkOpts); + + const receiveFromMain = (msg: d.DevServerMessage) => { + // get a message from main to send to the worker + if (serverProcess) { + serverProcess.send(msg); + } + }; + + // get a message from the worker and send it to main + serverProcess.on('message', (msg: d.DevServerMessage) => { + if (msg.serverClosed && serverProcess) { + serverProcess.kill(); + serverProcess = null; + } + sendToMain(msg); + }); + + serverProcess.stdout.on('data', (data: any) => { + // the child server process has console logged data + console.log(`dev server: ${data}`); + }); + + serverProcess.stderr.on('data', (data: any) => { + // the child server process has console logged an error + sendToMain({ error: { message: 'stderr: ' + data } }); + }); + + return receiveFromMain; +} diff --git a/src/dev-server/server-worker-thread.js b/src/dev-server/server-worker-thread.js new file mode 100644 index 00000000000..453c82a5928 --- /dev/null +++ b/src/dev-server/server-worker-thread.js @@ -0,0 +1,10 @@ +const { initServerProcess } = require('./server-process.js'); +const receiveMessageFromMain = initServerProcess(msg => { + process.send(msg); +}); +process.on('message', receiveMessageFromMain); +process.on('unhandledRejection', e => { + process.send({ + error: { message: 'unhandledRejection: ' + e, stack: typeof e.stack === 'string' ? e.stack : null }, + }); +}); diff --git a/src/dev-server/server-worker.ts b/src/dev-server/server-worker.ts deleted file mode 100644 index 0bc1b0a7648..00000000000 --- a/src/dev-server/server-worker.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type * as d from '../declarations'; -import { createMessageReceiver, sendMsg } from './dev-server-utils'; -import { startDevServerWorker } from './start-server-worker'; -import fs from 'graceful-fs'; -import path from 'path'; -import util from 'util'; - -async function startServer(devServerConfig: d.DevServerConfig) { - // received a message from main to start the server - try { - devServerConfig.contentTypes = await loadContentTypes(devServerConfig); - startDevServerWorker(process, devServerConfig); - } catch (e) { - sendMsg(process, { - serverStarted: { - address: null, - basePath: null, - browserUrl: null, - initialLoadUrl: null, - port: null, - protocol: null, - root: null, - error: String(e), - }, - }); - } -} - -async function loadContentTypes(devServerConfig: d.DevServerConfig) { - const contentTypePath = path.join(devServerConfig.devServerDir, 'content-type-db.json'); - const readFile = util.promisify(fs.readFile); - const contentTypeJson = await readFile(contentTypePath, 'utf8'); - return JSON.parse(contentTypeJson); -} - -createMessageReceiver(process, (msg: d.DevServerMessage) => { - if (msg.startServer) { - startServer(msg.startServer); - } -}); - -process.on('unhandledRejection', (e: any) => { - console.log('server worker error', e); -}); diff --git a/src/dev-server/start-server-worker.ts b/src/dev-server/start-server-worker.ts deleted file mode 100644 index deb2353ea9a..00000000000 --- a/src/dev-server/start-server-worker.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type * as d from '../declarations'; -import { createHttpServer } from './server-http'; -import { createNodeSys } from '../sys/node/node-sys'; -import { createWebSocket } from './server-web-socket'; -import { DEV_SERVER_INIT_URL } from './dev-server-constants'; -import { getBrowserUrl, sendError, sendMsg } from './dev-server-utils'; -import { getEditors } from './open-in-editor'; -import exit from 'exit'; - -export async function startDevServerWorker(prcs: NodeJS.Process, devServerConfig: d.DevServerConfig) { - let hasStarted = false; - - try { - const sys = createNodeSys({ process: prcs }); - const destroys: d.DevServerDestroy[] = []; - devServerConfig.editors = await getEditors(); - - // create the http server listening for and responding to requests from the browser - let httpServer = await createHttpServer(devServerConfig, sys, destroys); - - if (devServerConfig.websocket) { - // upgrade web socket requests the server receives - createWebSocket(prcs, httpServer, destroys); - } - - // start listening! - httpServer.listen(devServerConfig.port, devServerConfig.address); - - // have the server worker send a message to the main cli - // process that the server has successfully started up - sendMsg(prcs, { - serverStarted: { - address: devServerConfig.address, - basePath: devServerConfig.basePath, - browserUrl: getBrowserUrl(devServerConfig.protocol, devServerConfig.address, devServerConfig.port, devServerConfig.basePath, '/'), - port: devServerConfig.port, - protocol: devServerConfig.protocol, - root: devServerConfig.root, - initialLoadUrl: getBrowserUrl( - devServerConfig.protocol, - devServerConfig.address, - devServerConfig.port, - devServerConfig.basePath, - devServerConfig.initialLoadUrl || DEV_SERVER_INIT_URL, - ), - error: null, - }, - }); - hasStarted = true; - - const closeServer = () => { - // probably recived a SIGINT message from the parent cli process - // let's do our best to gracefully close everything down first - destroys.forEach(destroy => { - destroy(); - }); - - destroys.length = 0; - httpServer = null; - - setTimeout(() => { - exit(0); - }, 5000).unref(); - - prcs.removeAllListeners('message'); - }; - - prcs.once('SIGINT', closeServer); - } catch (e) { - if (!hasStarted) { - sendMsg(prcs, { - serverStarted: { - address: null, - basePath: null, - browserUrl: null, - initialLoadUrl: null, - port: null, - protocol: null, - root: null, - error: String(e), - }, - }); - } else { - sendError(prcs, e); - } - } -} diff --git a/test/hello-vdom/package.json b/test/hello-vdom/package.json index 01d18529975..1cf6c70ad4c 100644 --- a/test/hello-vdom/package.json +++ b/test/hello-vdom/package.json @@ -7,7 +7,7 @@ "collection": "./dist/collection/collection-manifest.json", "scripts": { "build": "node ../../bin/stencil build", - "start": "node ../../bin/stencil build --dev --watch --serve", + "start": "node ../../bin/stencil build --dev --watch --serve --debug", "start.prod": "node ../../bin/stencil build --watch --serve" } } \ No newline at end of file diff --git a/test/hello-vdom/src/index.ts b/test/hello-vdom/src/index.ts index 742094d5a2f..7531c10b867 100644 --- a/test/hello-vdom/src/index.ts +++ b/test/hello-vdom/src/index.ts @@ -1 +1 @@ -export * from './components/index'; +export { Components, JSX } from './components'; diff --git a/tsconfig.json b/tsconfig.json index b237638ded8..c2f0fa18fbf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,6 +28,7 @@ "@platform": ["src/client/index.ts"], "@runtime": ["src/runtime/index.ts"], "@deno-node-compat": ["src/sys/deno/deno-node-compat.ts"], + "@dev-server-process": ["src/dev-server/server-process.ts"], "@sys-api-deno": ["src/sys/deno/index.ts"], "@sys-api-node": ["src/sys/node/index.ts"], "@stencil/core/compiler": ["src/compiler/index.ts"], @@ -55,8 +56,6 @@ "src/compiler/public.ts", "src/compiler/sys/modules/index.ts", "src/dev-server/index.ts", - "src/dev-server/public.ts", - "src/dev-server/server-worker.ts", "src/dev-server/client/index.ts", "src/dev-server/dev-server-client/index.ts", "src/hydrate/platform/index.ts",