diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 99f2bc99ef..6dd2017a71 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -16,15 +16,16 @@ import { Command } from 'commander'; import { lazy } from '../lib/lazy'; -import { - configOption, - registerCommands as registerConfigCommands, -} from '../modules/config'; +import { registerCommands as registerConfigCommands } from '../modules/config'; import { registerPackageCommands as registerPackageBuildCommands, registerRepoCommands as registerRepoBuildCommands, registerCommands as registerBuildCommands, } from '../modules/build'; +import { + registerPackageCommands as registerPackageStartCommands, + registerRepoCommands as registerRepoStartCommands, +} from '../modules/start'; import { registerCommands as registerInfoCommands } from '../modules/info'; import { registerCommands as registerMigrateCommand } from '../modules/migrate'; import { @@ -45,6 +46,7 @@ export function registerRepoCommand(program: Command) { .command('repo [command]') .description('Command that run across an entire Backstage project'); + registerRepoStartCommands(command); registerRepoBuildCommands(command); registerRepoTestCommands(command); registerRepoLintCommands(command); @@ -56,24 +58,7 @@ export function registerScriptCommand(program: Command) { .command('package [command]') .description('Lifecycle scripts for individual packages'); - command - .command('start') - .description('Start a package for local development') - .option(...configOption) - .option('--role ', 'Run the command with an explicit package role') - .option('--check', 'Enable type checking and linting if available') - .option('--inspect [host]', 'Enable debugger in Node.js environments') - .option( - '--inspect-brk [host]', - 'Enable debugger in Node.js environments, breaking before code starts', - ) - .option( - '--require ', - 'Add a --require argument to the node process', - ) - .option('--link ', 'Link an external workspace for module resolution') - .action(lazy(() => import('./start'), 'command')); - + registerPackageStartCommands(command); registerPackageBuildCommands(command); registerPackageTestCommands(command); registerMaintenancePackageCommands(command); diff --git a/packages/cli/src/modules/start/alpha.ts b/packages/cli/src/modules/start/alpha.ts new file mode 100644 index 0000000000..cbd9ae3a28 --- /dev/null +++ b/packages/cli/src/modules/start/alpha.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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 { Command } from 'commander'; +import { createCliPlugin } from '../../wiring/factory'; +import { lazy } from '../../lib/lazy'; +import { configOption } from '../config'; + +export const buildPlugin = createCliPlugin({ + pluginId: 'build', + init: async reg => { + reg.addCommand({ + path: ['package', 'start'], + description: 'Start a package for local development', + execute: async ({ args }) => { + const command = new Command(); + + const defaultCommand = command + .option(...configOption) + .option( + '--role ', + 'Run the command with an explicit package role', + ) + .option('--check', 'Enable type checking and linting if available') + .option('--inspect [host]', 'Enable debugger in Node.js environments') + .option( + '--inspect-brk [host]', + 'Enable debugger in Node.js environments, breaking before code starts', + ) + .option( + '--require ', + 'Add a --require argument to the node process', + ) + .option( + '--link ', + 'Link an external workspace for module resolution', + ) + .action(lazy(() => import('./commands/package/start'), 'command')); + + await defaultCommand.parseAsync(args, { from: 'user' }); + }, + }); + + reg.addCommand({ + path: ['repo', 'start'], + description: 'Starts packages in the repo for local development', + execute: async () => { + throw new Error('Not implemented'); + }, + }); + }, +}); + +export default buildPlugin; diff --git a/packages/cli/src/modules/start/commands/package/start/command.ts b/packages/cli/src/modules/start/commands/package/start/command.ts new file mode 100644 index 0000000000..5a08949bf9 --- /dev/null +++ b/packages/cli/src/modules/start/commands/package/start/command.ts @@ -0,0 +1,90 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 fs from 'fs-extra'; +import { OptionValues } from 'commander'; +import { resolve as resolvePath } from 'node:path'; +import { PackageRole } from '@backstage/cli-node'; +import { findRoleFromCommand } from '../../../../../lib/role'; +import { startBackend, startBackendPlugin } from './startBackend'; +import { startFrontend } from './startFrontend'; +import { ForwardedError } from '@backstage/errors'; + +export async function command(opts: OptionValues): Promise { + const role = await findRoleFromCommand(opts); + + if (opts.link) { + const dir = resolvePath(opts.link); + if (!fs.pathExistsSync(dir)) { + throw new Error( + `Invalid workspace link, directory does not exist: ${dir}`, + ); + } + const pkgJson = await fs + .readJson(resolvePath(dir, 'package.json')) + .catch(error => { + throw new ForwardedError( + 'Failed to read package.json in linked workspace', + error, + ); + }); + + if (!pkgJson.workspaces) { + throw new Error( + `Invalid workspace link, directory is not a workspace: ${dir}`, + ); + } + } + + const options = { + configPaths: opts.config as string[], + checksEnabled: Boolean(opts.check), + linkedWorkspace: opts.link, + inspectEnabled: opts.inspect, + inspectBrkEnabled: opts.inspectBrk, + require: opts.require, + }; + + switch (role) { + case 'backend': + return startBackend(options); + case 'backend-plugin': + case 'backend-plugin-module': + case 'node-library': + return startBackendPlugin(options); + case 'frontend': + return startFrontend({ + ...options, + entry: 'src/index', + verifyVersions: true, + }); + case 'web-library': + case 'frontend-plugin': + case 'frontend-plugin-module': + return startFrontend({ entry: 'dev/index', ...options }); + case 'frontend-dynamic-container' as PackageRole: // experimental + return startFrontend({ + entry: 'src/index', + ...options, + skipOpenBrowser: true, + isModuleFederationRemote: true, + }); + default: + throw new Error( + `Start command is not supported for package role '${role}'`, + ); + } +} diff --git a/packages/cli/src/modules/start/commands/package/start/index.ts b/packages/cli/src/modules/start/commands/package/start/index.ts new file mode 100644 index 0000000000..680fe9e11d --- /dev/null +++ b/packages/cli/src/modules/start/commands/package/start/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { command } from './command'; diff --git a/packages/cli/src/modules/start/commands/package/start/startBackend.ts b/packages/cli/src/modules/start/commands/package/start/startBackend.ts new file mode 100644 index 0000000000..7c89a6d887 --- /dev/null +++ b/packages/cli/src/modules/start/commands/package/start/startBackend.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 fs from 'fs-extra'; +import { paths } from '../../../../../lib/paths'; +import { runBackend } from '../../../../../lib/runner'; + +interface StartBackendOptions { + checksEnabled: boolean; + inspectEnabled: boolean; + inspectBrkEnabled: boolean; + linkedWorkspace?: string; + require?: string; +} + +export async function startBackend(options: StartBackendOptions) { + const waitForExit = await runBackend({ + entry: 'src/index', + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + linkedWorkspace: options.linkedWorkspace, + require: options.require, + }); + + await waitForExit(); +} + +export async function startBackendPlugin(options: StartBackendOptions) { + const hasDevIndexEntry = await fs.pathExists( + paths.resolveTarget('dev', 'index.ts'), + ); + if (!hasDevIndexEntry) { + console.warn( + `The 'dev' directory is missing. Please create a proper dev/index.ts in order to start the plugin.`, + ); + return; + } + + const waitForExit = await runBackend({ + entry: 'dev/index', + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + require: options.require, + linkedWorkspace: options.linkedWorkspace, + }); + + await waitForExit(); +} diff --git a/packages/cli/src/modules/start/commands/package/start/startFrontend.ts b/packages/cli/src/modules/start/commands/package/start/startFrontend.ts new file mode 100644 index 0000000000..8abdf451b5 --- /dev/null +++ b/packages/cli/src/modules/start/commands/package/start/startFrontend.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { readJson } from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { + getModuleFederationOptions, + serveBundle, +} from '../../../../build/lib/bundler'; +import { paths } from '../../../../../lib/paths'; +import { BackstagePackageJson } from '@backstage/cli-node'; + +interface StartAppOptions { + verifyVersions?: boolean; + entry: string; + + checksEnabled: boolean; + configPaths: string[]; + skipOpenBrowser?: boolean; + isModuleFederationRemote?: boolean; + linkedWorkspace?: string; +} + +export async function startFrontend(options: StartAppOptions) { + const packageJson = (await readJson( + paths.resolveTarget('package.json'), + )) as BackstagePackageJson; + + const waitForExit = await serveBundle({ + entry: options.entry, + checksEnabled: options.checksEnabled, + configPaths: options.configPaths, + verifyVersions: options.verifyVersions, + skipOpenBrowser: options.skipOpenBrowser, + linkedWorkspace: options.linkedWorkspace, + moduleFederation: await getModuleFederationOptions( + packageJson, + resolvePath(paths.targetDir), + options.isModuleFederationRemote, + ), + }); + + await waitForExit(); +} diff --git a/packages/cli/src/modules/start/index.ts b/packages/cli/src/modules/start/index.ts new file mode 100644 index 0000000000..6724fb2af8 --- /dev/null +++ b/packages/cli/src/modules/start/index.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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 { Command } from 'commander'; +import { lazy } from '../../lib/lazy'; +import { configOption } from '../config'; + +export function registerRepoCommands(command: Command) { + command + .command('start') + .description('Starts packages in the repo for local development') + .action( + lazy( + async () => ({ + command: () => { + throw new Error('Not implemented'); + }, + }), + 'command', + ), + ); +} + +export function registerPackageCommands(command: Command) { + command + .command('start') + .description('Start a package for local development') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') + .option('--check', 'Enable type checking and linting if available') + .option('--inspect [host]', 'Enable debugger in Node.js environments') + .option( + '--inspect-brk [host]', + 'Enable debugger in Node.js environments, breaking before code starts', + ) + .option( + '--require ', + 'Add a --require argument to the node process', + ) + .option('--link ', 'Link an external workspace for module resolution') + .action(lazy(() => import('./commands/package/start'), 'command')); +}