Jack Hsu 0ae8665a88
feat(js): infer build-deps and watch-deps targets for incremental builds (#29609)
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.
2025-01-14 16:13:43 -05:00

148 lines
3.9 KiB
TypeScript

import {
addDependenciesToPackageJson,
NxJsonConfiguration,
ProjectGraph,
readJson,
readNxJson,
stripIndents,
Tree,
updateJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { nxVersion } from '../../utils/versions';
import { initGenerator } from './init';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
describe('@nx/vite:init', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
projectGraph = {
nodes: {},
dependencies: {},
};
});
describe('dependencies for package.json', () => {
it('should add required packages', async () => {
const existing = 'existing';
const existingVersion = '1.0.0';
addDependenciesToPackageJson(
tree,
{ '@nx/vite': nxVersion, [existing]: existingVersion },
{ [existing]: existingVersion }
);
await initGenerator(tree, {
addPlugin: true,
});
const packageJson = readJson(tree, 'package.json');
expect(packageJson).toMatchSnapshot();
});
});
describe('vitest targets', () => {
it('should add target defaults for test', async () => {
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs ??= {};
json.namedInputs.production = ['default'];
return json;
});
await initGenerator(tree, {
addPlugin: true,
});
const nxJson = readNxJson(tree);
expect(nxJson).toMatchInlineSnapshot(`
{
"affected": {
"defaultBase": "main",
},
"namedInputs": {
"production": [
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/src/test-setup.[jt]s",
],
},
"plugins": [
{
"options": {
"buildDepsTargetName": "build-deps",
"buildTargetName": "build",
"previewTargetName": "preview",
"serveStaticTargetName": "serve-static",
"serveTargetName": "serve",
"testTargetName": "test",
"typecheckTargetName": "typecheck",
"watchDepsTargetName": "watch-deps",
},
"plugin": "@nx/vite/plugin",
},
],
"targetDefaults": {
"build": {
"cache": true,
},
"lint": {
"cache": true,
},
},
}
`);
});
});
it('should add nxViteTsPaths plugin to vite config files when setupPathsPlugin is set to true', async () => {
tree.write(
'proj/vite.config.ts',
stripIndents`
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})`
);
await initGenerator(tree, {
addPlugin: true,
setupPathsPlugin: true,
});
expect(tree.read('proj/vite.config.ts').toString()).toMatchInlineSnapshot(`
"import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
plugins: [react(), nxViteTsPaths()],
});
"
`);
});
it(`should not add multiple instances of the same vite temp file glob to gitignore`, async () => {
// ARRANGE
tree.write('.gitignore', 'vite.config.*.timestamp*');
// ACT
await initGenerator(tree, {});
// ASSERT
expect(tree.read('.gitignore', 'utf-8')).toMatchInlineSnapshot(`
"vite.config.*.timestamp*
vitest.config.*.timestamp*"
`);
});
});