207 lines
5.5 KiB
TypeScript

import {
CreateDependencies,
CreateNodes,
CreateNodesContext,
detectPackageManager,
readJsonFile,
TargetConfiguration,
workspaceRoot,
writeJsonFile,
} from '@nx/devkit';
import { dirname, isAbsolute, join, relative, resolve } from 'path';
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
import { WebpackExecutorOptions } from '../executors/webpack/schema';
import { WebDevServerOptions } from '../executors/dev-server/schema';
import { existsSync, readdirSync } from 'fs';
import { readWebpackOptions } from '../utils/webpack/read-webpack-options';
import { resolveUserDefinedWebpackConfig } from '../utils/webpack/resolve-user-defined-webpack-config';
import { getLockFileName, getRootTsConfigPath } from '@nx/js';
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
export interface WebpackPluginOptions {
buildTargetName?: string;
serveTargetName?: string;
serveStaticTargetName?: string;
previewTargetName?: string;
}
const cachePath = join(projectGraphCacheDirectory, 'webpack.hash');
const targetsCache = existsSync(cachePath) ? readTargetsCache() : {};
const calculatedTargets: Record<
string,
Record<string, TargetConfiguration>
> = {};
function readTargetsCache(): Record<
string,
Record<string, TargetConfiguration>
> {
return readJsonFile(cachePath);
}
function writeTargetsToCache(
targets: Record<string, Record<string, TargetConfiguration>>
) {
writeJsonFile(cachePath, targets);
}
export const createDependencies: CreateDependencies = () => {
writeTargetsToCache(calculatedTargets);
return [];
};
export const createNodes: CreateNodes<WebpackPluginOptions> = [
'**/webpack.config.{js,ts,mjs,cjs}',
async (configFilePath, options, context) => {
options ??= {};
options.buildTargetName ??= 'build';
options.serveTargetName ??= 'serve';
options.serveStaticTargetName ??= 'serve-static';
options.previewTargetName ??= 'preview';
const projectRoot = dirname(configFilePath);
// Do not create a project if package.json and project.json isn't there.
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if (
!siblingFiles.includes('package.json') &&
!siblingFiles.includes('project.json')
) {
return {};
}
const hash = calculateHashForCreateNodes(projectRoot, options, context, [
getLockFileName(detectPackageManager(context.workspaceRoot)),
]);
const targets = targetsCache[hash]
? targetsCache[hash]
: await createWebpackTargets(
configFilePath,
projectRoot,
options,
context
);
return {
projects: {
[projectRoot]: {
projectType: 'application',
targets,
},
},
};
},
];
async function createWebpackTargets(
configFilePath: string,
projectRoot: string,
options: WebpackPluginOptions,
context: CreateNodesContext
): Promise<
Record<
string,
TargetConfiguration<WebpackExecutorOptions | WebDevServerOptions>
>
> {
const namedInputs = getNamedInputs(projectRoot, context);
const webpackConfig = resolveUserDefinedWebpackConfig(
join(context.workspaceRoot, configFilePath),
getRootTsConfigPath(),
true
);
const webpackOptions = await readWebpackOptions(webpackConfig);
const outputPath = normalizeOutputPath(
webpackOptions.output?.path,
projectRoot
);
const targets = {};
targets[options.buildTargetName] = {
command: `webpack-cli build`,
options: { cwd: projectRoot, args: ['--node-env=production'] },
cache: true,
dependsOn: [`^${options.buildTargetName}`],
inputs:
'production' in namedInputs
? [
'production',
'^production',
{
externalDependencies: ['webpack-cli'],
},
]
: [
'default',
'^default',
{
externalDependencies: ['webpack-cli'],
},
],
outputs: [outputPath],
};
targets[options.serveTargetName] = {
command: `webpack-cli serve`,
options: {
cwd: projectRoot,
args: ['--node-env=development'],
},
};
targets[options.previewTargetName] = {
command: `webpack-cli serve`,
options: {
cwd: projectRoot,
args: ['--node-env=production'],
},
};
targets[options.serveStaticTargetName] = {
executor: '@nx/web:file-server',
options: {
buildTarget: options.buildTargetName,
spa: true,
},
};
return targets;
}
function normalizeOutputPath(
outputPath: string | undefined,
projectRoot: string
): string | undefined {
if (!outputPath) {
// If outputPath is undefined, use webpack's default `dist` directory.
if (projectRoot === '.') {
return `{projectRoot}/dist`;
} else {
return `{workspaceRoot}/dist/{projectRoot}`;
}
} else {
if (isAbsolute(outputPath)) {
/**
* If outputPath is absolute, we need to resolve it relative to the workspaceRoot first.
* After that, we can use the relative path to the workspaceRoot token {workspaceRoot} to generate the output path.
*/
return `{workspaceRoot}/${relative(
workspaceRoot,
resolve(workspaceRoot, outputPath)
)}`;
} else {
if (outputPath.startsWith('..')) {
return join('{workspaceRoot}', join(projectRoot, outputPath));
} else {
return join('{projectRoot}', outputPath);
}
}
}
}