cli: add --link option for frontend start

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-10-22 11:38:14 +02:00
parent d6bdcb8876
commit feef0126fc
6 changed files with 85 additions and 0 deletions
+1
View File
@@ -138,6 +138,7 @@ export function registerScriptCommand(program: Command) {
'Enable debugger in Node.js environments, breaking before code starts',
)
.option('--require <path>', 'Add a --require argument to the node process')
.option('--link <path>', 'Link an external workspace for module resolution')
.action(lazy(() => import('./start').then(m => m.command)));
command
@@ -26,6 +26,7 @@ export async function command(opts: OptionValues): Promise<void> {
const options = {
configPaths: opts.config as string[],
checksEnabled: Boolean(opts.check),
linkedWorkspace: opts.link,
inspectEnabled: opts.inspect,
inspectBrkEnabled: opts.inspectBrk,
require: opts.require,
@@ -33,10 +34,20 @@ export async function command(opts: OptionValues): Promise<void> {
switch (role) {
case 'backend':
if (options.linkedWorkspace) {
throw new Error(
'The --link flag is not supported for this package role',
);
}
return startBackend(options);
case 'backend-plugin':
case 'backend-plugin-module':
case 'node-library':
if (options.linkedWorkspace) {
throw new Error(
'The --link flag is not supported for this package role',
);
}
return startBackendPlugin(options);
case 'frontend':
return startFrontend({
@@ -26,6 +26,7 @@ interface StartAppOptions {
configPaths: string[];
skipOpenBrowser?: boolean;
isModuleFederationRemote?: boolean;
linkedWorkspace?: string;
}
export async function startFrontend(options: StartAppOptions) {
@@ -37,6 +38,7 @@ export async function startFrontend(options: StartAppOptions) {
configPaths: options.configPaths,
verifyVersions: options.verifyVersions,
skipOpenBrowser: options.skipOpenBrowser,
linkedWorkspace: options.linkedWorkspace,
moduleFederation: getModuleFederationOptions(
name,
options.isModuleFederationRemote,
+10
View File
@@ -36,6 +36,7 @@ import { transforms } from './transforms';
import { version } from '../../lib/version';
import yn from 'yn';
import { hasReactDomClient } from './hasReactDomClient';
import { createWorkspaceLinkingPlugins } from './linkWorkspaces';
const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE';
@@ -295,6 +296,15 @@ export async function createConfig(
}),
);
if (options.linkedWorkspace) {
plugins.push(
...(await createWorkspaceLinkingPlugins(
bundler,
options.linkedWorkspace,
)),
);
}
// These files are required by the transpiled code when using React Refresh.
// They need to be excluded to the module scope plugin which ensures that files
// that exist in the package are required.
@@ -0,0 +1,57 @@
/*
* 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 { relative as relativePath } from 'path';
import { getPackages } from '@manypkg/get-packages';
import webpack from 'webpack';
import { paths } from '../paths';
/**
* This returns of collection of plugins that links a separate workspace into
* the target one. Any packages that are present in the linked workspaces will
* always be used in place of the ones in the target workspace, with the exception
* of react and react-dom which are always resolved from the target workspace.
*/
export async function createWorkspaceLinkingPlugins(
bundler: typeof webpack,
workspace: string,
) {
const { packages: linkedPackages, root: linkedRoot } = await getPackages(
workspace,
);
// Matches all packages in the linked workspaces, as well as sub-path exports from them
const replacementRegex = new RegExp(
`^(?:${linkedPackages
.map(pkg => pkg.packageJson.name)
.join('|')})(?:/.*)?$`,
);
return [
// Any imports of a package that is present in the linked workspace will
// be redirected to be resolved within the context of the linked workspace
new bundler.NormalModuleReplacementPlugin(replacementRegex, resource => {
resource.context = linkedRoot.dir;
}),
// react and react-dom are always resolved from the target directory
// Note: this often requires that the linked and target workspace use the same versions of React
new bundler.NormalModuleReplacementPlugin(/^react(?:-dom)?$/, resource => {
if (!relativePath(linkedRoot.dir, resource.context).startsWith('..')) {
resource.context = paths.targetDir;
}
}),
];
}
+4
View File
@@ -36,6 +36,8 @@ export type BundlingOptions = {
publicSubPath?: string;
// Mode that the app is running in, 'protected' or 'public', default is 'public'
appMode?: string;
// An external linked workspace to include in the bundling
linkedWorkspace?: string;
moduleFederation?: ModuleFederationOptions;
rspack?: typeof import('@rspack/core').rspack;
};
@@ -46,6 +48,8 @@ export type ServeOptions = BundlingPathsOptions & {
verifyVersions?: boolean;
skipOpenBrowser?: boolean;
moduleFederation?: ModuleFederationOptions;
// An external linked workspace to include in the bundling
linkedWorkspace?: string;
};
export type BuildOptions = BundlingPathsOptions & {