Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
adamdbradley committed Aug 20, 2020
1 parent 52c3485 commit cf335e3
Show file tree
Hide file tree
Showing 34 changed files with 996 additions and 859 deletions.
138 changes: 65 additions & 73 deletions scripts/bundles/dev-server.ts
Expand Up @@ -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
Expand All @@ -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,
},
Expand Down Expand Up @@ -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 = `<!doctype html><html><head><meta charset="utf-8"><style>body{background:black;color:white;font:18px monospace;text-align:center}</style></head><body>
const banner = `<!doctype html><html><head><meta charset="utf-8"><title>Stencil Dev Server Connector __VERSION:STENCIL__ &#9889</title><style>body{background:black;color:white;font:18px monospace;text-align:center}</style></head><body>
Stencil Dev Server Connector __VERSION:STENCIL__ &#9889;
Expand All @@ -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__, {});
`;

Expand Down
1 change: 1 addition & 0 deletions scripts/bundles/plugins/alias-plugin.ts
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions 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)}`;
}
4 changes: 2 additions & 2 deletions scripts/test/validate-build.ts
Expand Up @@ -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',
Expand Down
20 changes: 17 additions & 3 deletions src/compiler/bundle/dev-module.ts
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
21 changes: 9 additions & 12 deletions src/compiler/config/validate-dev-server.ts
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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)) {
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/config/validate-workers.ts
Expand Up @@ -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;
}
};

0 comments on commit cf335e3

Please sign in to comment.