diff --git a/packages/backend/package.json b/packages/backend/package.json index 4e2610380c..98607b5a00 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,7 +1,7 @@ { "name": "example-backend", "version": "0.1.1-alpha.7", - "main": "dist", + "main": "dist/index.cjs", "types": "src/index.ts", "private": true, "license": "Apache-2.0", @@ -9,8 +9,8 @@ "node": ">=12" }, "scripts": { - "build": "tsc", - "start": "backstage-cli watch-deps --build -- tsc-watch --onFirstSuccess \\\"nodemon -r esm\\\"", + "build": "backstage-cli build --outputs cjs", + "start": "backstage-cli watch-deps --build -- backstage-cli backend:dev", "lint": "backstage-cli lint", "test": "backstage-cli test", "clean": "backstage-cli clean", diff --git a/packages/cli/package.json b/packages/cli/package.json index 58b6c3fd84..0dbf9c2f71 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -39,6 +39,8 @@ "@rollup/plugin-node-resolve": "^7.1.1", "@spotify/eslint-config": "^7.0.1", "@sucrase/webpack-loader": "^2.0.0", + "@types/start-server-webpack-plugin": "^2.2.0", + "@types/webpack-node-externals": "^1.7.1", "bfj": "^7.0.2", "chalk": "^4.0.0", "chokidar": "^3.3.1", @@ -72,6 +74,7 @@ "rollup-plugin-peer-deps-external": "^2.2.2", "rollup-plugin-postcss": "^3.1.1", "rollup-plugin-typescript2": "^0.26.0", + "start-server-webpack-plugin": "^2.2.5", "style-loader": "^1.2.1", "sucrase": "^3.14.1", "tar": "^6.0.1", @@ -81,6 +84,7 @@ "url-loader": "^4.1.0", "webpack": "^4.41.6", "webpack-dev-server": "^3.10.3", + "webpack-node-externals": "^1.7.2", "yaml": "^1.10.0", "yml-loader": "^2.1.0", "yn": "^4.0.0" diff --git a/packages/cli/src/commands/backend/dev.ts b/packages/cli/src/commands/backend/dev.ts new file mode 100644 index 0000000000..f04f904e09 --- /dev/null +++ b/packages/cli/src/commands/backend/dev.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +import { ConfigReader } from '@backstage/config'; +import { loadConfig } from '@backstage/config-loader'; +import { Command } from 'commander'; +import { serveBackend } from '../../lib/bundler/backend'; + +export default async (cmd: Command) => { + const appConfigs = await loadConfig(); + const waitForExit = await serveBackend({ + entry: 'src/index', + checksEnabled: cmd.check, + config: ConfigReader.fromConfigs(appConfigs), + appConfigs, + }); + + await waitForExit(); +}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a0302c4713..ea71dc6485 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -41,6 +41,12 @@ const main = (argv: string[]) => { .option('--check', 'Enable type checking and linting') .action(lazyAction(() => import('./commands/app/serve'), 'default')); + program + .command('backend:dev') + .description('Start local development server with HMR for the backend') + .option('--check', 'Enable type checking and linting') + .action(lazyAction(() => import('./commands/backend/dev'), 'default')); + program .command('app:diff') .option('--check', 'Fail if changes are required') diff --git a/packages/cli/src/lib/bundler/backend.ts b/packages/cli/src/lib/bundler/backend.ts new file mode 100644 index 0000000000..81ff6b6cff --- /dev/null +++ b/packages/cli/src/lib/bundler/backend.ts @@ -0,0 +1,59 @@ +/* + * 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. + */ + +import webpack from 'webpack'; +import { createConfig } from './config'; +import { resolveBundlingPaths } from './paths'; +import { ServeOptions } from './types'; + +export async function serveBackend(options: ServeOptions) { + const paths = resolveBundlingPaths(options); + const config = createConfig(paths, { + ...options, + isDev: true, + isBackend: true, + }); + + const compiler = webpack(config); + + const watcher = compiler.watch( + { + poll: true, + }, + (err: Error, _stats: any) => { + console.log('Build succeeded'); + if (err) { + console.error(err); + } + }, + ); + + const waitForExit = async () => { + for (const signal of ['SIGINT', 'SIGTERM'] as const) { + process.on(signal, () => { + // server.close(); + watcher.close(() => console.log('Watcher closed')); + // exit instead of resolve. The process is shutting down and resolving a promise here logs an error + process.exit(); + }); + } + + // Block indefinitely and wait for the interrupt signal + return new Promise(() => {}); + }; + + return waitForExit; +} diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index d46308f043..608f2d7b91 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -14,13 +14,15 @@ * limitations under the License. */ -import webpack from 'webpack'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; -import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; +import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin'; +import StartServerPlugin from 'start-server-webpack-plugin'; +import webpack from 'webpack'; +import nodeExternals from 'webpack-node-externals'; +import { optimization } from './optimization'; import { BundlingPaths } from './paths'; import { transforms } from './transforms'; -import { optimization } from './optimization'; import { BundlingOptions } from './types'; // import checkRequiredFiles from 'react-dev-utils/checkRequiredFiles'; // import ModuleNotFoundPlugin from 'react-dev-utils/ModuleNotFoundPlugin'; @@ -32,7 +34,7 @@ export function createConfig( paths: BundlingPaths, options: BundlingOptions, ): webpack.Configuration { - const { checksEnabled, isDev } = options; + const { checksEnabled, isDev, isBackend } = options; const { plugins, loaders } = transforms(options); @@ -80,16 +82,65 @@ export function createConfig( return { mode: isDev ? 'development' : 'production', profile: false, + ...(isBackend + ? { + watch: true, + watchOptions: { + ignored: [/node_modules\/(?!\@backstage)/], + // poll: 1000 + }, + externals: [ + nodeExternals({ + modulesDir: paths.rootNodeModules, + whitelist: [ + 'webpack/hot/poll?100', + /\@backstage\/.*\/(?!node_modules)/, + ], + }), + nodeExternals({ + modulesDir: paths.targetNodeModules, + whitelist: [ + 'webpack/hot/poll?100', + /\@backstage\/.*\/(?!node_modules)/, + ], + }), + ], + target: 'node', + node: { + __dirname: true, + __filename: true, + global: true, + }, + } + : { + node: { + module: 'empty', + dgram: 'empty', + dns: 'mock', + fs: 'empty', + http2: 'empty', + net: 'empty', + tls: 'empty', + child_process: 'empty', + }, + optimization: optimization(options), + }), bail: false, performance: { hints: false, // we check the gzip size instead }, devtool: isDev ? 'cheap-module-eval-source-map' : 'source-map', context: paths.targetPath, - entry: [require.resolve('react-hot-loader/patch'), paths.targetEntry], + entry: [ + ...(isBackend + ? ['webpack/hot/poll?100'] + : [require.resolve('react-hot-loader/patch')]), + paths.targetEntry, + ], resolve: { extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'], mainFields: ['main:src', 'browser', 'module', 'main'], + modules: [paths.targetNodeModules, paths.rootNodeModules], plugins: [ new ModuleScopePlugin( [paths.targetSrc, paths.targetDev], @@ -111,17 +162,11 @@ export function createConfig( ? '[name].chunk.js' : '[name].[chunkhash:8].chunk.js', }, - optimization: optimization(options), - plugins, - node: { - module: 'empty', - dgram: 'empty', - dns: 'mock', - fs: 'empty', - http2: 'empty', - net: 'empty', - tls: 'empty', - child_process: 'empty', - }, + plugins: isBackend + ? [ + new StartServerPlugin('main.js'), + new webpack.HotModuleReplacementPlugin(), + ] + : plugins, }; } diff --git a/packages/cli/src/lib/bundler/index.ts b/packages/cli/src/lib/bundler/index.ts index cdfb5f3897..2030b4bb96 100644 --- a/packages/cli/src/lib/bundler/index.ts +++ b/packages/cli/src/lib/bundler/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ +export { serveBackend } from './backend'; export { buildBundle } from './bundle'; export { serveBundle } from './server'; diff --git a/packages/cli/src/lib/bundler/paths.ts b/packages/cli/src/lib/bundler/paths.ts index cec270178a..1e1cd1298a 100644 --- a/packages/cli/src/lib/bundler/paths.ts +++ b/packages/cli/src/lib/bundler/paths.ts @@ -60,6 +60,8 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) { targetTsConfig: paths.resolveTargetRoot('tsconfig.json'), targetNodeModules: paths.resolveTarget('node_modules'), targetPackageJson: paths.resolveTarget('package.json'), + rootNodeModules: paths.resolveTargetRoot('node_modules'), + root: paths.targetRoot, }; } diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 332a105182..b375897fc9 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -22,6 +22,7 @@ export type BundlingOptions = { isDev: boolean; config: Config; appConfigs: AppConfig[]; + isBackend?: boolean; }; export type ServeOptions = BundlingPathsOptions & { diff --git a/yarn.lock b/yarn.lock index c75690a1fd..107ba84663 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2406,11 +2406,6 @@ dependencies: "@types/node" ">= 8" -"@open-draft/until@^1.0.0": - version "1.0.3" - resolved "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" - integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== - "@reach/router@^1.2.1": version "1.3.3" resolved "https://registry.npmjs.org/@reach/router/-/router-1.3.3.tgz#58162860dce6c9449d49be86b0561b5ef46d80db" @@ -3481,11 +3476,6 @@ dependencies: "@types/express" "*" -"@types/cookie@^0.3.3": - version "0.3.3" - resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" - integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== - "@types/cookiejar@*": version "2.1.1" resolved "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz#90b68446364baf9efd8e8349bb36bd3852b75b80" @@ -4028,6 +4018,13 @@ resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/start-server-webpack-plugin@^2.2.0": + version "2.2.0" + resolved "https://registry.npmjs.org/@types/start-server-webpack-plugin/-/start-server-webpack-plugin-2.2.0.tgz#a7c4595c715eda083d92ca1ea184d97db3d8fc7b" + integrity sha512-TFiZWMPuiMR/utvjk6ENi0HPtQl38HnPMYfJqm04ztpzITHzTCXt7T7LyXnP9eTwg4lLQkmRUaFy04iEyjoJmw== + dependencies: + "@types/webpack" "*" + "@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" @@ -4151,6 +4148,13 @@ resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.15.1.tgz#c8e84705e08eed430b5e15b39c65b0944e4d1422" integrity sha512-eWN5ElDTeBc5lRDh95SqA8x18D0ll2pWudU3uWiyfsRmIZcmUXpEsxPU+7+BsdCrO2vfLRC629u/MmjbmF+2tA== +"@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" + integrity sha512-kbO2gYPKvMb5j1KOgnIuUH52CKul9Ud4b10J5n+JX8oHmgu86hYpBVfrV4bMDe5lhCaO64h8QrKz7WnRZzqkbA== + dependencies: + "@types/webpack" "*" + "@types/webpack-sources@*": version "0.1.6" resolved "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.6.tgz#3d21dfc2ec0ad0c77758e79362426a9ba7d7cbcb" @@ -6657,11 +6661,6 @@ cookie@0.4.0: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== -cookie@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" - integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== - cookiejar@^2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" @@ -9771,11 +9770,6 @@ graphql@15.0.0: resolved "https://registry.npmjs.org/graphql/-/graphql-15.0.0.tgz#042a5eb5e2506a2e2111ce41eb446a8e570b8be9" integrity sha512-ZyVO1xIF9F+4cxfkdhOJINM+51B06Friuv4M66W7HzUOeFd+vNzUn4vtswYINPi6sysjf1M2Ri/rwZALqgwbaQ== -graphql@^15.0.0: - version "15.1.0" - resolved "https://registry.npmjs.org/graphql/-/graphql-15.1.0.tgz#b93e28de805294ec08e1630d901db550cb8960a1" - integrity sha512-0TVyfOlCGhv/DBczQkJmwXOK6fjWkjzY3Pt7wY8i0gcYXq8aogG3weCsg48m72lywKSeOqedEHvVPOvZvSD51Q== - growly@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -9940,11 +9934,6 @@ he@^1.2.0: resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -headers-utils@^1.1.3, headers-utils@^1.1.9: - version "1.2.0" - resolved "https://registry.npmjs.org/headers-utils/-/headers-utils-1.2.0.tgz#5e10d1bc9d2bccf789547afca5b991a3167241e8" - integrity sha512-4/BMXcWrJErw7JpM87gF8MNEXcIMLzepYZjNRv/P9ctgupl2Ywa3u1PgHtNhSRq84bHH9Ndlkdy7bSi+bZ9I9A== - helmet-crossdomain@0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz#5f1fe5a836d0325f1da0a78eaa5fd8429078894e" @@ -13169,22 +13158,6 @@ ms@^2.0.0, ms@^2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -msw@^0.19.0: - version "0.19.0" - resolved "https://registry.npmjs.org/msw/-/msw-0.19.0.tgz#fd37015787d40db82d243a2853be66c466675e72" - integrity sha512-1TpmJzJ+afBWTRNJYoeW8KwLQbCVlvvhw2u/eRuIYfel+bPqcut5NaSgo+Bi4C0Q/7M5wza00w1GEuOXQu6FCA== - dependencies: - "@open-draft/until" "^1.0.0" - "@types/cookie" "^0.3.3" - chalk "^4.0.0" - cookie "^0.4.1" - graphql "^15.0.0" - headers-utils "^1.1.9" - node-match-path "^0.4.2" - node-request-interceptor "^0.2.4" - statuses "^2.0.0" - yargs "^15.3.1" - multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" @@ -13399,11 +13372,6 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -node-match-path@^0.4.2: - version "0.4.2" - resolved "https://registry.npmjs.org/node-match-path/-/node-match-path-0.4.2.tgz#30cc39510fa493bff03c3d0d2fff711c868ec457" - integrity sha512-wfde4FOC5A8RTSUVZ7pTpBV+dJsr2vVxT6374VrNam6wnnhx6EvwAwL/E/r3AW/YU6XkeZggF5xfBlu4a/ULBg== - node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -13444,14 +13412,6 @@ node-releases@^1.1.29, node-releases@^1.1.52: dependencies: semver "^6.3.0" -node-request-interceptor@^0.2.4: - version "0.2.4" - resolved "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.2.4.tgz#f03a1b874823d0bea311a14280227707be946298" - integrity sha512-/htjDLmygBczT5qYPaSxfAEtMkc0LGuH6jqAP1o+TKfQh6yQfFyTtac25cpY8+pb4EawHljCLUN7dCeed9SdPA== - dependencies: - debug "^4.1.1" - headers-utils "^1.1.3" - nodemon@^2.0.2: version "2.0.4" resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz#55b09319eb488d6394aa9818148c0c2d1c04c416" @@ -17296,6 +17256,11 @@ start-server-and-test@^1.10.11: ps-tree "1.2.0" wait-on "4.0.0" +start-server-webpack-plugin@^2.2.5: + version "2.2.5" + resolved "https://registry.npmjs.org/start-server-webpack-plugin/-/start-server-webpack-plugin-2.2.5.tgz#4a2838759b0f36acd11b0b2f5f196f289ae29d31" + integrity sha512-DRCkciwCJoCFZ+wt3wWMkR1M2mpVhJbUKFXqhK3FWyIUKYb42NnocH5sMwqgo+nPNHupqNwK/v8lgfBbr2NKdg== + state-toggle@^1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" @@ -17314,11 +17279,6 @@ static-extend@^0.1.1: resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -statuses@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.0.tgz#aa7b107e018eb33e08e8aee2e7337e762dda1028" - integrity sha512-w9jNUUQdpuVoYqXxnyOakhckBbOxRaoYqJscyIBYCS5ixyCnO7nQn7zBZvP9zf5QOPZcz2DLUpE3KsNPbJBOFA== - stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" @@ -19057,6 +19017,11 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" +webpack-node-externals@^1.7.2: + version "1.7.2" + resolved "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-1.7.2.tgz#6e1ee79ac67c070402ba700ef033a9b8d52ac4e3" + integrity sha512-ajerHZ+BJKeCLviLUUmnyd5B4RavLF76uv3cs6KNuO8W+HuQaEs0y0L7o40NQxdPy5w0pcv8Ew7yPUAQG0UdCg== + webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: version "1.4.3" resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"