feat: hmr logic for the backend

This commit is contained in:
Ivan Shmidt
2020-06-11 02:51:06 +02:00
parent 0331d33a37
commit a1a60ad9d1
16 changed files with 131 additions and 28 deletions
+3
View File
@@ -27,11 +27,13 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@types/stoppable": "^1.1.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
"helmet": "^3.22.0",
"morgan": "^1.10.0",
"stoppable": "^1.1.0",
"winston": "^3.2.1"
},
"devDependencies": {
@@ -42,6 +44,7 @@
"@types/http-errors": "^1.6.3",
"@types/morgan": "^1.9.0",
"@types/supertest": "^2.0.8",
"@types/webpack-env": "^1.15.2",
"@types/yaml": "^1.9.7",
"get-port": "^5.1.1",
"http-errors": "^1.7.3",
+51
View File
@@ -0,0 +1,51 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function useHotEffect(
_module: NodeModule,
effectFactory: () => () => void,
) {
const cancelEffect = effectFactory();
if (_module.hot) {
_module.hot.addDisposeHandler(() => {
cancelEffect();
});
}
}
export function useHotMemoize<T>(
_module: NodeModule,
valueFactory: () => T,
): T {
if (!_module.hot) {
return valueFactory();
}
const index = (useHotMemoize as any).index ?? 0;
(useHotMemoize as any).index += 1;
const prevValue = _module.hot?.data?.[index];
if (prevValue) {
_module.hot!.addDisposeHandler(data => {
data[index] = prevValue;
});
return prevValue;
}
const newValue = valueFactory();
if (_module.hot) {
_module.hot.addDisposeHandler(data => {
data[index] = newValue;
});
}
return newValue;
}
+1
View File
@@ -18,3 +18,4 @@ export * from './errors';
export * from './logging';
export * from './middleware';
export * from './service';
export * from './hot';
@@ -19,6 +19,7 @@ import cors from 'cors';
import express, { Router } from 'express';
import helmet from 'helmet';
import { Server } from 'http';
import stoppable from 'stoppable';
import { Logger } from 'winston';
import { getRootLogger } from '../logging';
import {
@@ -27,6 +28,7 @@ import {
requestLoggingHandler,
} from '../middleware';
import { ServiceBuilder } from './types';
import { useHotEffect } from '../hot';
const DEFAULT_PORT = 7000;
@@ -35,9 +37,14 @@ export class ServiceBuilderImpl implements ServiceBuilder {
private logger: Logger | undefined;
private corsOptions: cors.CorsOptions | undefined;
private routers: [string, Router][];
constructor() {
/**
* Reference to the module where builder is created
* Needed for the HMR
*/
private module: NodeModule;
constructor(module: NodeModule) {
this.routers = [];
this.module = module;
}
setPort(port: number): ServiceBuilder {
@@ -82,9 +89,20 @@ export class ServiceBuilderImpl implements ServiceBuilder {
logger.error(`Failed to start up on port ${port}, ${e}`);
reject(e);
});
const server = app.listen(port, () => {
logger.info(`Listening on port ${port}`);
const server = stoppable(
app.listen(port, () => {
logger.info(`Listening on port ${port}`);
}),
0,
);
useHotEffect(this.module, () => {
return () =>
server.stop((e: any) => {
if (e) console.error(e);
});
});
resolve(server);
});
}
@@ -19,6 +19,6 @@ import { ServiceBuilderImpl } from './ServiceBuilderImpl';
/**
* Creates a new service builder.
*/
export function createServiceBuilder() {
return new ServiceBuilderImpl();
export function createServiceBuilder(_module: NodeModule) {
return new ServiceBuilderImpl(_module);
}
+1 -1
View File
@@ -9,6 +9,6 @@
"target": "ES2019",
"module": "commonjs",
"esModuleInterop": true,
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
+17 -6
View File
@@ -22,7 +22,11 @@
* Happy hacking!
*/
import { createServiceBuilder, getRootLogger } from '@backstage/backend-common';
import {
createServiceBuilder,
getRootLogger,
useHotMemoize,
} from '@backstage/backend-common';
import knex from 'knex';
import auth from './plugins/auth';
import catalog from './plugins/catalog';
@@ -45,19 +49,24 @@ function createEnv(plugin: string): PluginEnvironment {
}
async function main() {
const service = createServiceBuilder()
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
const identityEnv = useHotMemoize(module, () => createEnv('identity'));
const service = createServiceBuilder(module)
.enableCors({
origin: 'http://localhost:3000',
credentials: true,
})
.addRouter('/catalog', await catalog(createEnv('catalog')))
.addRouter('/scaffolder', await scaffolder(createEnv('scaffolder')))
.addRouter('/catalog', await catalog(catalogEnv))
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
.addRouter(
'/sentry',
await sentry(getRootLogger().child({ type: 'plugin', plugin: 'sentry' })),
)
.addRouter('/auth', await auth(createEnv('auth')))
.addRouter('/identity', await identity(createEnv('identity')));
.addRouter('/auth', await auth(authEnv))
.addRouter('/identity', await identity(identityEnv));
await service.start().catch(err => {
console.log(err);
@@ -66,3 +75,5 @@ async function main() {
}
main();
module.hot?.accept();
+4 -1
View File
@@ -24,6 +24,7 @@ import {
runPeriodically,
} from '@backstage/plugin-catalog-backend';
import { PluginEnvironment } from '../types';
import { useHotEffect } from '@backstage/backend-common';
export default async function createPlugin({
logger,
@@ -41,7 +42,9 @@ export default async function createPlugin({
logger,
);
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000);
useHotEffect(module, () =>
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000),
);
return await createRouter({
entitiesCatalog,
+1 -1
View File
@@ -10,6 +10,6 @@
"module": "commonjs",
"esModuleInterop": true,
"lib": ["es2019", "dom"],
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
+5 -8
View File
@@ -87,17 +87,11 @@ export function createConfig(
externals: [
nodeExternals({
modulesDir: paths.rootNodeModules,
whitelist: [
'webpack/hot/poll?100',
/\@backstage\/.*\/(?!node_modules)/,
],
whitelist: ['webpack/hot/poll?100', /\@backstage\/.*/],
}),
nodeExternals({
modulesDir: paths.targetNodeModules,
whitelist: [
'webpack/hot/poll?100',
/\@backstage\/.*\/(?!node_modules)/,
],
whitelist: ['webpack/hot/poll?100', /\@backstage\/.*/],
}),
],
target: 'node' as const,
@@ -141,6 +135,9 @@ export function createConfig(
resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
mainFields: ['main:src', 'browser', 'module', 'main'],
...(isBackend
? { modules: [paths.targetNodeModules, paths.rootNodeModules] }
: {}),
plugins: [
new ModuleScopePlugin(
[paths.targetSrc, paths.targetDev],
+1 -1
View File
@@ -10,6 +10,6 @@
"module": "commonjs",
"esModuleInterop": true,
"lib": ["es2019"],
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
@@ -54,7 +54,7 @@ export async function startStandaloneServer(
higherOrderOperation,
logger,
});
const service = createServiceBuilder()
const service = createServiceBuilder(module)
.enableCors({ origin: 'http://localhost:3000' })
.addRouter('/catalog', router);
return await service.start().catch(err => {
@@ -62,3 +62,5 @@ export async function startStandaloneServer(
process.exit(1);
});
}
module.hot?.accept();
+1 -1
View File
@@ -10,6 +10,6 @@
"module": "commonjs",
"esModuleInterop": true,
"lib": ["es2019"],
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
+1 -1
View File
@@ -10,6 +10,6 @@
"module": "commonjs",
"esModuleInterop": true,
"lib": ["es2019"],
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
+1 -1
View File
@@ -10,6 +10,6 @@
"module": "commonjs",
"esModuleInterop": true,
"lib": ["es2019"],
"types": ["node", "jest"]
"types": ["node", "jest", "webpack-env"]
}
}
+17
View File
@@ -4025,6 +4025,13 @@
dependencies:
"@types/webpack" "*"
"@types/stoppable@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.0.tgz#a5fa6a48120b109ca9233eed05c67c50bc4f3b91"
integrity sha512-BRR23Q9CJduH7AM6mk4JRttd8XyFkb4qIPZu4mdLF+VoP+wcjIxIWIKiBbN78NBbEuynrAyMPtzOHnIp2B/JPQ==
dependencies:
"@types/node" "*"
"@types/styled-jsx@^2.2.8":
version "2.2.8"
resolved "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234"
@@ -4148,6 +4155,11 @@
resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.15.1.tgz#c8e84705e08eed430b5e15b39c65b0944e4d1422"
integrity sha512-eWN5ElDTeBc5lRDh95SqA8x18D0ll2pWudU3uWiyfsRmIZcmUXpEsxPU+7+BsdCrO2vfLRC629u/MmjbmF+2tA==
"@types/webpack-env@^1.15.2":
version "1.15.2"
resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a"
integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ==
"@types/webpack-node-externals@^1.7.1":
version "1.7.1"
resolved "https://registry.npmjs.org/@types/webpack-node-externals/-/webpack-node-externals-1.7.1.tgz#73d0d7ae0e98cfbd69b7443388302cd69217494a"
@@ -17284,6 +17296,11 @@ stealthy-require@^1.1.1:
resolved "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
stoppable@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b"
integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==
store2@^2.7.1:
version "2.10.0"
resolved "https://registry.npmjs.org/store2/-/store2-2.10.0.tgz#46b82bb91878daf1b0d56dec2f1d41e54d5103cf"