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",