This PR adds `build-deps` and `watch-deps` targets to buildable JS
projects to help with incremental builds.
A use-case for this is if an app (e.g. Vite React app) has buildable
dependencies that need to be rebuilt when they change.
Say, you create a React app and lib as follows:
```
nx g @nx/react:app apps/react-app --bundler vite
nx g @nx/react:lib packages/react-lib --bundler vite
```
And import `react-lib` inside the app.
```jsx
import { ReactLib } from '@acme/react-lib';
//...
return <ReactLib />
```
The user can then run:
```
nx watch-deps react-app
```
And then serve the app in another terminal:
```
nx serve react-app
```
Then whenever code is updated for a buildable dependency, it'll be
rebuilt and then reloaded in the app.
279 lines
7.9 KiB
TypeScript
279 lines
7.9 KiB
TypeScript
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
|
import { basename, dirname, join } from 'path';
|
|
import { existsSync, readdirSync } from 'fs';
|
|
import {
|
|
type CreateDependencies,
|
|
type CreateNodes,
|
|
CreateNodesContext,
|
|
createNodesFromFiles,
|
|
CreateNodesV2,
|
|
detectPackageManager,
|
|
getPackageManagerCommand,
|
|
joinPathFragments,
|
|
logger,
|
|
readJsonFile,
|
|
type TargetConfiguration,
|
|
writeJsonFile,
|
|
} from '@nx/devkit';
|
|
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
|
import { getLockFileName } from '@nx/js';
|
|
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
|
|
import { type RollupOptions } from 'rollup';
|
|
import { hashObject } from 'nx/src/hasher/file-hasher';
|
|
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
|
|
import { addBuildAndWatchDepsTargets } from '@nx/js/src/plugins/typescript/util';
|
|
|
|
const pmc = getPackageManagerCommand();
|
|
|
|
function readTargetsCache(
|
|
cachePath: string
|
|
): Record<string, Record<string, TargetConfiguration>> {
|
|
return existsSync(cachePath) ? readJsonFile(cachePath) : {};
|
|
}
|
|
|
|
function writeTargetsToCache(
|
|
cachePath: string,
|
|
results: Record<string, Record<string, TargetConfiguration>>
|
|
) {
|
|
writeJsonFile(cachePath, results);
|
|
}
|
|
|
|
/**
|
|
* @deprecated The 'createDependencies' function is now a no-op. This functionality is included in 'createNodesV2'.
|
|
*/
|
|
export const createDependencies: CreateDependencies = () => {
|
|
return [];
|
|
};
|
|
|
|
export interface RollupPluginOptions {
|
|
buildTargetName?: string;
|
|
buildDepsTargetName?: string;
|
|
watchDepsTargetName?: string;
|
|
}
|
|
|
|
const rollupConfigGlob = '**/rollup.config.{js,cjs,mjs,ts,cts,mts}';
|
|
|
|
export const createNodes: CreateNodes<RollupPluginOptions> = [
|
|
rollupConfigGlob,
|
|
async (configFilePath, options, context) => {
|
|
logger.warn(
|
|
'`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'
|
|
);
|
|
return createNodesInternal(
|
|
configFilePath,
|
|
normalizeOptions(options),
|
|
context,
|
|
{},
|
|
isUsingTsSolutionSetup()
|
|
);
|
|
},
|
|
];
|
|
|
|
export const createNodesV2: CreateNodesV2<RollupPluginOptions> = [
|
|
rollupConfigGlob,
|
|
async (configFilePaths, options, context) => {
|
|
const normalizedOptions = normalizeOptions(options);
|
|
const optionsHash = hashObject(normalizedOptions);
|
|
const cachePath = join(
|
|
workspaceDataDirectory,
|
|
`rollup-${optionsHash}.hash`
|
|
);
|
|
const targetsCache = readTargetsCache(cachePath);
|
|
const isTsSolutionSetup = isUsingTsSolutionSetup();
|
|
|
|
try {
|
|
return await createNodesFromFiles(
|
|
(configFile, _, context) =>
|
|
createNodesInternal(
|
|
configFile,
|
|
normalizedOptions,
|
|
context,
|
|
targetsCache,
|
|
isTsSolutionSetup
|
|
),
|
|
configFilePaths,
|
|
normalizedOptions,
|
|
context
|
|
);
|
|
} finally {
|
|
writeTargetsToCache(cachePath, targetsCache);
|
|
}
|
|
},
|
|
];
|
|
|
|
async function createNodesInternal(
|
|
configFilePath: string,
|
|
options: Required<RollupPluginOptions>,
|
|
context: CreateNodesContext,
|
|
targetsCache: Record<string, Record<string, TargetConfiguration>>,
|
|
isTsSolutionSetup: boolean
|
|
) {
|
|
const projectRoot = dirname(configFilePath);
|
|
const fullyQualifiedProjectRoot = join(context.workspaceRoot, projectRoot);
|
|
|
|
// Do not create a project if package.json and project.json do not exist
|
|
const siblingFiles = readdirSync(fullyQualifiedProjectRoot);
|
|
if (
|
|
!siblingFiles.includes('package.json') &&
|
|
!siblingFiles.includes('project.json')
|
|
) {
|
|
return {};
|
|
}
|
|
|
|
const hash = await calculateHashForCreateNodes(
|
|
projectRoot,
|
|
options,
|
|
context,
|
|
[getLockFileName(detectPackageManager(context.workspaceRoot))]
|
|
);
|
|
|
|
targetsCache[hash] ??= await buildRollupTarget(
|
|
configFilePath,
|
|
projectRoot,
|
|
options,
|
|
context,
|
|
isTsSolutionSetup
|
|
);
|
|
|
|
return {
|
|
projects: {
|
|
[projectRoot]: {
|
|
root: projectRoot,
|
|
targets: targetsCache[hash],
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
async function buildRollupTarget(
|
|
configFilePath: string,
|
|
projectRoot: string,
|
|
options: RollupPluginOptions,
|
|
context: CreateNodesContext,
|
|
isTsSolutionSetup: boolean
|
|
): Promise<Record<string, TargetConfiguration>> {
|
|
let loadConfigFile: (
|
|
path: string,
|
|
commandOptions: unknown,
|
|
watchMode: boolean
|
|
) => Promise<{ options: RollupOptions[] }>;
|
|
|
|
try {
|
|
// Try to load the workspace version of rollup first (it should already exist).
|
|
// Using the workspace rollup ensures that the config file is compatible with the `loadConfigFile` function.
|
|
// e.g. rollup@2 supports having `require` calls in rollup config, but rollup@4 does not.
|
|
const m = require(require.resolve('rollup/loadConfigFile', {
|
|
paths: [dirname(configFilePath)],
|
|
}));
|
|
// Rollup 2 has this has default export, but it is named in 3 and 4.
|
|
// See: https://www.unpkg.com/browse/rollup@2.79.1/dist/loadConfigFile.js
|
|
loadConfigFile = typeof m === 'function' ? m : m.loadConfigFile;
|
|
} catch {
|
|
// Fallback to our own if needed.
|
|
loadConfigFile = require('rollup/loadConfigFile').loadConfigFile;
|
|
}
|
|
|
|
const namedInputs = getNamedInputs(projectRoot, context);
|
|
const rollupConfig = (
|
|
(await loadConfigFile(
|
|
joinPathFragments(context.workspaceRoot, configFilePath),
|
|
{},
|
|
true // Enable watch mode so that rollup properly reloads config files without reusing a cached version
|
|
)) as { options: RollupOptions[] }
|
|
).options;
|
|
const outputs = getOutputs(rollupConfig, projectRoot);
|
|
|
|
const targets: Record<string, TargetConfiguration> = {};
|
|
targets[options.buildTargetName] = {
|
|
command: `rollup -c ${basename(configFilePath)}${
|
|
configFilePath.endsWith('ts')
|
|
? ' --configPlugin @rollup/plugin-typescript'
|
|
: ''
|
|
}`,
|
|
options: { cwd: projectRoot },
|
|
cache: true,
|
|
dependsOn: [`^${options.buildTargetName}`],
|
|
inputs: [
|
|
...('production' in namedInputs
|
|
? ['production', '^production']
|
|
: ['default', '^default']),
|
|
{ externalDependencies: ['rollup'] },
|
|
],
|
|
outputs,
|
|
metadata: {
|
|
technologies: ['rollup'],
|
|
description: 'Run Rollup',
|
|
help: {
|
|
command: `${pmc.exec} rollup --help`,
|
|
example: {
|
|
options: {
|
|
sourcemap: true,
|
|
watch: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
if (isTsSolutionSetup) {
|
|
targets[options.buildTargetName].syncGenerators = [
|
|
'@nx/js:typescript-sync',
|
|
];
|
|
}
|
|
|
|
addBuildAndWatchDepsTargets(
|
|
context.workspaceRoot,
|
|
projectRoot,
|
|
targets,
|
|
options,
|
|
pmc
|
|
);
|
|
|
|
return targets;
|
|
}
|
|
|
|
function getOutputs(
|
|
rollupConfigs: RollupOptions[],
|
|
projectRoot: string
|
|
): string[] {
|
|
const outputs = new Set<string>();
|
|
for (const rollupConfig of rollupConfigs) {
|
|
if (rollupConfig.output) {
|
|
const rollupConfigOutputs = [];
|
|
if (Array.isArray(rollupConfig.output)) {
|
|
rollupConfigOutputs.push(...rollupConfig.output);
|
|
} else {
|
|
rollupConfigOutputs.push(rollupConfig.output);
|
|
}
|
|
|
|
for (const output of rollupConfigOutputs) {
|
|
const outputPathFromConfig = output.dir
|
|
? output.dir
|
|
: output.file
|
|
? dirname(output.file)
|
|
: 'dist';
|
|
const outputPath =
|
|
projectRoot === '.'
|
|
? joinPathFragments(`{workspaceRoot}`, outputPathFromConfig)
|
|
: joinPathFragments(
|
|
`{workspaceRoot}`,
|
|
projectRoot,
|
|
outputPathFromConfig
|
|
);
|
|
outputs.add(outputPath);
|
|
}
|
|
}
|
|
}
|
|
return Array.from(outputs);
|
|
}
|
|
|
|
function normalizeOptions(
|
|
options: RollupPluginOptions
|
|
): Required<RollupPluginOptions> {
|
|
return {
|
|
buildTargetName: options.buildTargetName ?? 'build',
|
|
buildDepsTargetName: options.buildDepsTargetName ?? 'build-deps',
|
|
watchDepsTargetName: options.watchDepsTargetName ?? 'watch-deps',
|
|
};
|
|
}
|