-
Notifications
You must be signed in to change notification settings - Fork 1.4k
build(nx): add basic-lib generator for streamlined library creation #14992
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,3 +34,4 @@ | |
./apps/browser/src/safari/safari/Info.plist | ||
./apps/browser/src/safari/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist | ||
./SECURITY.md | ||
./libs/nx-plugin/src/generators/files/README.md__tmpl__ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might need a better way to do this in the future as we add more READMEs, more generators, other languages, etc. Maybe the no-capital-letters rule in general doesn't fit our project structure? I've let this be for now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const nxPreset = require("@nx/jest/preset").default; | ||
|
||
module.exports = { ...nxPreset }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
const { pathsToModuleNameMapper } = require("ts-jest"); | ||
|
||
const { compilerOptions } = require("../../../../tsconfig.base"); | ||
const { compilerOptions } = require("../../../tsconfig.base"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This started throwing errors on me. Not sure how it's not failing on main. |
||
|
||
const sharedConfig = require("../../shared/jest.config.angular"); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# nx-plugin | ||
|
||
Owned by: Platform | ||
|
||
Custom Nx tools like generators and executors for Bitwarden projects |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import baseConfig from "../../eslint.config.mjs"; | ||
|
||
export default [...baseConfig]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"generators": { | ||
"basic-lib": { | ||
"factory": "./src/generators/basic-lib", | ||
"schema": "./src/generators/schema.json", | ||
"description": "basic-lib generator" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export default { | ||
displayName: "nx-plugin", | ||
preset: "../../jest.preset.js", | ||
testEnvironment: "node", | ||
transform: { | ||
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }], | ||
}, | ||
moduleFileExtensions: ["ts", "js", "html"], | ||
coverageDirectory: "../../coverage/libs/nx-plugin", | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"name": "@bitwarden/nx-plugin", | ||
"version": "0.0.1", | ||
"description": "Custom Nx tools like generators and executors for Bitwarden projects", | ||
"private": true, | ||
"type": "commonjs", | ||
"main": "./src/index.js", | ||
"types": "./src/index.d.ts", | ||
"license": "GPL-3.0", | ||
"author": "Platform", | ||
"generators": "./generators.json" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
{ | ||
"name": "nx-plugin", | ||
"$schema": "../../node_modules/nx/schemas/project-schema.json", | ||
"sourceRoot": "libs/nx-plugin/src", | ||
"projectType": "library", | ||
"tags": [], | ||
"targets": { | ||
"build": { | ||
"executor": "@nx/js:tsc", | ||
"outputs": ["{options.outputPath}"], | ||
"options": { | ||
"outputPath": "dist/libs/nx-plugin", | ||
"main": "libs/nx-plugin/src/index.ts", | ||
"tsConfig": "libs/nx-plugin/tsconfig.lib.json", | ||
"assets": [ | ||
"libs/nx-plugin/*.md", | ||
{ | ||
"input": "./libs/nx-plugin/src", | ||
"glob": "**/!(*.ts)", | ||
"output": "./src" | ||
}, | ||
{ | ||
"input": "./libs/nx-plugin/src", | ||
"glob": "**/*.d.ts", | ||
"output": "./src" | ||
}, | ||
{ | ||
"input": "./libs/nx-plugin", | ||
"glob": "generators.json", | ||
"output": "." | ||
}, | ||
{ | ||
"input": "./libs/nx-plugin", | ||
"glob": "executors.json", | ||
"output": "." | ||
} | ||
] | ||
} | ||
}, | ||
"lint": { | ||
"executor": "@nx/eslint:lint" | ||
}, | ||
"test": { | ||
"executor": "@nx/jest:jest", | ||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"], | ||
"options": { | ||
"jestConfig": "libs/nx-plugin/jest.config.ts" | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { Tree, readProjectConfiguration } from "@nx/devkit"; | ||
import { createTreeWithEmptyWorkspace } from "@nx/devkit/testing"; | ||
|
||
import { basicLibGenerator } from "./basic-lib"; | ||
import { BasicLibGeneratorSchema } from "./schema"; | ||
|
||
describe("basic-lib generator", () => { | ||
let tree: Tree; | ||
const options: BasicLibGeneratorSchema = { | ||
name: "test", | ||
description: "test", | ||
team: "platform", | ||
directory: "libs", | ||
}; | ||
|
||
beforeEach(() => { | ||
tree = createTreeWithEmptyWorkspace(); | ||
}); | ||
|
||
it("should update tsconfig.base.json paths", async () => { | ||
tree.write("tsconfig.base.json", JSON.stringify({ compilerOptions: { paths: {} } })); | ||
await basicLibGenerator(tree, options); | ||
const tsconfigContent = tree.read("tsconfig.base.json"); | ||
expect(tsconfigContent).not.toBeNull(); | ||
const tsconfig = JSON.parse(tsconfigContent?.toString() ?? ""); | ||
expect(tsconfig.compilerOptions.paths[`@bitwarden/${options.name}`]).toEqual([ | ||
`libs/test/src/index.ts`, | ||
]); | ||
}); | ||
|
||
it("should update CODEOWNERS file", async () => { | ||
tree.write(".github/CODEOWNERS", "# Existing content\n"); | ||
await basicLibGenerator(tree, options); | ||
const codeownersContent = tree.read(".github/CODEOWNERS"); | ||
expect(codeownersContent).not.toBeNull(); | ||
const codeowners = codeownersContent?.toString(); | ||
expect(codeowners).toContain(`libs/test @bitwarden/team-platform-dev`); | ||
}); | ||
|
||
it("should generate expected files", async () => { | ||
await basicLibGenerator(tree, options); | ||
|
||
const config = readProjectConfiguration(tree, "test"); | ||
expect(config).toBeDefined(); | ||
|
||
expect(tree.exists(`libs/test/README.md`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/eslint.config.mjs`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/jest.config.js`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/package.json`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/tsconfig.json`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/tsconfig.lib.json`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/tsconfig.spec.json`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/src/index.ts`)).toBeTruthy(); | ||
}); | ||
|
||
it("should handle missing CODEOWNERS file gracefully", async () => { | ||
const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); | ||
await basicLibGenerator(tree, options); | ||
expect(consoleSpy).toHaveBeenCalledWith("CODEOWNERS file not found at .github/CODEOWNERS"); | ||
consoleSpy.mockRestore(); | ||
}); | ||
|
||
it("should map team names to correct GitHub handles", async () => { | ||
tree.write(".github/CODEOWNERS", ""); | ||
await basicLibGenerator(tree, { ...options, team: "vault" }); | ||
const codeownersContent = tree.read(".github/CODEOWNERS"); | ||
expect(codeownersContent).not.toBeNull(); | ||
const codeowners = codeownersContent?.toString(); | ||
expect(codeowners).toContain(`libs/test @bitwarden/team-vault-dev`); | ||
}); | ||
|
||
it("should generate expected files", async () => { | ||
await basicLibGenerator(tree, options); | ||
expect(tree.exists(`libs/test/README.md`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/eslint.config.mjs`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/jest.config.js`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/package.json`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/project.json`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/tsconfig.json`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/tsconfig.lib.json`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/tsconfig.spec.json`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/src/index.ts`)).toBeTruthy(); | ||
expect(tree.exists(`libs/test/src/test.spec.ts`)).toBeTruthy(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import { execSync } from "child_process"; | ||
import * as path from "path"; | ||
|
||
import { | ||
formatFiles, | ||
generateFiles, | ||
Tree, | ||
offsetFromRoot, | ||
updateJson, | ||
runTasksInSerial, | ||
GeneratorCallback, | ||
} from "@nx/devkit"; | ||
|
||
import { BasicLibGeneratorSchema } from "./schema"; | ||
|
||
/** | ||
* An Nx generator for creating basic libraries. | ||
* Generators help automate repetitive tasks like creating new components, libraries, or apps. | ||
* | ||
* @param {Tree} tree - The virtual file system tree that Nx uses to make changes | ||
* @param {BasicLibGeneratorSchema} options - Configuration options for the generator | ||
* @returns {Promise<void>} - Returns a promise that resolves when generation is complete | ||
*/ | ||
export async function basicLibGenerator( | ||
tree: Tree, | ||
options: BasicLibGeneratorSchema, | ||
): Promise<GeneratorCallback> { | ||
const projectRoot = `${options.directory}/${options.name}`; | ||
const srcRoot = `${projectRoot}/src`; | ||
|
||
/** | ||
* Generate files from templates in the 'files/' directory. | ||
* This copies all template files to the new library location. | ||
*/ | ||
generateFiles(tree, path.join(__dirname, "files"), projectRoot, { | ||
...options, | ||
// `tmpl` is used in file names for template files. Setting it to an | ||
// empty string here lets use be explicit with the naming of template | ||
// files, and lets Nx handle stripping out "__tmpl__" from file names. | ||
tmpl: "", | ||
// `name` is a variable passed to template files for interpolation into | ||
// their contents. It is set to the name of the library being generated. | ||
name: options.name, | ||
root: projectRoot, | ||
// `offsetFromRoot` is helper to calculate relative path from the new | ||
// library to project root. | ||
offsetFromRoot: offsetFromRoot(projectRoot), | ||
}); | ||
|
||
// Add TypeScript path to the base tsconfig | ||
updateTsConfigPath(tree, options.name, srcRoot); | ||
|
||
// Update CODEOWNERS with the new lib | ||
updateCodeowners(tree, options.directory, options.name, options.team); | ||
|
||
// Format all new files with prettier | ||
await formatFiles(tree); | ||
|
||
const tasks: GeneratorCallback[] = []; | ||
// Run npm i after generation. Nx ships a helper function for this called | ||
// installPackagesTask. When used here it was leaving package-lock in a | ||
// broken state, so a manual approach was used instead. | ||
tasks.push(() => { | ||
execSync("npm install", { stdio: "inherit" }); | ||
return Promise.resolve(); | ||
}); | ||
return runTasksInSerial(...tasks); | ||
} | ||
|
||
/** | ||
* Updates the base tsconfig.json file to include the new library. | ||
* This allows importing the library using its alias path. | ||
* | ||
* @param {Tree} tree - The virtual file system tree | ||
* @param {string} name - The library name | ||
* @param {string} srcRoot - Path to the library's source files | ||
*/ | ||
function updateTsConfigPath(tree: Tree, name: string, srcRoot: string) { | ||
updateJson(tree, "tsconfig.base.json", (json) => { | ||
const paths = json.compilerOptions.paths || {}; | ||
|
||
paths[`@bitwarden/${name}`] = [`${srcRoot}/index.ts`]; | ||
|
||
json.compilerOptions.paths = paths; | ||
return json; | ||
}); | ||
} | ||
|
||
/** | ||
* Updates the CODEOWNERS file to add ownership for the new library | ||
* | ||
* @param {Tree} tree - The virtual file system tree | ||
* @param {string} directory - Directory where the library is created | ||
* @param {string} name - The library name | ||
* @param {string} team - The team responsible for the library | ||
*/ | ||
function updateCodeowners(tree: Tree, directory: string, name: string, team: string) { | ||
const codeownersPath = ".github/CODEOWNERS"; | ||
|
||
if (!tree.exists(codeownersPath)) { | ||
console.warn("CODEOWNERS file not found at .github/CODEOWNERS"); | ||
return; | ||
} | ||
|
||
const teamHandleMap: Record<string, string> = { | ||
"admin-console": "@bitwarden/team-admin-console-dev", | ||
auth: "@bitwarden/team-auth-dev", | ||
autofill: "@bitwarden/team-autofill-dev", | ||
billing: "@bitwarden/team-billing-dev", | ||
"data-insights-and-reporting": "@bitwarden/team-data-insights-and-reporting-dev", | ||
"key-management": "@bitwarden/team-key-management-dev", | ||
platform: "@bitwarden/team-platform-dev", | ||
tools: "@bitwarden/team-tools-dev", | ||
"ui-foundation": "@bitwarden/team-ui-foundation", | ||
vault: "@bitwarden/team-vault-dev", | ||
}; | ||
|
||
const teamHandle = teamHandleMap[team] || `@bitwarden/team-${team}-dev`; | ||
const libPath = `${directory}/${name}`; | ||
|
||
const newLine = `${libPath} ${teamHandle}\n`; | ||
|
||
const content = tree.read(codeownersPath)?.toString() || ""; | ||
tree.write(codeownersPath, content + newLine); | ||
} | ||
|
||
export default basicLibGenerator; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# <%= name %> | ||
Owned by: <%= team %> | ||
|
||
<%= description %> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import baseConfig from "<%= offsetFromRoot %>eslint.config.mjs"; | ||
|
||
export default [...baseConfig]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ts-node is used by nx/js