feat: hmr logic for the backend
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"target": "ES2019",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"types": ["node", "jest"]
|
||||
"types": ["node", "jest", "webpack-env"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"lib": ["es2019", "dom"],
|
||||
"types": ["node", "jest"]
|
||||
"types": ["node", "jest", "webpack-env"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user