@joshmossas/nx-cargo 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.babelrc +3 -0
  2. package/.eslintrc.json +34 -0
  3. package/README.md +39 -0
  4. package/build.config.ts +10 -0
  5. package/dist/index.cjs +100 -0
  6. package/dist/index.d.cts +5 -0
  7. package/dist/index.d.mts +5 -0
  8. package/dist/index.d.ts +5 -0
  9. package/dist/index.mjs +79 -0
  10. package/executors.json +25 -0
  11. package/generators.json +25 -0
  12. package/jest.config.ts +17 -0
  13. package/package.json +30 -0
  14. package/project.json +53 -0
  15. package/src/common/index.spec.ts +183 -0
  16. package/src/common/index.ts +358 -0
  17. package/src/common/schema.d.ts +111 -0
  18. package/src/executors/build/executor.ts +21 -0
  19. package/src/executors/build/schema.d.ts +39 -0
  20. package/src/executors/build/schema.json +77 -0
  21. package/src/executors/clippy/executor.ts +18 -0
  22. package/src/executors/clippy/schema.d.ts +34 -0
  23. package/src/executors/clippy/schema.json +29 -0
  24. package/src/executors/run/executor.ts +19 -0
  25. package/src/executors/run/schema.d.ts +32 -0
  26. package/src/executors/run/schema.json +77 -0
  27. package/src/executors/test/executor.ts +19 -0
  28. package/src/executors/test/schema.d.ts +22 -0
  29. package/src/executors/test/schema.json +73 -0
  30. package/src/generators/binary/files/Cargo.toml__template__ +8 -0
  31. package/src/generators/binary/files/src/main.rs__template__ +3 -0
  32. package/src/generators/binary/generator.spec.ts +75 -0
  33. package/src/generators/binary/generator.ts +76 -0
  34. package/src/generators/binary/schema.d.ts +6 -0
  35. package/src/generators/binary/schema.json +35 -0
  36. package/src/generators/init/files/Cargo.toml +2 -0
  37. package/src/generators/init/files/rust-toolchain.toml__template__ +2 -0
  38. package/src/generators/init/files/rustfmt.toml +0 -0
  39. package/src/generators/init/generator.spec.ts +49 -0
  40. package/src/generators/init/generator.ts +55 -0
  41. package/src/generators/init/schema.d.ts +7 -0
  42. package/src/generators/init/schema.json +14 -0
  43. package/src/generators/library/files/Cargo.toml__template__ +8 -0
  44. package/src/generators/library/files/src/lib.rs__template__ +13 -0
  45. package/src/generators/library/generator.spec.ts +96 -0
  46. package/src/generators/library/generator.ts +78 -0
  47. package/src/generators/library/schema.d.ts +6 -0
  48. package/src/generators/library/schema.json +35 -0
  49. package/src/graph/index.ts +189 -0
  50. package/src/index.ts +1 -0
  51. package/tsconfig.json +13 -0
  52. package/tsconfig.lib.json +12 -0
  53. package/tsconfig.spec.json +20 -0
@@ -0,0 +1,96 @@
1
+ import { Tree } from "@nx/devkit";
2
+ import { createTreeWithEmptyWorkspace } from "@nx/devkit/testing";
3
+ import runGenerator from "./generator";
4
+
5
+ describe("library generator", () => {
6
+ let appTree: Tree;
7
+
8
+ beforeAll(() => {
9
+ appTree = createTreeWithEmptyWorkspace({ layout: "apps-libs" });
10
+ });
11
+
12
+ describe("with kebab-case project name", () => {
13
+ beforeAll(async () => {
14
+ await runGenerator(appTree, { name: "my-library" });
15
+ });
16
+
17
+ it("should create the correct file structure", () => {
18
+ let changes = appTree.listChanges();
19
+ let cargoToml = changes.find(c => c.path === "libs/my-library/Cargo.toml");
20
+ let libRs = changes.find(c => c.path === "libs/my-library/src/lib.rs");
21
+
22
+ expect(cargoToml).toBeTruthy();
23
+ expect(libRs).toBeTruthy();
24
+ });
25
+
26
+ it("should populate project files with the correct content", () => {
27
+ let changes = appTree.listChanges();
28
+ let cargoContent = changes
29
+ .find(c => c.path === "libs/my-library/Cargo.toml")!
30
+ .content!.toString();
31
+
32
+ expect(cargoContent).toContain(`name = "my-library"`);
33
+ expect(cargoContent).toContain(`edition = "2021"`);
34
+
35
+ let libRsContent = changes
36
+ .find(c => c.path === "libs/my-library/src/lib.rs")!
37
+ .content!.toString();
38
+
39
+ expect(libRsContent).toContain(`pub fn my_library() -> String {`);
40
+ expect(libRsContent).toContain(`"my-library".into()`);
41
+ expect(libRsContent).toContain(
42
+ `assert_eq!(my_library(), "my-library".to_string())`
43
+ );
44
+ });
45
+
46
+ it("should add project to workspace members", () => {
47
+ let changes = appTree.listChanges();
48
+ let members = changes.find(c => c.path === "Cargo.toml")!.content!.toString();
49
+
50
+ expect(members).toContain(`"libs/my-library"`);
51
+ });
52
+ });
53
+
54
+ describe("with snake_case project name", () => {
55
+ beforeAll(async () => {
56
+ appTree = createTreeWithEmptyWorkspace({ layout: "apps-libs" });
57
+ await runGenerator(appTree, { name: "my_library" });
58
+ });
59
+
60
+ it("should create the correct file structure", () => {
61
+ let changes = appTree.listChanges();
62
+ let cargoToml = changes.find(c => c.path === "libs/my_library/Cargo.toml");
63
+ let libRs = changes.find(c => c.path === "libs/my_library/src/lib.rs");
64
+
65
+ expect(cargoToml).toBeTruthy();
66
+ expect(libRs).toBeTruthy();
67
+ });
68
+
69
+ it("should populate project files with the correct content", () => {
70
+ let changes = appTree.listChanges();
71
+ let cargoContent = changes
72
+ .find(c => c.path === "libs/my_library/Cargo.toml")!
73
+ .content!.toString();
74
+
75
+ expect(cargoContent).toContain(`name = "my_library"`);
76
+ expect(cargoContent).toContain(`edition = "2021"`);
77
+
78
+ let libRsContent = changes
79
+ .find(c => c.path === "libs/my_library/src/lib.rs")!
80
+ .content!.toString();
81
+
82
+ expect(libRsContent).toContain(`pub fn my_library() -> String {`);
83
+ expect(libRsContent).toContain(`"my_library".into()`);
84
+ expect(libRsContent).toContain(
85
+ `assert_eq!(my_library(), "my_library".to_string())`
86
+ );
87
+ });
88
+
89
+ it("should add project to workspace members", () => {
90
+ let changes = appTree.listChanges();
91
+ let members = changes.find(c => c.path === "Cargo.toml")!.content!.toString();
92
+
93
+ expect(members).toContain(`"libs/my_library"`);
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,78 @@
1
+ import {
2
+ Tree,
3
+ addProjectConfiguration,
4
+ formatFiles,
5
+ generateFiles,
6
+ } from "@nx/devkit";
7
+ import * as path from "path";
8
+
9
+ import {
10
+ GeneratorOptions,
11
+ normalizeGeneratorOptions,
12
+ updateWorkspaceMembers,
13
+ } from "../../common";
14
+ import cargoInit from "../init/generator";
15
+ import CLIOptions from "./schema";
16
+
17
+ // prettier-ignore
18
+ type Options = CLIOptions & GeneratorOptions;
19
+
20
+ export default async function (host: Tree, opts: CLIOptions) {
21
+ let options = normalizeGeneratorOptions("library", host, opts);
22
+
23
+ addProjectConfiguration(host, options.projectName, {
24
+ root: options.projectRoot,
25
+ projectType: "library",
26
+ sourceRoot: `${options.projectRoot}/src`,
27
+ targets: {
28
+ build: {
29
+ executor: "@nxrs/cargo:build",
30
+ options: {
31
+ profile: "dev",
32
+ },
33
+ configurations: {
34
+ production: {
35
+ profile: "release",
36
+ },
37
+ },
38
+ },
39
+ test: {
40
+ executor: "@nxrs/cargo:test",
41
+ options: {},
42
+ },
43
+ lint: {
44
+ executor: "@nxrs/cargo:clippy",
45
+ options: {
46
+ fix: false,
47
+ failOnWarnings: true,
48
+ noDeps: true,
49
+ },
50
+ },
51
+ },
52
+ tags: options.parsedTags,
53
+ });
54
+
55
+ await addFiles(host, options);
56
+ updateWorkspaceMembers(host, options);
57
+ await formatFiles(host);
58
+ }
59
+
60
+ async function addFiles(host: Tree, opts: Options) {
61
+ if (!host.exists("Cargo.toml")) {
62
+ await cargoInit(host, {});
63
+ }
64
+
65
+ let substitutions = {
66
+ projectName: opts.projectName,
67
+ moduleName: opts.moduleName,
68
+ edition: opts.edition,
69
+ template: "",
70
+ };
71
+
72
+ generateFiles(
73
+ host,
74
+ path.join(__dirname, "files"),
75
+ opts.projectRoot,
76
+ substitutions
77
+ );
78
+ }
@@ -0,0 +1,6 @@
1
+ export default interface Options {
2
+ name: string;
3
+ edition?: number;
4
+ directory?: string;
5
+ tags?: string;
6
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "id": "Library",
4
+ "title": "",
5
+ "type": "object",
6
+ "properties": {
7
+ "name": {
8
+ "type": "string",
9
+ "description": "",
10
+ "$default": {
11
+ "$source": "argv",
12
+ "index": 0
13
+ },
14
+ "x-prompt": "What name would you like to use?"
15
+ },
16
+ "edition": {
17
+ "type": "number",
18
+ "description": "What Rust edition to use",
19
+ "default": 2021
20
+ },
21
+ "tags": {
22
+ "type": "string",
23
+ "description": "Add tags to the project (used for linting)",
24
+ "alias": "t"
25
+ },
26
+ "directory": {
27
+ "type": "string",
28
+ "description": "A directory where the project is placed",
29
+ "alias": "d"
30
+ }
31
+ },
32
+ "required": [
33
+ "name"
34
+ ]
35
+ }
@@ -0,0 +1,189 @@
1
+ import {
2
+ ProjectConfiguration,
3
+ CreateDependenciesContext as Context,
4
+ RawProjectGraphDependency as GraphDependency,
5
+ DependencyType,
6
+ } from "@nx/devkit";
7
+ import fs from "node:fs";
8
+ import * as cp from "node:child_process";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
11
+
12
+ type VersionNumber = `${number}.${number}.${number}`;
13
+ type PackageVersion = `${string}@${VersionNumber}` | VersionNumber;
14
+ type CargoId = `${"registry" | "path"}+${
15
+ | "http"
16
+ | "https"
17
+ | "file"}://${string}#${PackageVersion}`;
18
+
19
+ interface CargoPackage {
20
+ name: string;
21
+ version: string;
22
+ id: CargoId;
23
+ license: string;
24
+ license_file: string | null;
25
+ description: string;
26
+ source: string | null;
27
+ dependencies: CargoDependency[];
28
+ targets: unknown; // TODO
29
+ features: Record<string, string[]>;
30
+ manifest_path: string;
31
+ metadata: unknown | null; // TODO
32
+ publish: unknown | null; // TODO
33
+ authors: string[];
34
+ categories: string[];
35
+ keywords: string[];
36
+ readme: string | null;
37
+ repository: string | null;
38
+ homepage: string | null;
39
+ documentation: string | null;
40
+ edition: string;
41
+ links: unknown | null; // TODO
42
+ default_run: unknown | null; // TODO
43
+ rust_version: string;
44
+ }
45
+
46
+ interface CargoDependency {
47
+ name: string;
48
+ source: string | null;
49
+ req: string;
50
+ kind: "build" | "dev" | null;
51
+ rename: string | null;
52
+ optional: boolean;
53
+ uses_default_features: boolean;
54
+ features: string[];
55
+ target: string | null;
56
+ registry: string | null;
57
+ path?: string;
58
+ }
59
+
60
+ interface CargoMetadata {
61
+ packages: CargoPackage[];
62
+ workspace_members: CargoId[];
63
+ workspace_default_members: CargoId[];
64
+ resolve: {
65
+ nodes: ResolveNode[];
66
+ root: unknown;
67
+ };
68
+ target_directory: string;
69
+ version: number;
70
+ workspace_root: string;
71
+ metadata: unknown | null;
72
+ }
73
+
74
+ interface ResolveNode {
75
+ id: CargoId;
76
+ dependencies: CargoId[];
77
+ }
78
+ export function createDependencies(_: unknown, ctx: Context): GraphDependency[] {
79
+ const allDependencies: GraphDependency[] = [];
80
+ const processedWorkspaceRoots = new Set<string>();
81
+
82
+ // 1. Identify all potential Cargo workspaces/projects in the Nx graph
83
+ const cargoConfigPaths = Object.values(ctx.projects)
84
+ .map(p => path.join(ctx.workspaceRoot, p.root, "Cargo.toml"))
85
+ .filter(p => fs.existsSync(p));
86
+
87
+ for (const configPath of cargoConfigPaths) {
88
+ const configDir = path.dirname(configPath);
89
+
90
+ // 2. Get metadata for this specific workspace
91
+ const metadata = getCargoMetadata(configDir);
92
+
93
+ // 3. Skip if we've already processed this workspace (via another member)
94
+ if (processedWorkspaceRoots.has(metadata.workspace_root)) {
95
+ continue;
96
+ }
97
+ processedWorkspaceRoots.add(metadata.workspace_root);
98
+
99
+ // 4. Process this workspace's internal dependencies
100
+ const workspaceDeps = processWorkspaceMetadata(ctx, metadata);
101
+ allDependencies.push(...workspaceDeps);
102
+ }
103
+
104
+ return allDependencies;
105
+ }
106
+
107
+ function processWorkspaceMetadata(
108
+ ctx: Context,
109
+ metadata: CargoMetadata
110
+ ): GraphDependency[] {
111
+ const {
112
+ packages,
113
+ workspace_members: cargoWsMembers,
114
+ resolve: cargoResolve,
115
+ } = metadata;
116
+
117
+ const workspacePackages = new Map<CargoId, CargoPackage>();
118
+ for (const id of cargoWsMembers) {
119
+ const pkg = packages.find(p => p.id === id);
120
+ if (pkg) workspacePackages.set(id, pkg);
121
+ }
122
+
123
+ const nxData = mapCargoProjects(ctx, workspacePackages);
124
+
125
+ return cargoResolve.nodes
126
+ .filter(({ id }) => nxData.has(id))
127
+ .flatMap(({ id: sourceId, dependencies }) => {
128
+ const sourceProject = nxData.get(sourceId)!;
129
+ const cargoPackage = workspacePackages.get(sourceId)!;
130
+ const sourceManifest = path
131
+ .relative(ctx.workspaceRoot, cargoPackage.manifest_path)
132
+ .replace(/\\/g, "/");
133
+
134
+ return dependencies
135
+ .filter(depId => nxData.has(depId))
136
+ .map(depId => ({
137
+ source: sourceProject.name,
138
+ target: nxData.get(depId)!.name,
139
+ type: DependencyType.static,
140
+ sourceFile: sourceManifest,
141
+ }));
142
+ });
143
+ }
144
+
145
+ function getCargoMetadata(cwd: string): CargoMetadata {
146
+ const availableMemory = os.freemem();
147
+ // Run cargo metadata from the specific directory of the Cargo.toml
148
+ const metadata = cp.execSync("cargo metadata --format-version=1", {
149
+ encoding: "utf8",
150
+ maxBuffer: availableMemory,
151
+ cwd: cwd, // Crucial: run in the workspace directory
152
+ });
153
+
154
+ return JSON.parse(metadata);
155
+ }
156
+
157
+ type WithReq<T, K extends keyof T> = Omit<T, K> & {
158
+ [Key in K]-?: Exclude<T[Key], null | undefined>;
159
+ };
160
+
161
+ function mapCargoProjects(ctx: Context, packages: Map<CargoId, CargoPackage>) {
162
+ let result = new Map<CargoId, WithReq<ProjectConfiguration, "name">>();
163
+
164
+ for (let [cargoId, cargoPackage] of packages) {
165
+ if (!cargoPackage.manifest_path) {
166
+ throw new Error("Expected cargo package's `manifest_path` to exist");
167
+ }
168
+
169
+ let manifestDir = path.dirname(cargoPackage.manifest_path);
170
+ let projectDir = path
171
+ .relative(ctx.workspaceRoot, manifestDir)
172
+ .replace(/\\/g, "/");
173
+
174
+ let found = Object.entries(ctx.projects).find(
175
+ ([, config]) => config.root === projectDir
176
+ );
177
+
178
+ if (found) {
179
+ let [projectName, projectConfig] = found;
180
+
181
+ result.set(cargoId, {
182
+ ...projectConfig,
183
+ name: projectName,
184
+ });
185
+ }
186
+ }
187
+
188
+ return result;
189
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./graph";
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "files": [],
4
+ "include": [],
5
+ "references": [
6
+ {
7
+ "path": "./tsconfig.lib.json"
8
+ },
9
+ {
10
+ "path": "./tsconfig.spec.json"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "es2022",
5
+ "outDir": "../../dist/out-tsc",
6
+ "declaration": true,
7
+ "types": ["node"],
8
+ "noPropertyAccessFromIndexSignature": true
9
+ },
10
+ "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"],
11
+ "include": ["**/*.ts"]
12
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "module": "es2022",
6
+ "types": ["jest", "node"]
7
+ },
8
+ "include": [
9
+ "**/*.spec.ts",
10
+ "**/*.test.ts",
11
+ "**/*.spec.tsx",
12
+ "**/*.test.tsx",
13
+ "**/*.spec.js",
14
+ "**/*.test.js",
15
+ "**/*.spec.jsx",
16
+ "**/*.test.jsx",
17
+ "**/*.d.ts",
18
+ "jest.config.ts"
19
+ ]
20
+ }