fix: dynamic web ports
This commit is contained in:
parent
31fd7b46a4
commit
1b13374b10
|
@ -37,8 +37,7 @@ export const hooks = {
|
|||
await createWindow(flecks);
|
||||
},
|
||||
'@flecks/electron/server.window': async (win, flecks) => {
|
||||
const {public: $$public} = flecks.get('@flecks/web');
|
||||
const {installExtensions, url = `http://${$$public}`} = flecks.get('@flecks/electron');
|
||||
const {installExtensions, url} = flecks.get('@flecks/electron');
|
||||
if (installExtensions) {
|
||||
const installer = __non_webpack_require__('electron-devtools-installer');
|
||||
const {default: installExtension} = installer;
|
||||
|
@ -48,7 +47,17 @@ export const hooks = {
|
|||
.flat(),
|
||||
]);
|
||||
}
|
||||
await win.loadURL(url);
|
||||
let realUrl = url;
|
||||
if (!realUrl) {
|
||||
realUrl = `http://${flecks.web.public}`;
|
||||
while (!flecks.web.server) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 50);
|
||||
});
|
||||
}
|
||||
}
|
||||
await win.loadURL(realUrl);
|
||||
},
|
||||
'@flecks/repl.context': (flecks) => ({
|
||||
electron: {
|
||||
|
|
|
@ -5,7 +5,17 @@ import {createBrowser, connectPage} from './connect';
|
|||
export function withWeb(task, options) {
|
||||
return async function withWeb() {
|
||||
const optionsWithTask = {...options, task: this};
|
||||
const server = await startServer(optionsWithTask);
|
||||
const server = await startServer({
|
||||
...optionsWithTask,
|
||||
opts: {
|
||||
...optionsWithTask.opts,
|
||||
env: {
|
||||
...optionsWithTask.opts?.env,
|
||||
FLECKS_ENV__flecks_web__port: '0',
|
||||
FLECKS_ENV__flecks_web__devPort: '0',
|
||||
},
|
||||
},
|
||||
});
|
||||
const socket = await server.waitForSocket(optionsWithTask);
|
||||
if (options.beforeConnect) {
|
||||
await options.beforeConnect({server, socket});
|
||||
|
@ -13,7 +23,7 @@ export function withWeb(task, options) {
|
|||
const start = Date.now();
|
||||
const previousTimeout = this.timeout();
|
||||
this.timeout(0);
|
||||
const {payload: config} = await socket.send({type: 'config.get', payload: '@flecks/web'});
|
||||
const {payload} = await socket.send({type: 'web.public'});
|
||||
this.timeout(previousTimeout + (Date.now() - start));
|
||||
const {browser, page} = await createBrowser(optionsWithTask);
|
||||
if (options.beforePage) {
|
||||
|
@ -24,7 +34,7 @@ export function withWeb(task, options) {
|
|||
socket,
|
||||
});
|
||||
}
|
||||
const response = await connectPage(page, `http://${config.public}`, optionsWithTask);
|
||||
const response = await connectPage(page, `http://${payload}`, optionsWithTask);
|
||||
let taskError;
|
||||
try {
|
||||
await task({
|
||||
|
|
|
@ -1,314 +1,3 @@
|
|||
const {stat, unlink} = require('fs/promises');
|
||||
const {join} = require('path');
|
||||
const {Flecks} = require('@flecks/core/build/flecks');
|
||||
|
||||
const Build = require('@flecks/build/build/build');
|
||||
const {regexFromExtensions} = require('@flecks/build/src/server');
|
||||
const {binaryPath, spawnWith} = require('@flecks/core/src/server');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
const {
|
||||
FLECKS_CORE_ROOT = process.cwd(),
|
||||
} = process.env;
|
||||
|
||||
exports.hooks = {
|
||||
'@flecks/build.config': async (target, config, env, argv, flecks) => {
|
||||
const isProduction = 'production' === argv.mode;
|
||||
let finalLoader;
|
||||
switch (target) {
|
||||
case 'fleck': {
|
||||
finalLoader = {loader: MiniCssExtractPlugin.loader};
|
||||
config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'}));
|
||||
break;
|
||||
}
|
||||
case 'server': {
|
||||
finalLoader = {loader: MiniCssExtractPlugin.loader, options: {emit: false}};
|
||||
config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'}));
|
||||
break;
|
||||
}
|
||||
case 'test': {
|
||||
finalLoader = {loader: MiniCssExtractPlugin.loader};
|
||||
config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'}));
|
||||
break;
|
||||
}
|
||||
case 'web': {
|
||||
if (isProduction) {
|
||||
finalLoader = {loader: MiniCssExtractPlugin.loader};
|
||||
config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'}));
|
||||
}
|
||||
else {
|
||||
finalLoader = {loader: 'style-loader'};
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
const buildOneOf = (test, loaders, cssOptions = {}) => ({
|
||||
test,
|
||||
use: [
|
||||
finalLoader,
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
...cssOptions,
|
||||
importLoaders: loaders.length,
|
||||
},
|
||||
},
|
||||
...loaders,
|
||||
'source-map-loader',
|
||||
],
|
||||
});
|
||||
const stylesWithModulesRule = (extensions, loaders) => ({
|
||||
oneOf: [
|
||||
// `.module.*` must match first.
|
||||
buildOneOf(
|
||||
regexFromExtensions(extensions.map((ext) => `module${ext}`)),
|
||||
loaders,
|
||||
{
|
||||
modules: {
|
||||
localIdentName: isProduction
|
||||
? '[hash:base64:5]'
|
||||
: '[path][name]__[local]',
|
||||
},
|
||||
},
|
||||
),
|
||||
buildOneOf(
|
||||
regexFromExtensions(extensions),
|
||||
loaders,
|
||||
),
|
||||
],
|
||||
});
|
||||
const postcss = {
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
postcssOptions: {
|
||||
config: await flecks.resolveBuildConfig('postcss.config.js'),
|
||||
},
|
||||
},
|
||||
};
|
||||
// Originally separated because Sass can't handle incoming source maps, but probably more
|
||||
// performant with 3rd-party CSS anyway.
|
||||
config.module.rules.push(stylesWithModulesRule(['.css'], [postcss]));
|
||||
config.module.rules.push(stylesWithModulesRule(['.sass', '.scss'], [postcss, 'sass-loader']));
|
||||
// Fonts.
|
||||
if (isProduction) {
|
||||
config.module.rules.push({
|
||||
generator: {
|
||||
filename: 'assets/[hash][ext][query]',
|
||||
},
|
||||
test: /\.(eot|ttf|woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
type: 'asset',
|
||||
});
|
||||
}
|
||||
else {
|
||||
config.module.rules.push({
|
||||
test: /\.(eot|ttf|woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
type: 'asset/inline',
|
||||
});
|
||||
}
|
||||
// Images.
|
||||
config.module.rules.push({
|
||||
generator: {
|
||||
filename: 'assets/[hash][ext][query]',
|
||||
},
|
||||
test: /\.(ico|png|jpg|jpeg|gif|svg|webp)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
type: 'asset',
|
||||
});
|
||||
},
|
||||
'@flecks/build.config.alter': async (configs, env, argv, flecks) => {
|
||||
const isProduction = 'production' === argv.mode;
|
||||
// Only build vendor in dev.
|
||||
if (configs['web-vendor']) {
|
||||
if (isProduction) {
|
||||
delete configs['web-vendor'];
|
||||
}
|
||||
// Only build if something actually changed.
|
||||
const dll = flecks.get('@flecks/web.dll');
|
||||
if (dll.length > 0) {
|
||||
const manifest = join(
|
||||
FLECKS_CORE_ROOT,
|
||||
'node_modules',
|
||||
'.cache',
|
||||
'@flecks',
|
||||
'web',
|
||||
'vendor',
|
||||
'manifest.json',
|
||||
);
|
||||
let timestamp = 0;
|
||||
try {
|
||||
const stats = await stat(manifest);
|
||||
timestamp = stats.mtime;
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
catch (error) {}
|
||||
let latest = 0;
|
||||
for (let i = 0; i < dll.length; ++i) {
|
||||
const path = dll[i];
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const stats = await stat(join(FLECKS_CORE_ROOT, 'node_modules', path));
|
||||
if (stats.mtime > latest) {
|
||||
latest = stats.mtime;
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
catch (error) {}
|
||||
}
|
||||
if (timestamp > latest) {
|
||||
delete configs['web-vendor'];
|
||||
}
|
||||
else if (timestamp > 0) {
|
||||
await unlink(manifest);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Bail if there's no web build.
|
||||
if (!configs.web) {
|
||||
return;
|
||||
}
|
||||
// Bail if the build isn't watching.
|
||||
if (!process.argv.find((arg) => 'watch' === arg)) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, spawn `webpack-dev-server` (WDS).
|
||||
const cmd = [
|
||||
await binaryPath('webpack', '@flecks/build'),
|
||||
'serve',
|
||||
'--mode', 'development',
|
||||
'--hot',
|
||||
'--config', await flecks.resolveBuildConfig('fleckspack.config.js'),
|
||||
];
|
||||
spawnWith(
|
||||
cmd,
|
||||
{
|
||||
env: {
|
||||
FLECKS_CORE_BUILD_LIST: 'web',
|
||||
},
|
||||
},
|
||||
);
|
||||
// Remove the build config since we're handing off to WDS.
|
||||
delete configs.web;
|
||||
},
|
||||
'@flecks/build.files': () => [
|
||||
/**
|
||||
* Template file used to generate the client HTML.
|
||||
*
|
||||
* See: https://github.com/jantimon/html-webpack-plugin/blob/main/docs/template-option.md
|
||||
*/
|
||||
'template.ejs',
|
||||
/**
|
||||
* PostCSS config file.
|
||||
*
|
||||
* See: https://github.com/postcss/postcss#usage
|
||||
*/
|
||||
'postcss.config.js',
|
||||
/**
|
||||
* Web client build configuration. See: https://webpack.js.org/configuration/
|
||||
*/
|
||||
'web.webpack.config.js',
|
||||
/**
|
||||
* Web vendor DLL build configuration. See: https://webpack.js.org/configuration/
|
||||
*/
|
||||
'web-vendor.webpack.config.js',
|
||||
],
|
||||
'@flecks/core.config': () => ({
|
||||
/**
|
||||
* The ID of the root element on the page.
|
||||
*/
|
||||
appMountId: 'root',
|
||||
/**
|
||||
* Base tag path.
|
||||
*/
|
||||
base: '/',
|
||||
/**
|
||||
* (webpack-dev-server) Disable the host check.
|
||||
*
|
||||
* See: https://github.com/webpack/webpack-dev-server/issues/887
|
||||
*/
|
||||
devDisableHostCheck: false,
|
||||
/**
|
||||
* (webpack-dev-server) Host to bind.
|
||||
*/
|
||||
devHost: 'localhost',
|
||||
/**
|
||||
* (webpack-dev-server) Port to bind.
|
||||
*/
|
||||
devPort: undefined,
|
||||
/**
|
||||
* (webpack-dev-server) Public path to serve.
|
||||
*
|
||||
* Defaults to `flecks.get('@flecks/web.public')`.
|
||||
*/
|
||||
devPublic: undefined,
|
||||
/**
|
||||
* (webpack-dev-server) Webpack stats output.
|
||||
*/
|
||||
devStats: {
|
||||
preset: 'minimal',
|
||||
},
|
||||
/**
|
||||
* Modules to externalize using `webpack.DllPlugin`.
|
||||
*/
|
||||
dll: [],
|
||||
/**
|
||||
* Host to bind.
|
||||
*/
|
||||
host: '0.0.0.0',
|
||||
/**
|
||||
* Path to icon.
|
||||
*/
|
||||
icon: '',
|
||||
/**
|
||||
* Port to bind.
|
||||
*/
|
||||
port: 32340,
|
||||
/**
|
||||
* Meta tags.
|
||||
*/
|
||||
meta: {
|
||||
charset: 'utf-8',
|
||||
viewport: 'width=device-width, user-scalable=no',
|
||||
},
|
||||
/**
|
||||
* Public path to server.
|
||||
*/
|
||||
public: 'localhost:32340',
|
||||
/**
|
||||
* Webpack stats configuration.
|
||||
*/
|
||||
stats: {
|
||||
preset: 'minimal',
|
||||
},
|
||||
/**
|
||||
* HTML title.
|
||||
*/
|
||||
title: '[@flecks/core.id]',
|
||||
/**
|
||||
* Proxies to trust.
|
||||
*
|
||||
* See: https://www.npmjs.com/package/proxy-addr
|
||||
*/
|
||||
trust: false,
|
||||
}),
|
||||
'@flecks/build.targets': (flecks) => [
|
||||
'web',
|
||||
...(flecks.get('@flecks/web.dll').length > 0 ? ['web-vendor'] : []),
|
||||
],
|
||||
'@flecks/build.targets.alter': (targets) => {
|
||||
// Don't build if there's a fleck target.
|
||||
if (targets.has('fleck')) {
|
||||
targets.delete('web');
|
||||
}
|
||||
},
|
||||
'@flecks/fleck.packageJson': (json, compilation) => {
|
||||
if (Object.keys(compilation.assets).some((filename) => filename.match(/^assets\//))) {
|
||||
json.files.push('assets');
|
||||
}
|
||||
},
|
||||
'@flecks/server.runtime': async (flecks) => {
|
||||
const {config} = await Build.from({
|
||||
config: flecks.config,
|
||||
platforms: ['client', '!server'],
|
||||
});
|
||||
return JSON.stringify(config);
|
||||
},
|
||||
};
|
||||
exports.hooks = Flecks.hooks(require.context('./hooks'));
|
||||
|
|
73
packages/web/build/hooks/@flecks/build.config.alter.js
Normal file
73
packages/web/build/hooks/@flecks/build.config.alter.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
const {stat, unlink} = require('fs/promises');
|
||||
const {join} = require('path');
|
||||
|
||||
const {
|
||||
FLECKS_CORE_ROOT = process.cwd(),
|
||||
} = process.env;
|
||||
|
||||
exports.hook = async (configs, env, argv, flecks) => {
|
||||
const isProduction = 'production' === argv.mode;
|
||||
// Only build vendor in dev.
|
||||
if (configs['web-vendor']) {
|
||||
if (isProduction) {
|
||||
delete configs['web-vendor'];
|
||||
}
|
||||
// Only build if something actually changed.
|
||||
const dll = flecks.get('@flecks/web.dll');
|
||||
if (dll.length > 0) {
|
||||
const manifest = join(
|
||||
FLECKS_CORE_ROOT,
|
||||
'node_modules',
|
||||
'.cache',
|
||||
'@flecks',
|
||||
'web',
|
||||
'vendor',
|
||||
'manifest.json',
|
||||
);
|
||||
let timestamp = 0;
|
||||
try {
|
||||
const stats = await stat(manifest);
|
||||
timestamp = stats.mtime;
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
catch (error) {}
|
||||
let latest = 0;
|
||||
for (let i = 0; i < dll.length; ++i) {
|
||||
const path = dll[i];
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const stats = await stat(join(FLECKS_CORE_ROOT, 'node_modules', path));
|
||||
if (stats.mtime > latest) {
|
||||
latest = stats.mtime;
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
catch (error) {}
|
||||
}
|
||||
if (timestamp > latest) {
|
||||
delete configs['web-vendor'];
|
||||
}
|
||||
else if (timestamp > 0) {
|
||||
await unlink(manifest);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Bail if there's no web server build.
|
||||
const {server} = configs;
|
||||
if (!configs.web || !server) {
|
||||
return;
|
||||
}
|
||||
// Bail if the build isn't watching.
|
||||
if (!process.argv.find((arg) => 'watch' === arg)) {
|
||||
return;
|
||||
}
|
||||
// Send the build details to the server node process so it can spawn the dev server.
|
||||
const plugin = server.plugins.find(({pluginName}) => pluginName === 'StartServerPlugin');
|
||||
if (plugin) {
|
||||
plugin.options.env.FLECKS_WEB_DEV_SERVER = (
|
||||
await flecks.resolveBuildConfig('fleckspack.config.js')
|
||||
);
|
||||
// Remove the build config since we're handing off to the server.
|
||||
delete configs.web;
|
||||
}
|
||||
};
|
106
packages/web/build/hooks/@flecks/build.config.js
Normal file
106
packages/web/build/hooks/@flecks/build.config.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
const {regexFromExtensions} = require('@flecks/build/src/server');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
exports.hook = async (target, config, env, argv, flecks) => {
|
||||
const isProduction = 'production' === argv.mode;
|
||||
let finalLoader;
|
||||
switch (target) {
|
||||
case 'fleck': {
|
||||
finalLoader = {loader: MiniCssExtractPlugin.loader};
|
||||
config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'}));
|
||||
break;
|
||||
}
|
||||
case 'server': {
|
||||
finalLoader = {loader: MiniCssExtractPlugin.loader, options: {emit: false}};
|
||||
config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'}));
|
||||
break;
|
||||
}
|
||||
case 'test': {
|
||||
finalLoader = {loader: MiniCssExtractPlugin.loader};
|
||||
config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'}));
|
||||
break;
|
||||
}
|
||||
case 'web': {
|
||||
if (isProduction) {
|
||||
finalLoader = {loader: MiniCssExtractPlugin.loader};
|
||||
config.plugins.push(new MiniCssExtractPlugin({filename: 'assets/[name].css'}));
|
||||
}
|
||||
else {
|
||||
finalLoader = {loader: 'style-loader'};
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
const buildOneOf = (test, loaders, cssOptions = {}) => ({
|
||||
test,
|
||||
use: [
|
||||
finalLoader,
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
...cssOptions,
|
||||
importLoaders: loaders.length,
|
||||
},
|
||||
},
|
||||
...loaders,
|
||||
'source-map-loader',
|
||||
],
|
||||
});
|
||||
const stylesWithModulesRule = (extensions, loaders) => ({
|
||||
oneOf: [
|
||||
// `.module.*` must match first.
|
||||
buildOneOf(
|
||||
regexFromExtensions(extensions.map((ext) => `module${ext}`)),
|
||||
loaders,
|
||||
{
|
||||
modules: {
|
||||
localIdentName: isProduction
|
||||
? '[hash:base64:5]'
|
||||
: '[path][name]__[local]',
|
||||
},
|
||||
},
|
||||
),
|
||||
buildOneOf(
|
||||
regexFromExtensions(extensions),
|
||||
loaders,
|
||||
),
|
||||
],
|
||||
});
|
||||
const postcss = {
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
postcssOptions: {
|
||||
config: await flecks.resolveBuildConfig('postcss.config.js'),
|
||||
},
|
||||
},
|
||||
};
|
||||
// Originally separated because Sass can't handle incoming source maps, but probably more
|
||||
// performant with 3rd-party CSS anyway.
|
||||
config.module.rules.push(stylesWithModulesRule(['.css'], [postcss]));
|
||||
config.module.rules.push(stylesWithModulesRule(['.sass', '.scss'], [postcss, 'sass-loader']));
|
||||
// Fonts.
|
||||
if (isProduction) {
|
||||
config.module.rules.push({
|
||||
generator: {
|
||||
filename: 'assets/[hash][ext][query]',
|
||||
},
|
||||
test: /\.(eot|ttf|woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
type: 'asset',
|
||||
});
|
||||
}
|
||||
else {
|
||||
config.module.rules.push({
|
||||
test: /\.(eot|ttf|woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
type: 'asset/inline',
|
||||
});
|
||||
}
|
||||
// Images.
|
||||
config.module.rules.push({
|
||||
generator: {
|
||||
filename: 'assets/[hash][ext][query]',
|
||||
},
|
||||
test: /\.(ico|png|jpg|jpeg|gif|svg|webp)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
type: 'asset',
|
||||
});
|
||||
};
|
22
packages/web/build/hooks/@flecks/build.files.js
Normal file
22
packages/web/build/hooks/@flecks/build.files.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
exports.hook = () => [
|
||||
/**
|
||||
* Template file used to generate the client HTML.
|
||||
*
|
||||
* See: https://github.com/jantimon/html-webpack-plugin/blob/main/docs/template-option.md
|
||||
*/
|
||||
'template.ejs',
|
||||
/**
|
||||
* PostCSS config file.
|
||||
*
|
||||
* See: https://github.com/postcss/postcss#usage
|
||||
*/
|
||||
'postcss.config.js',
|
||||
/**
|
||||
* Web client build configuration. See: https://webpack.js.org/configuration/
|
||||
*/
|
||||
'web.webpack.config.js',
|
||||
/**
|
||||
* Web vendor DLL build configuration. See: https://webpack.js.org/configuration/
|
||||
*/
|
||||
'web-vendor.webpack.config.js',
|
||||
];
|
6
packages/web/build/hooks/@flecks/build.targets.alter.js
Normal file
6
packages/web/build/hooks/@flecks/build.targets.alter.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
exports.hook = (targets) => {
|
||||
// Don't build if there's a fleck target.
|
||||
if (targets.has('fleck')) {
|
||||
targets.delete('web');
|
||||
}
|
||||
};
|
4
packages/web/build/hooks/@flecks/build.targets.js
Normal file
4
packages/web/build/hooks/@flecks/build.targets.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
exports.hook = (flecks) => [
|
||||
'web',
|
||||
...(flecks.get('@flecks/web.dll').length > 0 ? ['web-vendor'] : []),
|
||||
];
|
69
packages/web/build/hooks/@flecks/core.config.js
Normal file
69
packages/web/build/hooks/@flecks/core.config.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
exports.hook = () => ({
|
||||
/**
|
||||
* The ID of the root element on the page.
|
||||
*/
|
||||
appMountId: 'root',
|
||||
/**
|
||||
* Base tag path.
|
||||
*/
|
||||
base: '/',
|
||||
/**
|
||||
* (webpack-dev-server) Host to bind. Defaults to `flecks.web.host`
|
||||
*/
|
||||
devHost: undefined,
|
||||
/**
|
||||
* (webpack-dev-server) Port to bind. Defaults to random port.
|
||||
*/
|
||||
devPort: 0,
|
||||
/**
|
||||
* (webpack-dev-server) Webpack stats output.
|
||||
*/
|
||||
devStats: {
|
||||
preset: 'minimal',
|
||||
},
|
||||
/**
|
||||
* Modules to externalize using `webpack.DllPlugin`.
|
||||
*/
|
||||
dll: [],
|
||||
/**
|
||||
* Host to bind.
|
||||
*
|
||||
* Defaults to 'localhost' in development and '0.0.0.0' in production.
|
||||
*/
|
||||
host: undefined,
|
||||
/**
|
||||
* Path to icon.
|
||||
*/
|
||||
icon: '',
|
||||
/**
|
||||
* Port to bind.
|
||||
*/
|
||||
port: 3000,
|
||||
/**
|
||||
* Meta tags.
|
||||
*/
|
||||
meta: {
|
||||
charset: 'utf-8',
|
||||
viewport: 'width=device-width, user-scalable=no',
|
||||
},
|
||||
/**
|
||||
* Public path to server. Defaults to `[@flecks/web.host]:[@flecks/web.port]`.
|
||||
*/
|
||||
public: undefined,
|
||||
/**
|
||||
* Webpack stats configuration.
|
||||
*/
|
||||
stats: {
|
||||
preset: 'minimal',
|
||||
},
|
||||
/**
|
||||
* HTML title.
|
||||
*/
|
||||
title: '[@flecks/core.id]',
|
||||
/**
|
||||
* Proxies to trust.
|
||||
*
|
||||
* See: https://www.npmjs.com/package/proxy-addr
|
||||
*/
|
||||
trust: false,
|
||||
});
|
5
packages/web/build/hooks/@flecks/fleck.packageJson.js
Normal file
5
packages/web/build/hooks/@flecks/fleck.packageJson.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
exports.hook = (json, compilation) => {
|
||||
if (Object.keys(compilation.assets).some((filename) => filename.match(/^assets\//))) {
|
||||
json.files.push('assets');
|
||||
}
|
||||
};
|
9
packages/web/build/hooks/@flecks/server.runtime.js
Normal file
9
packages/web/build/hooks/@flecks/server.runtime.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const Build = require('@flecks/build/build/build');
|
||||
|
||||
exports.hook = async (flecks) => {
|
||||
const {config} = await Build.from({
|
||||
config: flecks.config,
|
||||
platforms: ['client', '!server'],
|
||||
});
|
||||
return JSON.stringify(config);
|
||||
};
|
|
@ -22,19 +22,16 @@ module.exports = async (env, argv, flecks) => {
|
|||
const {
|
||||
appMountId,
|
||||
base,
|
||||
devHost,
|
||||
devPort,
|
||||
devStats,
|
||||
meta,
|
||||
icon,
|
||||
port,
|
||||
title,
|
||||
} = flecks.get('@flecks/web');
|
||||
const isProduction = 'production' === argv.mode;
|
||||
const plugins = [
|
||||
// Environment.
|
||||
new webpack.EnvironmentPlugin({
|
||||
FLECKS_CORE_BUILD_TARGET: 'web',
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.FLECKS_CORE_BUILD_TARGET': "'web'",
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
|
@ -168,8 +165,6 @@ module.exports = async (env, argv, flecks) => {
|
|||
disableDotRule: true,
|
||||
},
|
||||
hot: false,
|
||||
host: devHost,
|
||||
port: devPort || (port + 1),
|
||||
},
|
||||
devtool: 'source-map',
|
||||
entry,
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import {createReadStream} from 'fs';
|
||||
import {createServer, ServerResponse} from 'http';
|
||||
import {join} from 'path';
|
||||
import {PassThrough, Transform} from 'stream';
|
||||
|
||||
import {D} from '@flecks/core';
|
||||
import {binaryPath, spawnWith} from '@flecks/core/server';
|
||||
import bodyParser from 'body-parser';
|
||||
import compression from 'compression';
|
||||
import express from 'express';
|
||||
|
@ -10,7 +12,9 @@ import httpProxy from 'http-proxy';
|
|||
|
||||
const {
|
||||
FLECKS_CORE_ROOT = process.cwd(),
|
||||
FLECKS_WEB_DEV_SERVER,
|
||||
NODE_ENV,
|
||||
TERM,
|
||||
} = process.env;
|
||||
|
||||
const debug = D('@flecks/web/server/http');
|
||||
|
@ -20,18 +24,15 @@ const deliverHtmlStream = async (stream, flecks, req, res) => {
|
|||
};
|
||||
|
||||
export const createHttpServer = async (flecks) => {
|
||||
const {trust} = flecks.get('@flecks/web');
|
||||
const {
|
||||
devHost,
|
||||
devPort,
|
||||
host,
|
||||
public: publicConfig,
|
||||
port,
|
||||
trust,
|
||||
} = flecks.get('@flecks/web');
|
||||
const app = express();
|
||||
app.set('trust proxy', trust);
|
||||
const httpServer = createServer(app);
|
||||
httpServer.app = app;
|
||||
flecks.web.server = httpServer;
|
||||
// Body parser.
|
||||
app.use(bodyParser.json());
|
||||
// Compression. heheh
|
||||
|
@ -43,25 +44,121 @@ export const createHttpServer = async (flecks) => {
|
|||
const routes = (await Promise.all(flecks.invokeFlat('@flecks/web.routes'))).flat();
|
||||
debug('routes: %O', routes);
|
||||
routes.forEach(({method, path, middleware}) => app[method](path, routeMiddleware, middleware));
|
||||
const {host} = flecks.web;
|
||||
await new Promise((resolve, reject) => {
|
||||
const args = [port];
|
||||
if (host) {
|
||||
args.push(host);
|
||||
}
|
||||
args.push(async (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
await flecks.invokeSequentialAsync('@flecks/web/server.up', httpServer);
|
||||
const actualPort = 0 === port ? httpServer.address().port : port;
|
||||
debug(
|
||||
'HTTP server up @ %s!',
|
||||
[host, actualPort].filter((e) => !!e).join(':'),
|
||||
);
|
||||
if ('undefined' === typeof publicConfig) {
|
||||
flecks.web.public = [host, actualPort].join(':');
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
debug('httpServer.listen(...%O)', args.slice(0, -1));
|
||||
httpServer.listen(...args);
|
||||
});
|
||||
// In development mode, create a proxy to the webpack-dev-server.
|
||||
if ('production' !== NODE_ENV) {
|
||||
const {
|
||||
devHost,
|
||||
devPort,
|
||||
} = flecks.get('@flecks/web');
|
||||
const wdsHost = devHost || host;
|
||||
let wdsPort;
|
||||
if (0 === devPort) {
|
||||
wdsPort = 0;
|
||||
}
|
||||
else if (devPort) {
|
||||
wdsPort = devPort;
|
||||
}
|
||||
else {
|
||||
wdsPort = httpServer.address().port + 1;
|
||||
}
|
||||
if (FLECKS_WEB_DEV_SERVER) {
|
||||
// Otherwise, spawn `webpack-dev-server` (WDS).
|
||||
const cmd = [
|
||||
await binaryPath('webpack', '@flecks/build'),
|
||||
'serve',
|
||||
'--mode', 'development',
|
||||
'--hot',
|
||||
'--host', wdsHost,
|
||||
'--port', wdsPort,
|
||||
'--config', FLECKS_WEB_DEV_SERVER,
|
||||
];
|
||||
const wds = spawnWith(
|
||||
cmd,
|
||||
{
|
||||
env: {
|
||||
DEBUG_COLORS: 'dumb' !== TERM,
|
||||
FLECKS_CORE_BUILD_LIST: 'web',
|
||||
FORCE_COLOR: 'dumb' !== TERM,
|
||||
},
|
||||
stdio: 0 === wdsPort ? 'pipe' : 'inherit',
|
||||
},
|
||||
);
|
||||
if (0 === wdsPort) {
|
||||
class ParsePort extends Transform {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let resolve;
|
||||
let reject;
|
||||
this.port = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
this.resolve = resolve;
|
||||
this.on('error', reject);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle, class-methods-use-this
|
||||
_transform(chunk, encoding, done) {
|
||||
this.push(chunk);
|
||||
const matches = chunk.toString().match(/http:\/\/.*\//g);
|
||||
if (!matches) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
const [port] = matches
|
||||
.slice(0, 1)
|
||||
.map((match) => new URL(match))
|
||||
.map(({port}) => port);
|
||||
done();
|
||||
this.resolve(port);
|
||||
}
|
||||
|
||||
}
|
||||
const parsePort = new ParsePort();
|
||||
const stderr = new PassThrough();
|
||||
wds.stderr.pipe(parsePort).pipe(stderr);
|
||||
stderr.pipe(process.stderr);
|
||||
wds.stdout.pipe(process.stdout);
|
||||
wdsPort = await parsePort.port;
|
||||
parsePort.unpipe(stderr);
|
||||
stderr.unpipe(process.stderr);
|
||||
wds.stderr.pipe(process.stderr);
|
||||
}
|
||||
}
|
||||
const proxy = httpProxy.createProxyServer({
|
||||
secure: false,
|
||||
target: `http://${devHost}:${devPort || (port + 1)}`,
|
||||
target: `http://${wdsHost}:${wdsPort}`,
|
||||
});
|
||||
proxy.on('proxyRes', async (proxyRes, req, res) => {
|
||||
res.statusCode = proxyRes.statusCode;
|
||||
// HTML.
|
||||
if (proxyRes.headers['content-type']?.match('text/html')) {
|
||||
// Tests bypass middleware and stream processing.
|
||||
const {pathname} = new URL(req.url, 'https://example.org/');
|
||||
if ('/tests.html' === pathname) {
|
||||
if (!res.headersSent) {
|
||||
res.setHeader('Content-Type', proxyRes.headers['content-type']);
|
||||
}
|
||||
proxyRes.pipe(res);
|
||||
return;
|
||||
}
|
||||
routeMiddleware(req, res, (error) => {
|
||||
if (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -127,23 +224,7 @@ export const createHttpServer = async (flecks) => {
|
|||
}
|
||||
});
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [port];
|
||||
if (host) {
|
||||
args.push(host);
|
||||
}
|
||||
args.push(async (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
await flecks.invokeSequentialAsync('@flecks/web/server.up', httpServer);
|
||||
debug('HTTP server up @ %s!', [host, port].filter((e) => !!e).join(':'));
|
||||
resolve();
|
||||
});
|
||||
debug('httpServer.listen(...%O)', args.slice(0, -1));
|
||||
httpServer.listen(...args);
|
||||
});
|
||||
flecks.web.server = httpServer;
|
||||
};
|
||||
|
||||
export const destroyHttpServer = (httpServer) => {
|
||||
|
|
|
@ -32,7 +32,27 @@ export const hooks = {
|
|||
return routes;
|
||||
},
|
||||
'@flecks/web/server.stream.html': inlineConfig,
|
||||
'@flecks/server.up': (flecks) => createHttpServer(flecks),
|
||||
'@flecks/server.test.socket': async (action, socket, flecks) => {
|
||||
while (!flecks.web.server) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 50);
|
||||
});
|
||||
}
|
||||
const {meta, type} = action;
|
||||
switch (type) {
|
||||
case 'web.public':
|
||||
socket.write(JSON.stringify({
|
||||
meta,
|
||||
payload: flecks.web.public,
|
||||
}));
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
'@flecks/server.up': async (flecks) => {
|
||||
await createHttpServer(flecks);
|
||||
},
|
||||
};
|
||||
|
||||
export const mixin = (Flecks) => class FlecksWithWeb extends Flecks {
|
||||
|
@ -40,7 +60,17 @@ export const mixin = (Flecks) => class FlecksWithWeb extends Flecks {
|
|||
constructor(runtime) {
|
||||
super(runtime);
|
||||
if (!this.web) {
|
||||
this.web = {config: runtime['@flecks/web'], server: undefined};
|
||||
const {
|
||||
host = 'production' === NODE_ENV ? '0.0.0.0' : 'localhost',
|
||||
port,
|
||||
public: httpPublic,
|
||||
} = runtime.config['@flecks/web'];
|
||||
this.web = {
|
||||
config: runtime['@flecks/web'],
|
||||
host,
|
||||
public: httpPublic || [host, port].join(':'),
|
||||
server: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user