From a1a60ad9d1563d36483d2b293eb53fcfc49f17de Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 11 Jun 2020 02:51:06 +0200 Subject: [PATCH] feat: hmr logic for the backend --- packages/backend-common/package.json | 3 ++ packages/backend-common/src/hot.ts | 51 +++++++++++++++++++ packages/backend-common/src/index.ts | 1 + .../src/service/ServiceBuilderImpl.ts | 26 ++++++++-- .../src/service/createServiceBuilder.ts | 4 +- packages/backend-common/tsconfig.json | 2 +- packages/backend/src/index.ts | 23 ++++++--- packages/backend/src/plugins/catalog.ts | 5 +- packages/backend/tsconfig.json | 2 +- packages/cli/src/lib/bundler/config.ts | 13 ++--- plugins/auth-backend/tsconfig.json | 2 +- .../src/service/standaloneServer.ts | 4 +- plugins/catalog-backend/tsconfig.json | 2 +- plugins/identity-backend/tsconfig.json | 2 +- plugins/sentry-backend/tsconfig.json | 2 +- yarn.lock | 17 +++++++ 16 files changed, 131 insertions(+), 28 deletions(-) create mode 100644 packages/backend-common/src/hot.ts diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 54202975c3..b7d5cf7c11 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -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", diff --git a/packages/backend-common/src/hot.ts b/packages/backend-common/src/hot.ts new file mode 100644 index 0000000000..3043bcb681 --- /dev/null +++ b/packages/backend-common/src/hot.ts @@ -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( + _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; +} diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index 11689aafff..0e4e5b2c6b 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -18,3 +18,4 @@ export * from './errors'; export * from './logging'; export * from './middleware'; export * from './service'; +export * from './hot'; diff --git a/packages/backend-common/src/service/ServiceBuilderImpl.ts b/packages/backend-common/src/service/ServiceBuilderImpl.ts index ac35d52112..844990a39d 100644 --- a/packages/backend-common/src/service/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/ServiceBuilderImpl.ts @@ -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); }); } diff --git a/packages/backend-common/src/service/createServiceBuilder.ts b/packages/backend-common/src/service/createServiceBuilder.ts index ffd8901def..daef612fcf 100644 --- a/packages/backend-common/src/service/createServiceBuilder.ts +++ b/packages/backend-common/src/service/createServiceBuilder.ts @@ -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); } diff --git a/packages/backend-common/tsconfig.json b/packages/backend-common/tsconfig.json index ca39bf9964..2e645bef0b 100644 --- a/packages/backend-common/tsconfig.json +++ b/packages/backend-common/tsconfig.json @@ -9,6 +9,6 @@ "target": "ES2019", "module": "commonjs", "esModuleInterop": true, - "types": ["node", "jest"] + "types": ["node", "jest", "webpack-env"] } } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 0f0733a8ec..09e244545b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -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(); diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 8d8ac5b47b..a9e4fa8ce6 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -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, diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index 04701a5502..269b22607c 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -10,6 +10,6 @@ "module": "commonjs", "esModuleInterop": true, "lib": ["es2019", "dom"], - "types": ["node", "jest"] + "types": ["node", "jest", "webpack-env"] } } diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 241ef6a9af..0b664f7cf3 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -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], diff --git a/plugins/auth-backend/tsconfig.json b/plugins/auth-backend/tsconfig.json index 015a967f76..6f1acf8dce 100644 --- a/plugins/auth-backend/tsconfig.json +++ b/plugins/auth-backend/tsconfig.json @@ -10,6 +10,6 @@ "module": "commonjs", "esModuleInterop": true, "lib": ["es2019"], - "types": ["node", "jest"] + "types": ["node", "jest", "webpack-env"] } } diff --git a/plugins/catalog-backend/src/service/standaloneServer.ts b/plugins/catalog-backend/src/service/standaloneServer.ts index f19dfaf282..93c8b86779 100644 --- a/plugins/catalog-backend/src/service/standaloneServer.ts +++ b/plugins/catalog-backend/src/service/standaloneServer.ts @@ -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(); diff --git a/plugins/catalog-backend/tsconfig.json b/plugins/catalog-backend/tsconfig.json index 015a967f76..6f1acf8dce 100644 --- a/plugins/catalog-backend/tsconfig.json +++ b/plugins/catalog-backend/tsconfig.json @@ -10,6 +10,6 @@ "module": "commonjs", "esModuleInterop": true, "lib": ["es2019"], - "types": ["node", "jest"] + "types": ["node", "jest", "webpack-env"] } } diff --git a/plugins/identity-backend/tsconfig.json b/plugins/identity-backend/tsconfig.json index 015a967f76..6f1acf8dce 100644 --- a/plugins/identity-backend/tsconfig.json +++ b/plugins/identity-backend/tsconfig.json @@ -10,6 +10,6 @@ "module": "commonjs", "esModuleInterop": true, "lib": ["es2019"], - "types": ["node", "jest"] + "types": ["node", "jest", "webpack-env"] } } diff --git a/plugins/sentry-backend/tsconfig.json b/plugins/sentry-backend/tsconfig.json index 015a967f76..6f1acf8dce 100644 --- a/plugins/sentry-backend/tsconfig.json +++ b/plugins/sentry-backend/tsconfig.json @@ -10,6 +10,6 @@ "module": "commonjs", "esModuleInterop": true, "lib": ["es2019"], - "types": ["node", "jest"] + "types": ["node", "jest", "webpack-env"] } } diff --git a/yarn.lock b/yarn.lock index 107ba84663..343d9ef3dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"