Skip to content

Commit

Permalink
cli: switch around handling of external linked modules
Browse files Browse the repository at this point in the history
  • Loading branch information
Rugvip committed Dec 12, 2020
1 parent 8226e06 commit c36a01b
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/swift-sheep-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---

Re-enable symlink resolution during bundling, and switch to using a resolve plugin for external linked packages.
96 changes: 75 additions & 21 deletions packages/cli/src/lib/bundler/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
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 webpack, { ResolvePlugin } from 'webpack';
import nodeExternals from 'webpack-node-externals';
import { optimization } from './optimization';
import { Config } from '@backstage/config';
Expand Down Expand Up @@ -70,27 +70,88 @@ async function readBuildInfo() {
};
}

async function loadLernaPackages(): Promise<
{ name: string; location: string }[]
> {
type LernaPackage = {
name: string;
location: string;
};

async function loadLernaPackages(): Promise<LernaPackage[]> {
const LernaProject = require('@lerna/project');
const project = new LernaProject(cliPaths.targetDir);
return project.getPackages();
}

// Enables proper resolution of packages when linking in external packages.
// Without this the packages would depend on dependencies in the node_modules
// of the external packages themselves, leading to module duplication
class LinkedPackageResolvePlugin implements ResolvePlugin {
constructor(
private readonly targetModules: string,
private readonly packages: LernaPackage[],
) {}

apply(resolver: any) {
resolver.hooks.resolve.tapAsync(
'LinkedPackageResolvePlugin',
(
request: { path?: false | string; context?: { issuer?: string } },
context: unknown,
callback: () => void,
) => {
const pkg = this.packages.find(
pkg => request.path && request.path.startsWith(pkg.location),
);
if (!pkg) {
callback();
return;
}

// pkg here is an external package. We rewrite the context of any imports to resolve
// from the location of the package within the node_modules of the target root rather
// than the real location of the external package.
const modulesLocation = resolvePath(this.targetModules, pkg.name);
const newContext = request.context?.issuer
? {
...request.context,
issuer: request.context.issuer.replace(
pkg.location,
modulesLocation,
),
}
: request.context;

// Re-run resolution but this time from the point of view of our target monorepo rather
// than the location of the external package. By resolving modules using this method we avoid
// pulling in e.g. `react` from the external repo, which would otherwise lead to conflicts.
resolver.doResolve(
resolver.hooks.resolve,
{
...request,
context: newContext,
path:
request.path &&
request.path.replace(pkg.location, modulesLocation),
},
null,
context,
callback,
);
},
);
}
}

export async function createConfig(
paths: BundlingPaths,
options: BundlingOptions,
): Promise<webpack.Configuration> {
const { checksEnabled, isDev, frontendConfig } = options;

const packages = await loadLernaPackages();
const { plugins, loaders } = transforms({
...options,
externalTransforms: packages.map(({ name }) =>
cliPaths.resolveTargetRoot('node_modules', name),
),
});
const { plugins, loaders } = transforms(options);
// Any package that is part of the monorepo but outside the monorepo root dir need
// separate resolution logic.
const externalPkgs = packages.filter(p => !p.location.startsWith(paths.root));

const baseUrl = frontendConfig.getString('app.baseUrl');
const validBaseUrl = new URL(baseUrl);
Expand Down Expand Up @@ -165,6 +226,7 @@ export async function createConfig(
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
mainFields: ['browser', 'module', 'main'],
plugins: [
new LinkedPackageResolvePlugin(paths.rootNodeModules, externalPkgs),
new ModuleScopePlugin(
[paths.targetSrc, paths.targetDev],
[paths.targetPackageJson],
Expand All @@ -173,10 +235,6 @@ export async function createConfig(
alias: {
'react-dom': '@hot-loader/react-dom',
},
// Enables proper resolution of packages when linking in external packages.
// Without this the packages would depend on dependencies in the node_modules
// of the external packages themselves, leading to module duplication
symlinks: false,
},
module: {
rules: loaders,
Expand Down Expand Up @@ -205,13 +263,9 @@ export async function createBackendConfig(
const moduleDirs = packages.map((p: any) =>
resolvePath(p.location, 'node_modules'),
);
const externalPkgs = packages.filter(p => !p.location.startsWith(paths.root)); // See frontend config

const { loaders } = transforms({
...options,
externalTransforms: packages.map(({ name }) =>
cliPaths.resolveTargetRoot('node_modules', name),
),
});
const { loaders } = transforms(options);

return {
mode: isDev ? 'development' : 'production',
Expand Down Expand Up @@ -253,6 +307,7 @@ export async function createBackendConfig(
mainFields: ['browser', 'module', 'main'],
modules: [paths.rootNodeModules, ...moduleDirs],
plugins: [
new LinkedPackageResolvePlugin(paths.rootNodeModules, externalPkgs),
new ModuleScopePlugin(
[paths.targetSrc, paths.targetDev],
[paths.targetPackageJson],
Expand All @@ -261,7 +316,6 @@ export async function createBackendConfig(
alias: {
'react-dom': '@hot-loader/react-dom',
},
symlinks: false, // See frontend config, added here for the same reason
},
module: {
rules: loaders,
Expand Down
17 changes: 3 additions & 14 deletions packages/cli/src/lib/bundler/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,17 @@ type Transforms = {

type TransformOptions = {
isDev: boolean;
// External paths that should be transformed
externalTransforms: string[];
};

export const transforms = (options: TransformOptions): Transforms => {
const { isDev, externalTransforms } = options;
const { isDev } = options;

const extraTransforms = isDev ? ['react-hot-loader'] : [];

const transformExcludeCondition = {
or: [
// This makes sure we don't transform node_modules inside any of the local monorepo packages
/node_modules.*node_modules/,
// This excludes the local monorepo packages from the excludes, meaning they will be transformed
{ and: [/node_modules/, { not: externalTransforms }] },
],
};

const loaders = [
{
test: /\.(tsx?)$/,
exclude: transformExcludeCondition,
exclude: /node_modules/,
loader: require.resolve('@sucrase/webpack-loader'),
options: {
transforms: ['typescript', 'jsx', ...extraTransforms],
Expand All @@ -55,7 +44,7 @@ export const transforms = (options: TransformOptions): Transforms => {
},
{
test: /\.(jsx?|mjs)$/,
exclude: transformExcludeCondition,
exclude: /node_modules/,
loader: require.resolve('@sucrase/webpack-loader'),
options: {
transforms: ['jsx', ...extraTransforms],
Expand Down

0 comments on commit c36a01b

Please sign in to comment.