Jack Hsu d8cb932422
fix(webpack): handle relative paths for additionalEntryPath (#27885)
The `NxAppWebpackPlugin` does not support relative paths in
`additionalEntryPoints`.

So this will fail:

```js
new NxAppWebpackPlugin({
  ...
  additionalEntryPoints: ['.src/foo.ts']
```

The resolved path is relative to workspace root when it should be
project root.

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
Build will fail.
## Expected Behavior
Build should work.

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
2024-09-11 16:37:25 -04:00

237 lines
8.0 KiB
TypeScript

import { basename, dirname, join, parse, relative, resolve } from 'path';
import { statSync } from 'fs';
import {
normalizePath,
parseTargetString,
readCachedProjectGraph,
workspaceRoot,
} from '@nx/devkit';
import {
AssetGlobPattern,
FileReplacement,
NormalizedNxAppWebpackPluginOptions,
NxAppWebpackPluginOptions,
} from '../nx-app-webpack-plugin-options';
export function normalizeOptions(
options: NxAppWebpackPluginOptions
): NormalizedNxAppWebpackPluginOptions {
const combinedPluginAndMaybeExecutorOptions: Partial<NormalizedNxAppWebpackPluginOptions> =
{};
const isProd = process.env.NODE_ENV === 'production';
// Since this is invoked by the executor, the graph has already been created and cached.
const projectGraph = readCachedProjectGraph();
const taskDetailsFromBuildTarget = process.env.NX_BUILD_TARGET
? parseTargetString(process.env.NX_BUILD_TARGET, projectGraph)
: undefined;
const projectName = taskDetailsFromBuildTarget
? taskDetailsFromBuildTarget.project
: process.env.NX_TASK_TARGET_PROJECT;
const targetName = taskDetailsFromBuildTarget
? taskDetailsFromBuildTarget.target
: process.env.NX_TASK_TARGET_TARGET;
const configurationName = taskDetailsFromBuildTarget
? taskDetailsFromBuildTarget.configuration
: process.env.NX_TASK_TARGET_CONFIGURATION;
const projectNode = projectGraph.nodes[projectName];
const targetConfig = projectNode.data.targets[targetName];
normalizeRelativePaths(projectNode.data.root, options);
// Merge options from `@nx/webpack:webpack` into plugin options.
// Options from `@nx/webpack:webpack` take precedence.
const originalTargetOptions = targetConfig.options;
if (configurationName) {
Object.assign(
originalTargetOptions,
targetConfig.configurations?.[configurationName]
);
}
// This could be called from dev-server which means we need to read `buildTarget` to get actual build options.
// Otherwise, the options are passed from the `@nx/webpack:webpack` executor.
if (originalTargetOptions.buildTarget) {
const buildTargetOptions = targetConfig.options;
if (configurationName) {
Object.assign(
buildTargetOptions,
targetConfig.configurations?.[configurationName]
);
}
Object.assign(
combinedPluginAndMaybeExecutorOptions,
options,
// executor options take precedence (especially for overriding with CLI args)
buildTargetOptions
);
} else {
Object.assign(
combinedPluginAndMaybeExecutorOptions,
options,
// executor options take precedence (especially for overriding with CLI args)
originalTargetOptions
);
}
const sourceRoot = projectNode.data.sourceRoot ?? projectNode.data.root;
if (!combinedPluginAndMaybeExecutorOptions.main) {
throw new Error(
`Missing "main" option for the entry file. Set this option in your Nx webpack plugin.`
);
}
return {
...combinedPluginAndMaybeExecutorOptions,
assets: combinedPluginAndMaybeExecutorOptions.assets
? normalizeAssets(
combinedPluginAndMaybeExecutorOptions.assets,
workspaceRoot,
sourceRoot,
projectNode.data.root
)
: [],
baseHref: combinedPluginAndMaybeExecutorOptions.baseHref ?? '/',
buildLibsFromSource:
combinedPluginAndMaybeExecutorOptions.buildLibsFromSource ?? true,
commonChunk: combinedPluginAndMaybeExecutorOptions.commonChunk ?? true,
compiler: combinedPluginAndMaybeExecutorOptions.compiler ?? 'babel',
configurationName,
deleteOutputPath:
combinedPluginAndMaybeExecutorOptions.deleteOutputPath ?? true,
extractCss: combinedPluginAndMaybeExecutorOptions.extractCss ?? true,
fileReplacements: normalizeFileReplacements(
workspaceRoot,
combinedPluginAndMaybeExecutorOptions.fileReplacements
),
generateIndexHtml:
combinedPluginAndMaybeExecutorOptions.generateIndexHtml ?? true,
main: combinedPluginAndMaybeExecutorOptions.main,
namedChunks: combinedPluginAndMaybeExecutorOptions.namedChunks ?? !isProd,
optimization: combinedPluginAndMaybeExecutorOptions.optimization ?? isProd,
outputFileName:
combinedPluginAndMaybeExecutorOptions.outputFileName ?? 'main.js',
outputHashing:
combinedPluginAndMaybeExecutorOptions.outputHashing ??
(isProd ? 'all' : 'none'),
outputPath: combinedPluginAndMaybeExecutorOptions.outputPath,
projectGraph,
projectName,
projectRoot: projectNode.data.root,
root: workspaceRoot,
runtimeChunk: combinedPluginAndMaybeExecutorOptions.runtimeChunk ?? true,
scripts: combinedPluginAndMaybeExecutorOptions.scripts ?? [],
sourceMap: combinedPluginAndMaybeExecutorOptions.sourceMap ?? !isProd,
sourceRoot,
styles: combinedPluginAndMaybeExecutorOptions.styles ?? [],
target: combinedPluginAndMaybeExecutorOptions.target,
targetName,
vendorChunk: combinedPluginAndMaybeExecutorOptions.vendorChunk ?? !isProd,
};
}
export function normalizeAssets(
assets: any[],
root: string,
sourceRoot: string,
projectRoot: string,
resolveRelativePathsToProjectRoot = true
): AssetGlobPattern[] {
return assets.map((asset) => {
if (typeof asset === 'string') {
const assetPath = normalizePath(asset);
const resolvedAssetPath = resolve(root, assetPath);
const resolvedSourceRoot = resolve(root, sourceRoot);
if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
throw new Error(
`The ${resolvedAssetPath} asset path must start with the project source root: ${sourceRoot}`
);
}
const isDirectory = statSync(resolvedAssetPath).isDirectory();
const input = isDirectory
? resolvedAssetPath
: dirname(resolvedAssetPath);
const output = relative(resolvedSourceRoot, resolve(root, input));
const glob = isDirectory ? '**/*' : basename(resolvedAssetPath);
return {
input,
output,
glob,
};
} else {
if (asset.output.startsWith('..')) {
throw new Error(
'An asset cannot be written to a location outside of the output path.'
);
}
const assetPath = normalizePath(asset.input);
let resolvedAssetPath = resolve(root, assetPath);
if (resolveRelativePathsToProjectRoot && asset.input.startsWith('.')) {
const resolvedProjectRoot = resolve(root, projectRoot);
resolvedAssetPath = resolve(resolvedProjectRoot, assetPath);
}
return {
...asset,
input: resolvedAssetPath,
// Now we remove starting slash to make Webpack place it from the output root.
output: asset.output.replace(/^\//, ''),
};
}
});
}
export function normalizeFileReplacements(
root: string,
fileReplacements: FileReplacement[]
): FileReplacement[] {
return fileReplacements
? fileReplacements.map((fileReplacement) => ({
replace: resolve(root, fileReplacement.replace),
with: resolve(root, fileReplacement.with),
}))
: [];
}
function normalizeRelativePaths(
projectRoot: string,
options: NxAppWebpackPluginOptions
): void {
for (const [fieldName, fieldValue] of Object.entries(options)) {
if (isRelativePath(fieldValue)) {
options[fieldName] = join(projectRoot, fieldValue);
} else if (fieldName === 'additionalEntryPoints') {
for (let i = 0; i < fieldValue.length; i++) {
const v = fieldValue[i];
if (isRelativePath(v)) {
fieldValue[i] = {
entryName: parse(v).name,
entryPath: join(projectRoot, v),
};
} else if (isRelativePath(v.entryPath)) {
v.entryPath = join(projectRoot, v.entryPath);
}
}
} else if (Array.isArray(fieldValue)) {
for (let i = 0; i < fieldValue.length; i++) {
if (isRelativePath(fieldValue[i])) {
fieldValue[i] = join(projectRoot, fieldValue[i]);
}
}
}
}
}
function isRelativePath(val: unknown): boolean {
return (
typeof val === 'string' &&
(val.startsWith('./') ||
// Windows
val.startsWith('.\\'))
);
}