fix: dynamic web ports
This commit is contained in:
parent
31fd7b46a4
commit
1b13374b10
|
@ -37,8 +37,7 @@ export const hooks = {
|
||||||
await createWindow(flecks);
|
await createWindow(flecks);
|
||||||
},
|
},
|
||||||
'@flecks/electron/server.window': async (win, flecks) => {
|
'@flecks/electron/server.window': async (win, flecks) => {
|
||||||
const {public: $$public} = flecks.get('@flecks/web');
|
const {installExtensions, url} = flecks.get('@flecks/electron');
|
||||||
const {installExtensions, url = `http://${$$public}`} = flecks.get('@flecks/electron');
|
|
||||||
if (installExtensions) {
|
if (installExtensions) {
|
||||||
const installer = __non_webpack_require__('electron-devtools-installer');
|
const installer = __non_webpack_require__('electron-devtools-installer');
|
||||||
const {default: installExtension} = installer;
|
const {default: installExtension} = installer;
|
||||||
|
@ -48,7 +47,17 @@ export const hooks = {
|
||||||
.flat(),
|
.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) => ({
|
'@flecks/repl.context': (flecks) => ({
|
||||||
electron: {
|
electron: {
|
||||||
|
|
|
@ -5,7 +5,17 @@ import {createBrowser, connectPage} from './connect';
|
||||||
export function withWeb(task, options) {
|
export function withWeb(task, options) {
|
||||||
return async function withWeb() {
|
return async function withWeb() {
|
||||||
const optionsWithTask = {...options, task: this};
|
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);
|
const socket = await server.waitForSocket(optionsWithTask);
|
||||||
if (options.beforeConnect) {
|
if (options.beforeConnect) {
|
||||||
await options.beforeConnect({server, socket});
|
await options.beforeConnect({server, socket});
|
||||||
|
@ -13,7 +23,7 @@ export function withWeb(task, options) {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const previousTimeout = this.timeout();
|
const previousTimeout = this.timeout();
|
||||||
this.timeout(0);
|
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));
|
this.timeout(previousTimeout + (Date.now() - start));
|
||||||
const {browser, page} = await createBrowser(optionsWithTask);
|
const {browser, page} = await createBrowser(optionsWithTask);
|
||||||
if (options.beforePage) {
|
if (options.beforePage) {
|
||||||
|
@ -24,7 +34,7 @@ export function withWeb(task, options) {
|
||||||
socket,
|
socket,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const response = await connectPage(page, `http://${config.public}`, optionsWithTask);
|
const response = await connectPage(page, `http://${payload}`, optionsWithTask);
|
||||||
let taskError;
|
let taskError;
|
||||||
try {
|
try {
|
||||||
await task({
|
await task({
|
||||||
|
|
|
@ -1,314 +1,3 @@
|
||||||
const {stat, unlink} = require('fs/promises');
|
const {Flecks} = require('@flecks/core/build/flecks');
|
||||||
const {join} = require('path');
|
|
||||||
|
|
||||||
const Build = require('@flecks/build/build/build');
|
exports.hooks = Flecks.hooks(require.context('./hooks'));
|
||||||
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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
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 {
|
const {
|
||||||
appMountId,
|
appMountId,
|
||||||
base,
|
base,
|
||||||
devHost,
|
|
||||||
devPort,
|
|
||||||
devStats,
|
devStats,
|
||||||
meta,
|
meta,
|
||||||
icon,
|
icon,
|
||||||
port,
|
|
||||||
title,
|
title,
|
||||||
} = flecks.get('@flecks/web');
|
} = flecks.get('@flecks/web');
|
||||||
const isProduction = 'production' === argv.mode;
|
const isProduction = 'production' === argv.mode;
|
||||||
const plugins = [
|
const plugins = [
|
||||||
// Environment.
|
// Environment.
|
||||||
new webpack.EnvironmentPlugin({
|
new webpack.DefinePlugin({
|
||||||
FLECKS_CORE_BUILD_TARGET: 'web',
|
'process.env.FLECKS_CORE_BUILD_TARGET': "'web'",
|
||||||
}),
|
}),
|
||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
Buffer: ['buffer', 'Buffer'],
|
Buffer: ['buffer', 'Buffer'],
|
||||||
|
@ -168,8 +165,6 @@ module.exports = async (env, argv, flecks) => {
|
||||||
disableDotRule: true,
|
disableDotRule: true,
|
||||||
},
|
},
|
||||||
hot: false,
|
hot: false,
|
||||||
host: devHost,
|
|
||||||
port: devPort || (port + 1),
|
|
||||||
},
|
},
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
entry,
|
entry,
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import {createReadStream} from 'fs';
|
import {createReadStream} from 'fs';
|
||||||
import {createServer, ServerResponse} from 'http';
|
import {createServer, ServerResponse} from 'http';
|
||||||
import {join} from 'path';
|
import {join} from 'path';
|
||||||
|
import {PassThrough, Transform} from 'stream';
|
||||||
|
|
||||||
import {D} from '@flecks/core';
|
import {D} from '@flecks/core';
|
||||||
|
import {binaryPath, spawnWith} from '@flecks/core/server';
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import compression from 'compression';
|
import compression from 'compression';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
@ -10,7 +12,9 @@ import httpProxy from 'http-proxy';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
FLECKS_CORE_ROOT = process.cwd(),
|
FLECKS_CORE_ROOT = process.cwd(),
|
||||||
|
FLECKS_WEB_DEV_SERVER,
|
||||||
NODE_ENV,
|
NODE_ENV,
|
||||||
|
TERM,
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
const debug = D('@flecks/web/server/http');
|
const debug = D('@flecks/web/server/http');
|
||||||
|
@ -20,18 +24,15 @@ const deliverHtmlStream = async (stream, flecks, req, res) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createHttpServer = async (flecks) => {
|
export const createHttpServer = async (flecks) => {
|
||||||
const {trust} = flecks.get('@flecks/web');
|
|
||||||
const {
|
const {
|
||||||
devHost,
|
public: publicConfig,
|
||||||
devPort,
|
|
||||||
host,
|
|
||||||
port,
|
port,
|
||||||
|
trust,
|
||||||
} = flecks.get('@flecks/web');
|
} = flecks.get('@flecks/web');
|
||||||
const app = express();
|
const app = express();
|
||||||
app.set('trust proxy', trust);
|
app.set('trust proxy', trust);
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
httpServer.app = app;
|
httpServer.app = app;
|
||||||
flecks.web.server = httpServer;
|
|
||||||
// Body parser.
|
// Body parser.
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
// Compression. heheh
|
// Compression. heheh
|
||||||
|
@ -43,25 +44,121 @@ export const createHttpServer = async (flecks) => {
|
||||||
const routes = (await Promise.all(flecks.invokeFlat('@flecks/web.routes'))).flat();
|
const routes = (await Promise.all(flecks.invokeFlat('@flecks/web.routes'))).flat();
|
||||||
debug('routes: %O', routes);
|
debug('routes: %O', routes);
|
||||||
routes.forEach(({method, path, middleware}) => app[method](path, routeMiddleware, middleware));
|
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.
|
// In development mode, create a proxy to the webpack-dev-server.
|
||||||
if ('production' !== NODE_ENV) {
|
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({
|
const proxy = httpProxy.createProxyServer({
|
||||||
secure: false,
|
secure: false,
|
||||||
target: `http://${devHost}:${devPort || (port + 1)}`,
|
target: `http://${wdsHost}:${wdsPort}`,
|
||||||
});
|
});
|
||||||
proxy.on('proxyRes', async (proxyRes, req, res) => {
|
proxy.on('proxyRes', async (proxyRes, req, res) => {
|
||||||
res.statusCode = proxyRes.statusCode;
|
res.statusCode = proxyRes.statusCode;
|
||||||
// HTML.
|
// HTML.
|
||||||
if (proxyRes.headers['content-type']?.match('text/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) => {
|
routeMiddleware(req, res, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
@ -127,23 +224,7 @@ export const createHttpServer = async (flecks) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
flecks.web.server = httpServer;
|
||||||
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);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const destroyHttpServer = (httpServer) => {
|
export const destroyHttpServer = (httpServer) => {
|
||||||
|
|
|
@ -32,7 +32,27 @@ export const hooks = {
|
||||||
return routes;
|
return routes;
|
||||||
},
|
},
|
||||||
'@flecks/web/server.stream.html': inlineConfig,
|
'@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 {
|
export const mixin = (Flecks) => class FlecksWithWeb extends Flecks {
|
||||||
|
@ -40,7 +60,17 @@ export const mixin = (Flecks) => class FlecksWithWeb extends Flecks {
|
||||||
constructor(runtime) {
|
constructor(runtime) {
|
||||||
super(runtime);
|
super(runtime);
|
||||||
if (!this.web) {
|
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