Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,13 @@
"npm-run-path": "4.0.1",
"nx": "22.3.3",
"nx-cloud": "19.1.0",
"p-limit": "^6.2.0",
"prettier": "^2.6.2",
"semver": "^7.6.0",
"strip-indent": "^3.0.0",
"tcp-port-used": "^1.0.2",
"tree-kill": "^1.2.2",
"tree-sitter-go": "^0.25.0",
"ts-jest": "29.4.0",
"ts-node": "10.9.1",
"tsx": "^4.19.4",
Expand All @@ -84,6 +87,7 @@
"verdaccio": "6.0.5",
"vite": "7.3.1",
"vitest": "4.0.9",
"web-tree-sitter": "^0.26.3",
"webpack": "^5.90.1",
"wrangler": "^4.26.0"
},
Expand Down
194 changes: 194 additions & 0 deletions packages/gonx-e2e/src/static-analysis.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {
uniq,
tmpProjPath,
runNxCommand,
ensureNxProject,
cleanup,
readJson,
} from '@nx/plugin/testing';
import { join } from 'path';
import { writeFileSync, readFileSync } from 'fs';

describe('Dependency Detection', () => {
beforeEach(() => {
ensureNxProject('@naxodev/gonx', 'dist/packages/gonx');

// Initialize Go support
runNxCommand('generate @naxodev/gonx:init');
});

afterEach(() => cleanup());

it('should detect dependencies between Go projects', async () => {
const goapp = uniq('goapp');
const golib = uniq('golib');

// Generate a library first
runNxCommand(`generate @naxodev/gonx:library ${golib}`, {
env: { NX_ADD_PLUGINS: 'true' },
});

// Generate an application
runNxCommand(`generate @naxodev/gonx:application ${goapp}`, {
env: { NX_ADD_PLUGINS: 'true' },
});

// Get the library's module path from go.mod
const libGoModPath = join(tmpProjPath(), golib, 'go.mod');
const libGoModContent = readFileSync(libGoModPath, 'utf-8');
const moduleMatch = libGoModContent.match(/module\s+(\S+)/);
const libModulePath = moduleMatch ? moduleMatch[1] : `example.com/${golib}`;

// Update the application to import the library
const mainGoPath = join(tmpProjPath(), goapp, 'main.go');
writeFileSync(
mainGoPath,
`package main

import (
"fmt"
"${libModulePath}"
)

func main() {
fmt.Println("Hello from ${goapp}")
fmt.Println(${golib}.Hello())
}
`
);

// Add a replace directive to the app's go.mod to point to the library
const appGoModPath = join(tmpProjPath(), goapp, 'go.mod');
const appGoModContent = readFileSync(appGoModPath, 'utf-8');
writeFileSync(
appGoModPath,
`${appGoModContent}
replace ${libModulePath} => ../${golib}
`
);

// Reset Nx to pick up the changes
runNxCommand('reset');

// Run nx graph to generate the project graph JSON
runNxCommand('graph --file=graph.json');

// Read the generated graph file
const graphJson = readJson('graph.json');

// Verify that the dependency was detected
const appDeps = graphJson.graph?.dependencies?.[goapp] || [];
const hasDependencyOnLib = appDeps.some(
(dep: { target: string }) => dep.target === golib
);

expect(hasDependencyOnLib).toBe(true);
}, 120_000);

it('should detect dependencies in multi-project workspace', async () => {
const goapp = uniq('goapp');
const golib1 = uniq('golib1');
const golib2 = uniq('golib2');

// Generate two libraries
runNxCommand(`generate @naxodev/gonx:library ${golib1}`, {
env: { NX_ADD_PLUGINS: 'true' },
});
runNxCommand(`generate @naxodev/gonx:library ${golib2}`, {
env: { NX_ADD_PLUGINS: 'true' },
});

// Generate an application
runNxCommand(`generate @naxodev/gonx:application ${goapp}`, {
env: { NX_ADD_PLUGINS: 'true' },
});

// Get module paths
const lib1GoModContent = readFileSync(
join(tmpProjPath(), golib1, 'go.mod'),
'utf-8'
);
const lib1ModulePath =
lib1GoModContent.match(/module\s+(\S+)/)?.[1] || `example.com/${golib1}`;

const lib2GoModContent = readFileSync(
join(tmpProjPath(), golib2, 'go.mod'),
'utf-8'
);
const lib2ModulePath =
lib2GoModContent.match(/module\s+(\S+)/)?.[1] || `example.com/${golib2}`;

// Update lib1 to import lib2
const lib1MainPath = join(tmpProjPath(), golib1, `${golib1}.go`);
writeFileSync(
lib1MainPath,
`package ${golib1}

import (
"${lib2ModulePath}"
)

func Hello() string {
return "Hello from ${golib1}: " + ${golib2}.Hello()
}
`
);

// Add replace directive to lib1's go.mod
const lib1GoModPath = join(tmpProjPath(), golib1, 'go.mod');
writeFileSync(
lib1GoModPath,
`${lib1GoModContent}
replace ${lib2ModulePath} => ../${golib2}
`
);

// Update app to import lib1
const mainGoPath = join(tmpProjPath(), goapp, 'main.go');
writeFileSync(
mainGoPath,
`package main

import (
"fmt"
"${lib1ModulePath}"
)

func main() {
fmt.Println(${golib1}.Hello())
}
`
);

// Add replace directive to app's go.mod
const appGoModPath = join(tmpProjPath(), goapp, 'go.mod');
const appGoModContent = readFileSync(appGoModPath, 'utf-8');
writeFileSync(
appGoModPath,
`${appGoModContent}
replace ${lib1ModulePath} => ../${golib1}
`
);

// Reset Nx
runNxCommand('reset');

// Generate and check the graph
runNxCommand('graph --file=graph.json');
const graphJson = readJson('graph.json');

// Check app -> lib1 dependency
const appDeps = graphJson.graph?.dependencies?.[goapp] || [];
const appDependsOnLib1 = appDeps.some(
(dep: { target: string }) => dep.target === golib1
);
expect(appDependsOnLib1).toBe(true);

// Check lib1 -> lib2 dependency
const lib1Deps = graphJson.graph?.dependencies?.[golib1] || [];
const lib1DependsOnLib2 = lib1Deps.some(
(dep: { target: string }) => dep.target === golib2
);
expect(lib1DependsOnLib2).toBe(true);
}, 180_000);
});
72 changes: 51 additions & 21 deletions packages/gonx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,26 @@

<hr>

## ✨ Features

- ✅ Generate Go Applications
- ✅ Customizable Go module setup
- ✅ Well-structured Go code scaffolding
- ✅ Generate Go Libraries
- ✅ Full Nx integration
- ✅ Inferred Tasks: Build, Generate, Tidy, Test, Run, and Lint
- ✅ Cacheable Tasks: Build, Generate, Tidy, Test, and Lint
- ✅ GraphV2 Support
- ✅ Version Actions for Go release
- ✅ Nx Release Publish executor to release to list the module on the registry
- ✅ Use official Go commands in the background
- ✅ Efficient caching and dependency graph tools for Go projects

## 🚀 Getting started

You need to have a [stable version of Go](https://go.dev/dl/) installed on your machine. And... you are ready!
## Features

- Generate Go Applications
- Customizable Go module setup
- Well-structured Go code scaffolding
- Generate Go Libraries
- Full Nx integration
- Inferred Tasks: Build, Generate, Tidy, Test, Run, and Lint
- Cacheable Tasks: Build, Generate, Tidy, Test, and Lint
- GraphV2 Support
- Version Actions for Go release
- Nx Release Publish executor
- Use official Go commands in the background
- Dependency detection via tree-sitter static analysis (no Go
required)

## Getting started

You need to have a [stable version of Go](https://go.dev/dl/)
installed on your machine. And... you are ready!

### Generate a Nx workspace with Go support

Expand All @@ -48,13 +50,37 @@ npx create-nx-workspace go-workspace --preset=@naxodev/gonx
nx add @naxodev/gonx
```

## Plugin Options

Configure the plugin in your `nx.json`:

```json
{
"plugins": [
{
"plugin": "@naxodev/gonx",
"options": {}
}
]
}
```

| Option | Type | Default | Description |
| ----------------------- | ------- | ------- | ------------------------------------- |
| `skipGoDependencyCheck` | boolean | `false` | Disable dependency detection entirely |

See [Dependency Detection](./docs/static-analysis.md) for details on
how dependencies between Go projects are resolved.

## Docs

To read the full documentation, check out the [docs](https://gonx.naxo.dev/) site.
To read the full documentation, check out the
[docs](https://gonx.naxo.dev/) site.

## Contributors

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
Thanks goes to these wonderful people
([emoji key](https://allcontributors.org/docs/en/emoji-key)):

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
Expand Down Expand Up @@ -105,4 +131,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d

## Acknowledgements

This project is a fork of [nx-go](https://github.com/nx-go/nx-go), a plugin for Nx that provides tools for building Go applications. Most credit goes to the original maintainers of nx-go - we've built upon their excellent foundation to modernize the plugin for the latest Nx features.
This project is a fork of [nx-go](https://github.com/nx-go/nx-go), a
plugin for Nx that provides tools for building Go applications. Most
credit goes to the original maintainers of nx-go - we've built upon
their excellent foundation to modernize the plugin for the latest Nx
features.
71 changes: 71 additions & 0 deletions packages/gonx/docs/static-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Dependency Detection

## Overview

gonx detects dependencies between Go projects using tree-sitter to
parse Go source files directly. This approach does not require Go to
be installed.

## How It Works

1. **Module Discovery**: Scans all Nx projects for `go.mod` files and
extracts module paths and replace directives.

2. **Import Extraction**: Parses `.go` files using tree-sitter to
extract import statements. Excludes `vendor/` and `testdata/`
directories.

3. **Dependency Resolution**: Uses longest-prefix matching to resolve
imports to Nx projects. For example, importing
`github.com/myorg/shared/utils` resolves to the project containing
`module github.com/myorg/shared`.

4. **Replace Directives**: Scoped per-project. A replace directive in
one project's `go.mod` only affects that project's imports.

## Configuration

In your `nx.json`:

```json
{
"plugins": [
{
"plugin": "@naxodev/gonx",
"options": {}
}
]
}
```

### Options

| Option | Type | Default | Description |
| ----------------------- | ------- | ------- | ------------------------------------- |
| `skipGoDependencyCheck` | boolean | `false` | Disable dependency detection entirely |

## Limitations

- **No build tag support**: All `.go` files are parsed regardless of
`//go:build` constraints. This may include platform-specific
dependencies that wouldn't be compiled in practice.

- **No cgo support**: The `import "C"` pseudo-import is filtered out.

- **No dynamic imports**: Only static `import` statements are
detected.

## Troubleshooting

### Dependencies not detected

1. Verify both projects have `go.mod` files
2. Check the module paths match the import statements
3. Ensure the importing project has a replace directive pointing to
the local project (required when not using `go.work`)

### Debugging

```bash
NX_VERBOSE_LOGGING=true nx graph
```
5 changes: 4 additions & 1 deletion packages/gonx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
"tslib": "^2.3.0",
"npm-run-path": "4.0.1",
"chalk": "^4.1.2",
"@melkeydev/go-blueprint": "0.10.11"
"@melkeydev/go-blueprint": "0.10.11",
"p-limit": "^6.2.0",
"web-tree-sitter": "^0.26.3",
"tree-sitter-go": "^0.25.0"
}
}
Loading
Loading