1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

build(cli): integrate nx (#16648)

* build(cli): integrate nx

* refactor(project.json): rename "bit" builds to "commercial"

* refactor(webpack.base): implement DEFAULT_PARAMS

* refactor(webpack.base): move DEFAULT_PARAMS out of buildConfig
This commit is contained in:
Addison Beck
2025-10-07 09:48:02 -04:00
committed by GitHub
parent bcc050d4de
commit ddc840027a
8 changed files with 2134 additions and 332 deletions

View File

@@ -147,6 +147,7 @@
"@nx/eslint",
"@nx/jest",
"@nx/js",
"@nx/webpack",
"@types/chrome",
"@types/firefox-webext-browser",
"@types/glob",

86
apps/cli/project.json Normal file
View File

@@ -0,0 +1,86 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "cli",
"projectType": "application",
"sourceRoot": "apps/cli/src",
"tags": ["scope:cli", "type:app"],
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "oss-dev",
"options": {
"outputPath": "dist/apps/cli",
"webpackConfig": "apps/cli/webpack.config.js",
"tsConfig": "apps/cli/tsconfig.json",
"main": "apps/cli/src/bw.ts",
"target": "node",
"compiler": "tsc"
},
"configurations": {
"oss": {
"mode": "production",
"outputPath": "dist/apps/cli/oss"
},
"oss-dev": {
"mode": "development",
"outputPath": "dist/apps/cli/oss-dev"
},
"commercial": {
"mode": "production",
"outputPath": "dist/apps/cli/commercial",
"webpackConfig": "bitwarden_license/bit-cli/webpack.config.js",
"main": "bitwarden_license/bit-cli/src/bw.ts",
"tsConfig": "bitwarden_license/bit-cli/tsconfig.json"
},
"commercial-dev": {
"mode": "development",
"outputPath": "dist/apps/cli/commercial-dev",
"webpackConfig": "bitwarden_license/bit-cli/webpack.config.js",
"main": "bitwarden_license/bit-cli/src/bw.ts",
"tsConfig": "bitwarden_license/bit-cli/tsconfig.json"
}
}
},
"serve": {
"executor": "@nx/webpack:webpack",
"defaultConfiguration": "oss-dev",
"options": {
"outputPath": "dist/apps/cli",
"webpackConfig": "apps/cli/webpack.config.js",
"tsConfig": "apps/cli/tsconfig.json",
"main": "apps/cli/src/bw.ts",
"target": "node",
"compiler": "tsc",
"watch": true
},
"configurations": {
"oss-dev": {
"mode": "development",
"outputPath": "dist/apps/cli/oss-dev"
},
"commercial-dev": {
"mode": "development",
"outputPath": "dist/apps/cli/commercial-dev",
"webpackConfig": "bitwarden_license/bit-cli/webpack.config.js",
"main": "bitwarden_license/bit-cli/src/bw.ts",
"tsConfig": "bitwarden_license/bit-cli/tsconfig.json"
}
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/cli/jest.config.js"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/cli/**/*.ts"]
}
}
}
}

View File

@@ -10,16 +10,32 @@ module.exports.getEnv = function getEnv() {
return { ENV };
};
const DEFAULT_PARAMS = {
localesPath: "./src/locales",
modulesPath: [path.resolve("../../node_modules")],
externalsModulesDir: "../../node_modules",
outputPath: path.resolve(__dirname, "build"),
watch: false,
};
/**
*
* @param {{
* configName: string;
* entry: string;
* tsConfig: string;
* outputPath?: string;
* mode?: string;
* env?: string;
* modulesPath?: string[];
* localesPath?: string;
* externalsModulesDir?: string;
* watch?: boolean;
* }} params
*/
module.exports.buildConfig = function buildConfig(params) {
const { ENV } = module.exports.getEnv();
params = { ...DEFAULT_PARAMS, ...params };
const ENV = params.env || module.exports.getEnv().ENV;
const envConfig = config.load(ENV);
config.log(`Building CLI - ${params.configName} version`);
@@ -35,7 +51,7 @@ module.exports.buildConfig = function buildConfig(params) {
const plugins = [
new CopyWebpackPlugin({
patterns: [{ from: "./src/locales", to: "locales" }],
patterns: [{ from: params.localesPath, to: "locales" }],
}),
new webpack.DefinePlugin({
"process.env.BWCLI_ENV": JSON.stringify(ENV),
@@ -61,7 +77,7 @@ module.exports.buildConfig = function buildConfig(params) {
];
const webpackConfig = {
mode: ENV,
mode: params.mode || ENV,
target: "node",
devtool: ENV === "development" ? "eval-source-map" : "source-map",
node: {
@@ -77,19 +93,19 @@ module.exports.buildConfig = function buildConfig(params) {
resolve: {
extensions: [".ts", ".js"],
symlinks: false,
modules: [path.resolve("../../node_modules")],
modules: params.modulesPath,
plugins: [new TsconfigPathsPlugin({ configFile: params.tsConfig })],
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "build"),
path: path.resolve(params.outputPath),
clean: true,
},
module: { rules: moduleRules },
plugins: plugins,
externals: [
nodeExternals({
modulesDir: "../../node_modules",
modulesDir: params.externalsModulesDir,
allowlist: [/@bitwarden/],
}),
],
@@ -97,6 +113,12 @@ module.exports.buildConfig = function buildConfig(params) {
asyncWebAssembly: true,
},
};
if (params.watch) {
webpackConfig.watch = true;
webpackConfig.watchOptions = {
ignored: /node_modules/,
poll: 1000,
};
}
return webpackConfig;
};

View File

@@ -1,7 +1,48 @@
const path = require("path");
const { buildConfig } = require("./webpack.base");
module.exports = buildConfig({
module.exports = (webpackConfig, context) => {
// Detect if called by Nx (context parameter exists)
const isNxBuild = context && context.options;
if (isNxBuild) {
// Nx build configuration
const mode = context.options.mode || "development";
if (process.env.NODE_ENV == null) {
process.env.NODE_ENV = mode;
}
const ENV = (process.env.ENV = process.env.NODE_ENV);
return buildConfig({
configName: "OSS",
entry: context.options.main || "apps/cli/src/bw.ts",
tsConfig: "tsconfig.base.json",
outputPath: path.resolve(context.context.root, context.options.outputPath),
mode: mode,
env: ENV,
modulesPath: [path.resolve("node_modules")],
localesPath: "apps/cli/src/locales",
externalsModulesDir: "node_modules",
watch: context.options.watch || false,
});
} else {
// npm build configuration
if (process.env.NODE_ENV == null) {
process.env.NODE_ENV = "development";
}
const ENV = (process.env.ENV = process.env.NODE_ENV);
const mode = ENV;
return buildConfig({
configName: "OSS",
entry: "./src/bw.ts",
tsConfig: "./tsconfig.json",
outputPath: path.resolve(__dirname, "build"),
mode: mode,
env: ENV,
modulesPath: [path.resolve("../../node_modules")],
localesPath: "./src/locales",
externalsModulesDir: "../../node_modules",
});
}
};

View File

@@ -1,7 +1,48 @@
const path = require("path");
const { buildConfig } = require("../../apps/cli/webpack.base");
module.exports = buildConfig({
module.exports = (webpackConfig, context) => {
// Detect if called by Nx (context parameter exists)
const isNxBuild = context && context.options;
if (isNxBuild) {
// Nx build configuration
const mode = context.options.mode || "development";
if (process.env.NODE_ENV == null) {
process.env.NODE_ENV = mode;
}
const ENV = (process.env.ENV = process.env.NODE_ENV);
return buildConfig({
configName: "Commercial",
entry: context.options.main || "bitwarden_license/bit-cli/src/bw.ts",
tsConfig: "tsconfig.base.json",
outputPath: path.resolve(context.context.root, context.options.outputPath),
mode: mode,
env: ENV,
modulesPath: [path.resolve("node_modules")],
localesPath: "apps/cli/src/locales",
externalsModulesDir: "node_modules",
watch: context.options.watch || false,
});
} else {
// npm build configuration
if (process.env.NODE_ENV == null) {
process.env.NODE_ENV = "development";
}
const ENV = (process.env.ENV = process.env.NODE_ENV);
const mode = ENV;
return buildConfig({
configName: "Commercial",
entry: "../../bitwarden_license/bit-cli/src/bw.ts",
tsConfig: "../../bitwarden_license/bit-cli/tsconfig.json",
outputPath: path.resolve(__dirname, "../../apps/cli/build"),
mode: mode,
env: ENV,
modulesPath: [path.resolve("../../node_modules")],
localesPath: "../../apps/cli/src/locales",
externalsModulesDir: "../../node_modules",
});
}
};

View File

@@ -0,0 +1,208 @@
# Using Nx to Build Projects
Bitwarden uses [Nx](https://nx.dev/) to make building projects from the monorepo easier. To build, lint, or test a project you'll want to reference the project's `project.json` file for availible commands and their names. Then you'll run `npx nx [your_command] [your_project] [your_options]`. Run `npx nx --help` to see availible options, there are many.
Please note: the Nx implementation is a work in progress. Not all apps support Nx yet, CI still uses the old npm builds, and we have many "legacy" libraries that use hacks to get them into the Nx project graph.
## Quick Start
### Basic Commands
```bash
# Build a project
npx nx build cli
npx nx build state # Modern libs and apps have simple, all lowercase target names
npx nx build @bitwarden/common # Legacy libs have a special naming convention and include the @bitwarden prefix
# Test a project
npx nx test cli
# Lint a project
npx nx lint cli
# Serve/watch a project (for projects with serve targets)
npx nx serve cli
# Build all projects that differ from origin/main
nx affected --target=build --base=origin/main
# Build, lint, and test every project at once
npx nx run-many --target=build,test,lint --all
# Most projects default to the "oss-dev" build, so if you need the bitwarden license build add a --configuration
npx nx build cli --configuration=commercial-dev
# If you need a production build drop the "dev" suffix
npx nx build cli --configuration=oss # or "commercial"
# Configurations can also be passed to run-many
# For example: to run all Bitwarden licensed builds
npx nx run-many --target=build,test,lint --all --configuration=commercial
# Outputs are distrubuted in a root level /dist/ folder
# Run a locally built CLI
node dist/apps/cli/oss-dev/bw.js
```
### Global Commands
```bash
# See all projects
npx nx show projects
# Run affected projects only (great for local dev and CI)
npx nx affected:build
npx nx affected:test
npx nx affected:lint
# Show dependency graph
npx nx dep-graph
```
## Library Projects
Our libraries use two different Nx integration patterns depending on their migration status.
### Legacy Libraries
Most existing libraries use a facade pattern where `project.json` delegates to existing npm scripts. This approach maintains backward compatibility with the build methods we used before introducing Nx. These libraries are considered tech debt and Platform has a focus on updating them. For an example reference `libs/common/project.json`.
These libraries use `nx:run-script` executor to call existing npm scripts:
```json
{
"targets": {
"build": {
"executor": "nx:run-script",
"options": {
"script": "build"
}
}
}
}
```
#### Available Commands for Legacy Libraries
All legacy libraries support these standardized commands:
- **`nx build <library>`** - Build the library
- **`nx build:watch <library>`** - Build and watch for changes
- **`nx clean <library>`** - Clean build artifacts
- **`nx test <library>`** - Run tests
- **`nx lint <library>`** - Run linting
### Modern Libraries
Newer libraries like `libs/state` use native Nx executors for better performance and caching.
```json
{
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/state"
}
}
}
}
```
## What Happens When You Run An Nx Command
```mermaid
flowchart TD
Start([You just ran an nx command]) --> ParseCmd[Nx parses command args]
ParseCmd --> ReadWorkspace[Nx reads nx.json, workspace configuration, cache settings, and plugins]
ReadWorkspace --> ReadProject[Nx reads project.json, finds the target configuration, and checks executor to use]
ReadProject --> CheckCache{Nx checks the cache: has this exact build been done before?}
CheckCache -->|Cache hit| UseCached[Nx uses cached outputs, copies from .nx/cache, and skips execution]
UseCached --> Done([Your command is done])
CheckCache -->|Cache miss| DetermineExecutor{Which executor is configured?}
DetermineExecutor -->|nx:run-script| FacadePattern[Legacy Facade Pattern]
DetermineExecutor -->|nx/webpack:webpack| WebpackExecutor[Webpack Executor]
DetermineExecutor -->|nx/js:tsc| TypeScriptExecutor[TypeScript Executor]
DetermineExecutor -->|nx/jest:jest| JestExecutor[Jest Executor]
DetermineExecutor -->|nx/eslint:lint| ESLintExecutor[ESLint Executor]
%% Facade Pattern Flow
FacadePattern --> ReadPackageJson[The run-script executor finds npm script to run in package.json]
ReadPackageJson --> RunNpmScript[Npm script is executed]
RunNpmScript --> NpmDelegates{What does the npm script do?}
NpmDelegates -->|TypeScript| TSCompile[TypeScript compiles to JavaScript using tsconfig.json]
NpmDelegates -->|Webpack| WebpackBuild[Webpack bundles and optimizes code]
NpmDelegates -->|Jest| JestTest[Jest executes unit tests]
TSCompile --> FacadeOutput[Outputs written to libs/LIB/dist/]
WebpackBuild --> FacadeOutput
JestTest --> FacadeOutput
FacadeOutput --> CacheResults1[Nx caches results in .nx/cache/]
%% Webpack Executor Flow
WebpackExecutor --> ReadWebpackConfig[Webpack config read from apps/cli/webpack.config.js or bit-cli/webpack.config.js]
ReadWebpackConfig --> ConfigureWebpack[Webpack configured with entry points, TypeScript paths, and plugins]
ConfigureWebpack --> WebpackProcess[Webpack resolves paths, compiles TypeScript, bundles dependencies, and applies optimizations]
WebpackProcess --> WebpackOutput[Single executable bundle written to dist/apps/cli/]
WebpackOutput --> CacheResults2[Nx caches results in .nx/cache/]
%% TypeScript Executor Flow
TypeScriptExecutor --> ReadTSConfig[TypeScript reads tsconfig.lib.json compilation options]
ReadTSConfig --> TSProcess[TypeScript performs type checking, emits declarations, and compiles to JavaScript]
TSProcess --> TSOutput[Outputs written to dist/libs/LIB/]
TSOutput --> CacheResults3[Nx caches results in .nx/cache/]
%% Jest Executor Flow
JestExecutor --> ReadJestConfig[Jest reads jest.config.js test configuration]
ReadJestConfig --> JestProcess[Jest finds test files, runs suites, and generates coverage]
JestProcess --> JestOutput[Test results and coverage reports output]
JestOutput --> CacheResults4[Nx caches results in .nx/cache/]
%% ESLint Executor Flow
ESLintExecutor --> ReadESLintConfig[ESLint reads .eslintrc.json rules and configuration]
ReadESLintConfig --> ESLintProcess[ESLint checks code style, finds issues, and applies auto-fixes]
ESLintProcess --> ESLintOutput[Lint results with errors and warnings output]
ESLintOutput --> CacheResults5[Nx caches results in .nx/cache/]
%% All paths converge
CacheResults1 --> UpdateGraph[Dependency graph updated to track project relationships]
CacheResults2 --> UpdateGraph
CacheResults3 --> UpdateGraph
CacheResults4 --> UpdateGraph
CacheResults5 --> UpdateGraph
UpdateGraph --> Done
```
## Caching and Performance
### Nx Caching
Nx automatically caches build outputs and only rebuilds what changed:
```bash
# First run builds everything
npx nx build cli
# Second run uses cache (much faster)
npx nx build cli
```
### Clearing Cache
```bash
# Clear all caches
npx nx reset
```
## Additional Resources
- [Nx Documentation](https://nx.dev/getting-started/intro)
- [Nx CLI Reference](https://nx.dev/packages/nx/documents/cli)
- [Nx Workspace Configuration](https://nx.dev/reference/project-configuration)

2032
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -170,6 +170,7 @@
"@nx/eslint": "21.3.11",
"@nx/jest": "21.3.11",
"@nx/js": "21.3.11",
"@nx/webpack": "21.3.11",
"big-integer": "1.6.52",
"braintree-web-drop-in": "1.44.0",
"buffer": "6.0.3",