Loading workspace insights... Statistics interval
7 days30 daysLatest CI Pipeline Executions
348220e7 feat(gonx): harden build-constraint context and tree-sitter WASM loading (#108, #113, #104) (#160)
## TL;DR
Three follow-ups from PR #106's review hardened the Go build-constraint
analyzer: policy (`cgo`, Go version) was hardcoded inside `matchTag` and
invisible from the type; the shared default `BuildContext` was mutable
and mapped cygwin to a non-existent GOOS; and the tree-sitter WASM
bytes-loading workaround had zero test coverage. This PR lifts that
policy onto `BuildContext`, freezes the default context (mapping cygwin
→ linux with a warning), and adds a contract test pinning
`Language.load(Uint8Array)` against an exact-pinned `web-tree-sitter`.
**Files to review (8, +266 / -32):**
| File | Why |
|---|---|
| `packages/gonx/src/graph/static-analysis/build-constraints.ts` *(start
here)* | `BuildContext` gains `cgoEnabled`/`goVersion`; `matchTag`
becomes pure; `getDefaultBuildContext` is frozen and cygwin-aware. |
| `packages/gonx/src/graph/static-analysis/build-constraints.spec.ts` |
New cgo/Go-version/freeze/cygwin tests; the old cygwin pass-through test
is rewritten to the new behavior. |
| `packages/gonx/src/graph/static-analysis/parser-init.spec.ts` *(new)*
| Contract test for the real WASM bytes path under Jest — the existing
suite mocks `web-tree-sitter`. |
| `packages/gonx/src/graph/static-analysis/parser-init.ts` | Documents
the `Language.load(bytes)` coupling and the pin/contract-test guarding
it. |
| `package.json`, `packages/gonx/package.json` | Pin `web-tree-sitter`
to exact `0.26.3` (repo resolution + published manifest). |
| `packages/gonx/docs/static-analysis.md` | Corrects a doc anchor and
documents the new context fields. |
## Why
These three issues were surfaced by PR #106's review and tracked as
follow-ups (#108, #113, #104). Five sibling issues from the same review
(#107, #109, #110, #111, #112) were already satisfied by code that
landed inside #106 and are now closed; this PR covers the remainder.
The concrete problems: a caller could not model "cgo is enabled" or
"this code compiles under Go 1.18" — `matchTag` answered `cgo → false`
and `go1.X → true` unconditionally. The default context was a plain
mutable object shared process-wide, and `cygwin` passed through as a
GOOS Go doesn't have, so `linux`/`unix`-gated files were wrongly
excluded on cygwin hosts. And the WASM workaround — reading bytes
ourselves to dodge Jest's dynamic-import failure — was exercised only
through mocks, so an upstream change to the bytes overload would have
broken graph construction silently.
## How
- **Policy onto the context (#108).** `BuildContext` gains optional
`cgoEnabled?: boolean` and a `goVersion?` field typed `1.${number}`.
`matchTag` now reads `cgo → ctx.cgoEnabled ?? false` and evaluates
`go1.M` against `ctx.goVersion` (satisfied when unset). Defaults
reproduce today's behavior exactly.
- **Harden the default (#113).** `getDefaultBuildContext` returns an
`Object.freeze`'d context with a frozen tags set; `nodePlatformToGoos`
special-cases `cygwin → linux` and warns once.
- **Contract-test the WASM path (#104).** A new `parser-init.spec.ts`
loads the real grammar from bytes and parses Go, with `web-tree-sitter`
pinned to exact `0.26.3` so the overload can't drift unseen.
## Reviewer notes
- **Default behavior is unchanged.** With `cgoEnabled`/`goVersion` unset
and no cygwin host, every existing result is identical — the 44 prior
`build-constraints` tests pass untouched. The new fields are purely
additive opt-ins.
- **Go versions compare numerically, not lexically.** `go1.10` under
`goVersion: '1.9'` must be *unsatisfied* (9 < 10); a string compare
would put `'1.9' > '1.10'` and wrongly include. A test pins this.
- **`Object.freeze` on a Set is partial by design.** It makes
`Object.isFrozen` true and blocks property addition, but the real
immutability guarantee for `tags` is the compile-time
`ReadonlySet<string>`; freeze is the runtime backstop the issue asked
for.
- **The cygwin warning fires once.** `extract-imports.ts` memoizes the
default context at module load, so the `logger.warn` is emitted a single
time per process, not per file.
- **The contract test deliberately un-mocks `web-tree-sitter`.** That is
the point — it runs the exact bytes path production uses under Jest, so
a breaking upgrade fails loudly here instead of silently dropping edges.
- **A malformed `goVersion` over-includes, never excludes.** The ``
`1.${number}` `` type can be subverted via `as`/external config;
`satisfiesGoVersion` validates `/^1\.\d+$/` and falls back to
satisfy-all rather than letting `NaN >= M` silently drop edges —
honoring the over-include policy. Validation of a user-facing
`goVersion` belongs at that future input boundary.
## Tests
`nx test gonx` → 278 passed (27 suites). New: cgo defaults (incl.
`!cgo`), Go-version policy (numeric boundary, legacy `// +build` path,
`&&` composition, unparseable over-include), cygwin/freeze, and the WASM
contract cases. `tsc --noEmit` clean; `eslint` 0 errors.
## Links
- [#108 — lift cgo/Go-version policy onto
BuildContext](https://github.com/naxodev/oss/issues/108)
- [#113 — freeze default context + cygwin
handling](https://github.com/naxodev/oss/issues/113)
- [#104 — harden WASM loading for tree-sitter under
Jest](https://github.com/naxodev/oss/issues/104)
- Parent review: [#106](https://github.com/naxodev/oss/pull/106) 2b61e80b refactor(nx-cloudflare): drop the createNodesV2 targets-cache (#164) (#166)
## TL;DR
The `createNodesV2` inference plugin memoized computed targets in a
`.nx/cache` file keyed by config path+content and plugin options — but
**not** the plugin's own code version. So upgrading
`@naxodev/nx-cloudflare` to a version that changes the inferred targets
served stale targets until `nx reset`. This drops the cache entirely.
## Why
The staleness is not hypothetical: it surfaced while implementing #137 —
editing `buildWorkerTargets` (adding `serve`'s `readyWhen`) returned a
prior run's cached targets until the cache file was manually cleared,
because the key can't capture "the plugin's logic changed."
Two fixes were possible: version the cache key, or drop the cache.
Dropping wins here because the cache we had was both **non-idiomatic**
and **cheap to do without**:
- **It was non-idiomatic.** Official Nx plugins that cache inference
(`@nx/vite`, `@nx/eslint`, `@nx/jest`) store under
`workspaceDataDirectory` and key via `calculateHashesForCreateNodes` — a
project-file hash **plus the lockfile** plus the options hash. Ours
stored under `cacheDir` and keyed only on `configFile + content` (no
lockfile). The lockfile component is what gives the official caches *de
facto* upgrade-invalidation (a plugin bump almost always moves the
lockfile hash); ours lacked it, so it was strictly more stale-prone than
the upstream pattern.
- **Inference here is trivial.** Per config: one `dirname`, one
directory read for the project-marker guard, one config read+parse to
validate, and building a small static targets object. The cache bought
marginal speed for real staleness risk plus
`readTargetsCache`/`writeTargetsCache` complexity.
This is a deliberate divergence from the official cache-the-targets
pattern, justified by the trivial cost — not a claim that caching is
wrong in general. `@naxodev/gonx`'s `createNodesV2` is likewise
uncached, so this also aligns the two plugins.
## How
`createNodesV2` now calls `createNodesFromFiles(createNodesInternal,
...)` directly — no cache file, no try/finally. `createNodesInternal`
builds targets inline. Removes the
`readTargetsCache`/`writeTargetsCache` helpers, the `TargetsCache` type,
and the
`cacheDir`/`node:crypto`/`readJsonFile`/`writeJsonFile`/`existsSync`
plumbing.
## Tests
12 unit tests pass; lint and build clean. The two cache-framed specs are
reworded to assert the underlying invariants (options flow through to
target names; multi-config isolation) without referencing the removed
cache. The old "repeated runs (cache safe)" test is dropped — with no
cache, `createNodesInternal` is a pure function of its inputs, so re-run
equality is tautological and can't fail on any plausible logic change
(the `typegen.cache: true` assertion it carried is already covered by
the richer five-target test). No behavior change to the inferred targets
themselves.
## Links
- Closes #164
- Cache originated in #136 (#158); staleness surfaced in #137 (#162) ·
Part of #115 c0df7b81 fix(nx-cloudflare): strip dangling @nx/node prune targets from generated worker projects (#163) (#165)
## TL;DR
The application generator already strips `@nx/node`'s `build`/`serve`
targets, but `@nx/node` *also* emits `prune`, `prune-lockfile`, and
`copy-workspace-modules` — all `dependsOn: ['build']`. With `build`
deleted these dangle (`nx prune <app>` can't resolve its dep) and are
meaningless for a Worker anyway, since Wrangler bundles on deploy. This
extends `removeNodeAppTargets` to delete them too.
## Why
Pre-existing leftover, not introduced here — the old `addTargets` also
deleted `build` while leaving these three. But #137's goal is a coherent
inferred-target Worker project, and these Node deployment helpers
undercut it: they reference a target that no longer exists.
Confirmed against `@nx/node@22.7.5` source: its application generator
unconditionally emits `build`, `serve`, `prune-lockfile`,
`copy-workspace-modules`, and `prune` (the prune chain all `dependsOn`
`build`), and exposes **no** option (`bundler`/`framework`) to suppress
them — so stripping by name after generation is the only available
approach.
## How
`removeNodeAppTargets` now deletes an explicit allowlist of the
`@nx/node` app targets that don't apply to a Worker — `build`, `serve`,
`prune`, `prune-lockfile`, `copy-workspace-modules` — and keeps
`lint`/`test`. The allowlist is a named constant so the intent (and
what's deliberately kept) is legible.
## Reviewer notes
- **The allowlist is coupled to `@nx/node`'s target set.** A future
`@nx/node` that adds another build-dependent target would silently
reintroduce this exact dangling-target bug. Guarded by a structural test
(below) that fails CI if any surviving target depends on a stripped one,
rather than relying on the list staying hand-synced.
## Tests
Three assertions, covering both failure directions of an allowlist:
1. The existing "without executor targets" test now also asserts
`prune`/`prune-lockfile`/`copy-workspace-modules` are absent. Verified
non-vacuous: with the three removed from the allowlist the test fails
(`@nx/node` does emit them).
2. A new test asserts `lint` is **kept** — guards against the allowlist
degrading into a denylist/over-broad loop that wipes targets the Worker
still needs.
3. A new structural test asserts no surviving target `dependsOn` a
stripped one — the future-`@nx/node` guard described above.
30 unit tests pass; lint and format clean.
## Links
- Closes #163
- Follow-up from #137 (#162) · Part of #115 070a79c2 feat(nx-cloudflare)!: replace serve/deploy executors with inferred targets (#137) (#162)
## TL;DR
The `serve`/`deploy` executors duplicated what the #136 inference plugin
now provides, and the generator still wrote explicit executor targets
into `project.json` that **shadowed** the inferred ones. This deletes
both executors and stops generating those targets, so
`serve`/`deploy`/`typegen`/`version-upload`/`tail` come solely from
`@naxodev/nx-cloudflare/plugin`.
**Files to review (31, +169 / −1049):**
| File | Why |
|---|---|
| `packages/nx-cloudflare/src/generators/init/generator.ts` *(start
here)* | Registers the inference plugin in `nx.json` so generated
workspaces get inferred targets. |
| `packages/nx-cloudflare/src/generators/application/generator.ts` |
`addTargets` → `removeNodeAppTargets` (strips `@nx/node`'s build+serve);
`port` now feeds the wrangler config. |
| `packages/nx-cloudflare/src/plugin.ts` | Inferred `serve` gains
`readyWhen: 'Ready on'` so dependents wait for the dev server to
actually listen. |
|
`packages/nx-cloudflare/src/generators/application/files/config/{jsonc,toml}/wrangler.*__tmpl__`
| Generated config gains `dev.port`. |
| `packages/nx-cloudflare/package.json`, `project.json`,
`.eslintrc.json` | Drop the executors manifest entirely (no executors
ship); keep `wrangler` as a CLI runtime dep in dependency-checks. |
| `packages/nx-cloudflare-e2e/src/application.spec.ts` | Asserts
inferred targets instead of executors — the first true end-to-end
exercise of #136. |
| `packages/nx-cloudflare/README.md` | Documents inferred targets; drops
the executor option tables. |
## Why
Nx 22 Crystal favors inferring CLI-wrapping targets over bespoke
executors. The `serve` executor hand-rolled continuous-server plumbing
(`createAsyncIterable`, `waitForPortOpen`, signal handling) that
`continuous: true` now provides for free; `deploy` was a thin `wrangler
deploy` wrapper. #136 already infers all five targets — but **explicit
`project.json` targets win over inferred ones in Nx**, so for generated
apps the inference was inert until those explicit targets were removed.
This completes that migration.
## How
The generator's `init` step registers `@naxodev/nx-cloudflare/plugin` in
`nx.json` (idempotent), so a freshly generated app immediately gets
inferred targets. The application generator no longer writes
`serve`/`deploy`; it deletes the `build` and `serve` targets
`@nx/node`'s generator adds (neither applies to a Worker). The `port`
option moves into the generated wrangler config's `dev.port`, which
`wrangler dev` reads. With nothing left referencing them, both executors
and their four executor-only utilities are deleted.
## Reviewer notes
- **Breaking — pairs with #138.** Existing workspaces' `project.json`
files reference the deleted executors and break until #138's migration
strips those targets and registers the plugin. **#137 must ship in the
same release as #138.**
- **`removeNodeAppTargets` deletes `@nx/node`'s `build` *and* `serve`.**
The node generator emits an `@nx/js:node` `serve` (`dependsOn:
['build']`); with `build` gone it would both break and shadow the
inferred `serve`. The old `addTargets` masked this by overwriting
`serve`.
- **`serve` signals readiness via `readyWhen: 'Ready on'`.** The deleted
executor used `waitForPortOpen` to report ready only once the port was
listening; bare `continuous: true` doesn't gate on that. `wrangler dev`
prints `[wrangler:info] Ready on http://...` when listening (verified
against wrangler 4.26), so dependents wait correctly.
- **No executors ship, so no executors manifest.** Rather than an empty
`{ "executors": {} }`, the `executors` field, `executors.json`, and the
build/exports/lint references are removed — the Nx-recommended shape for
a generators+inference plugin (an empty manifest is vestigial).
`@naxodev/gonx` keeps its `executors.json` because its inference points
at real executors; nx-cloudflare deliberately infers `nx:run-commands`
instead.
- **`port` default is now `8787`, not `3000`.** This aligns
`normalizeOptions` with the schema's declared default (and Wrangler's),
fixing a pre-existing code/schema mismatch.
- **`wrangler` stays a `peerDependency` but is ignored by
`@nx/dependency-checks`.** The inferred targets invoke the `wrangler`
CLI (`externalDependencies: ['wrangler']`), but nothing imports it
statically anymore, so the dependency check needs the explicit ignore.
## Tests
82 unit tests pass. New/updated coverage: init registers the plugin
(idempotent); the application generator writes no
`serve`/`deploy`/`build` targets and emits `dev.port` (default 8787);
the e2e generates an app and asserts
`serve`/`deploy`/`typegen`/`version-upload`/`tail` are inferred (with
`serve` continuous) and not the old executor. The three e2e serve-runner
tests are unchanged — `nx serve` now runs `wrangler dev`, which still
prints `Starting local server`.
## Follow-up
- **#138** — migration to strip the old executor targets from existing
`project.json` files and register the plugin in existing workspaces.
- **#163** — strip `@nx/node`'s dangling
`prune`/`prune-lockfile`/`copy-workspace-modules` targets (pre-existing;
`dependsOn` the now-deleted `build`).
- **#164** — invalidate the createNodesV2 targets-cache on plugin
version change (cache key omits the plugin code version, so upgrades
need `nx reset`; pre-existing from #136).
## Links
- Closes #137
- Builds on #136 (#158)
- Epic #115 8e3d8210 docs: add CLAUDE.md with repo guidance for Claude Code (#161)
## TL;DR
This repo had no `CLAUDE.md`, so Claude Code started each session blind
to the monorepo's non-obvious shape — independent per-project releases,
the shared-Verdaccio e2e setup, and the `createNodesV2` inference
plugins. This adds one at the root covering commands, architecture, the
release/CI flow, and conventions.
## Why
The high-value context here lives across many files and isn't
discoverable from a single read: releases are independent per project
and triggered by a GitHub Release tag matching
`{projectName}@v{version}`; the two e2e suites share one Verdaccio
registry (hence serial CI, no Windows); unit tests run on Jest even
though nx-cloudflare *scaffolds* Vitest into generated user projects.
The doc front-loads exactly these gotchas so an agent doesn't rediscover
them by trial and error.
## Reviewer notes
- **Verify the facts, not the prose.** Each claim is drawn from
`nx.json` (release config), `.github/workflows/{ci,release}.yml`, the
`*-e2e` projects, and `tools/scripts/start-local-registry.ts` — worth a
sanity check against your mental model.
- **No release impact.** The file is workspace-root only and touches no
published project, so it won't trigger a version bump for `gonx` or
`nx-cloudflare`. bf4d6c9d feat(nx-cloudflare): add createNodesV2 inference plugin (#136) (#158)
## TL;DR
The plugin had no Nx 22 inference, so every Cloudflare Worker project
had to hand-write its `serve`/`deploy` targets in `project.json` — the
only `plugins/` entry was a Next.js webpack helper, not Crystal. This
adds a `createNodesV2` plugin that globs `wrangler.{toml,jsonc,json}`
and infers `serve`, `deploy`, `typegen`, `version-upload`, and `tail`
targets automatically, registered via `@naxodev/nx-cloudflare/plugin`.
**Files to review (3, +519 / -0):**
| File | Why |
|---|---|
| `packages/nx-cloudflare/src/plugin.ts` *(start here)* | The inference
plugin: glob → project guard → config gating → target builder, with a
computation cache. |
| `packages/nx-cloudflare/src/plugin.spec.ts` *(new)* | 13 tests driving
`createNodesV2` over temp-dir fixtures. |
| `packages/nx-cloudflare/package.json` | Adds the `./plugin` export
subpath. |
## Why
Nx 22 plugins infer targets from tool config rather than writing them
into `project.json`. This is the keystone of the modernization epic
(#115): it unblocks replacing the `serve`/`deploy` executors with
inferred run-commands (#137) and adopting `useProjectJson` (#140).
Registering `@naxodev/nx-cloudflare/plugin` in `nx.json` gives any
project beside a wrangler config its full Worker target set with zero
target wiring.
## How
Per matched config, the project root is the config's directory, and
targets are emitted only when a sibling `project.json` or `package.json`
is present — no phantom projects. The config is then gated:
`.json`/`.jsonc` must parse (`parseJson`, comments allowed); `.toml` is
accepted when readable and non-empty. A parse failure or an unreadable
directory warns and skips that one project.
Each target shells out to the Wrangler CLI via `nx:run-commands`
(`command` shorthand), run from the project root so Wrangler resolves
its own config:
| Target | Command | Cached | Continuous |
|---|---|:---:|:---:|
| `serve` | `wrangler dev` | — | yes |
| `deploy` | `wrangler deploy` | — | — |
| `typegen` | `wrangler types` | yes | — |
| `version-upload` | `wrangler versions upload` | — | — |
| `tail` | `wrangler tail` | — | yes |
A targets cache memoizes the calculation across graph constructions: one
cache file per plugin-options hash, with entries keyed by config path
plus content. Cache reads and writes are best-effort — a corrupt or
unwritable cache warns and recomputes rather than failing the graph.
## Reviewer notes
- **Run-commands, not executors.** Targets call the Wrangler CLI
directly rather than the existing `serve`/`deploy` executors, baking in
the #137 end-state so those executors can be removed next.
- **No `build` target.** Wrangler bundles on deploy; a standalone build
step is un-Cloudflare, so the only cacheable target is `typegen`
(deterministic `worker-configuration.d.ts` output, invalidated by
`externalDependencies: ['wrangler']`). `deploy`/`version-upload` have
remote side effects; `serve`/`tail` are long-running.
- **Public `@nx/devkit` only.** No `nx/src/*` deep imports — continuing
#139. The cache directory uses the public `cacheDir` because
`workspaceDataDirectory` isn't exported in the installed Nx 22.7.5.
- **Fail loud, per project.** An unparseable config, an unreadable
project directory, or a corrupt/unwritable cache warns and degrades to
no-targets-for-that-file or recompute; graph construction for the rest
of the workspace continues rather than aborting.
- **Structural gate, not semantic.** A parseable-but-minimal config
(e.g. `{}`) is accepted — Wrangler validates its own contents at
runtime. Empty `.toml` is rejected only because no TOML parser is
bundled, so non-empty is the available proxy for "usable".
- **Target names are configurable** via `CloudflarePluginOptions`
(`serveTargetName`, `deployTargetName`, …) to resolve collisions with
user-defined targets.
## Tests
13 unit tests in `plugin.spec.ts` over real temp-dir fixtures: the
five-target happy path, both project markers, plain `.json` and minimal
`{}` configs, no-marker skip, unparseable jsonc, empty toml, valid toml,
custom target names, options-keyed caching, multi-config isolation (one
valid + one invalid in a single invocation), cross-run cache identity,
and graceful skip when the project directory is unreadable. Run with `nx
test nx-cloudflare`.
## Links
- Closes #136
- Epic #115 af70c30a feat(nx-cloudflare)!: bump scaffolded deps to latest and migrate vitest config to v4 (#157)
## TL;DR
The init generator scaffolds worker projects with stale toolchain
versions — `vitest ^3.2.4`, `@cloudflare/vitest-pool-workers ^0.12.0`,
`wrangler ^4.26.0`. This bumps all five scaffolded dependencies to
latest, which drags `vitest` across a major (3 → 4) and `pool-workers`
to 0.16 — a release that **removed** the
`@cloudflare/vitest-pool-workers/config` subpath the generated
`vitest.config.ts` imported, so the config template is migrated to the
new `cloudflareTest` plugin API.
**Files to review (3, +24 / -24):**
| File | Why |
|---|---|
| `packages/nx-cloudflare/src/utils/versions.ts` *(start here)* | The
five version constants injected as devDependencies by `init`. |
| `.../application/files/common/vitest.config.ts__tmpl__` | Generated
vitest config migrated from `defineWorkersConfig` to the
`cloudflareTest` plugin. |
| `.../application/generator.spec.ts` | Inline snapshot updated to the
new config output. |
## Why
`vitest@^3.2.4` and `@cloudflare/vitest-pool-workers@^0.12.0` are
version-locked by peer deps — `0.12` peers `vitest 2.0.x - 3.2.x`, while
latest `0.16` peers `vitest ^4.1.0`. They can't be bumped independently.
Moving to latest means a coordinated vitest 3 → 4 major in every
newly-generated project.
Pool-workers 0.16 didn't just bump — it reshaped its config API:
| | Before (v3 / pool-workers 0.12) | After (v4 / pool-workers 0.16) |
|---|---|---|
| Config factory | `defineWorkersConfig` from
`@cloudflare/vitest-pool-workers/config` | `defineConfig` from
`vitest/config` |
| Pool wiring | `test.poolOptions.workers` | `cloudflareTest(...)`
plugin from package main |
| `/config` subpath | exported | **removed** — old import throws
`Missing "./config" specifier` |
A version-only bump would have shipped a generated `vitest.config.ts`
that fails at startup.
## How
1. Bump the three uncoupled deps to latest (`wrangler ^4.98.0`,
`@cloudflare/workers-types ^4.20260606.1`, `hono ^4.12.23`) —
same-major, no API change.
2. Bump the coupled pair together (`vitest ^4.1.0`,
`@cloudflare/vitest-pool-workers ^0.16.0`).
3. Migrate the generated `vitest.config.ts` template to the v4 plugin
shape:
```ts
import { cloudflareTest } from '@cloudflare/vitest-pool-workers';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: './<%= configFileName %>' },
}),
],
});
```
This matches Cloudflare's [canonical v4
config](https://developers.cloudflare.com/workers/testing/vitest-integration/configuration/)
— `test` is omitted when empty, mirroring the official docs example and
the `workers-sdk` pool-workers fixtures.
## Reviewer notes
- **Generated config matches Cloudflare's canonical output.** The shape
mirrors pool-workers' own `vitest-v3-to-v4` codemod
(`defineWorkersConfig` → `defineConfig`, `poolOptions.workers` → a
`cloudflareTest` plugin) and the official docs/fixture examples —
including omitting `test` when empty. Not an invented migration.
- **Breaking for regeneration only.** Existing generated projects are
untouched; the change affects output of future `init`/`application`
runs. Users who regenerate get vitest 4 and the new config.
- **wrangler bump self-consistent.** pool-workers 0.16.13 bundles
`wrangler@4.98.0` — exactly the version this PR pins, so no transitive
conflict.
## Tests
- **Unit:** 101 tests / 15 snapshots pass. The `vitest.config.ts` inline
snapshot in `generator.spec.ts` is updated to the new output; the
toml-configPath assertion still holds.
- **E2e:** `nx-cloudflare-e2e` (Verdaccio publish → install → generate →
serve) passes 8/8 — confirms vitest 4 + pool-workers 0.16 + wrangler
4.98 resolve and install cleanly together.
- **Runtime (manual):** an isolated worker project replicating the
generated files (worker + `cloudflare:test` unit & integration tests +
new config) runs green under `vitest@4.1.8` — `2 passed`. This surfaced
the removed `/config` subpath that the version bump alone would have
shipped broken; the e2e suite generates and serves apps but never runs a
generated app's tests.
## Follow-up
The e2e gap is worth closing: add a "generate app → run its vitest
tests" case to `nx-cloudflare-e2e` so future vitest/pool-workers bumps
are caught in CI rather than by hand. 1717a980 fix(nx-cloudflare): correct dependency classification, add exports map, drop deep @nx imports (#141, #142, #139) (#153)
## TL;DR
Corrects the published-package shape after the Next.js drop: `@nx/*`
stay in `dependencies` (matching every official Nx plugin — see below),
`wrangler` becomes a `peerDependencies` (the `@nx/vite`→`vite` pattern),
an `exports` map is added, and the two deep `@nx/*/src/...` imports that
were cheap to remove are vendored — which also drops the `@nx/web`
dependency.
**Files to review (7, +192 / -8):**
| File | Why |
|---|---|
| `packages/nx-cloudflare/package.json` *(start here)* | Dep
reclassification + `exports` map. |
| `src/utils/wait-for-port-open.ts` *(new)* | Vendored from `@nx/web`
(drops the dep). |
| `src/utils/wait-for-port-open.spec.ts` *(new)* | Direct coverage for
the vendored poller (real `net.Server`). |
| `src/utils/versions.ts` | `tslibVersion` now local (drops the
`@nx/node/src` deep import). |
| `src/executors/serve/executor.ts` + `executor.spec.ts` | Use the
vendored `waitForPortOpen`; route a port-wait failure through `error()`.
|
| `src/generators/init/generator.ts` | `tslibVersion` from local
`versions.ts`. |
## Why
`@nx/dependency-checks` and the Next.js drop exposed three packaging
issues (#141, #142, #139). The headline one — #141 — proposed moving
`@nx/*` to `peerDependencies`. Checking how Nx actually ships shows
that's backwards:
| Plugin | `@nx/*` + tslib | peerDependencies |
|---|---|---|
| @nx/react, @nx/node, @nx/js, @nx/jest, @nx/plugin | `dependencies` | —
|
| @nx/vite | `dependencies` | `vite`, `vitest` |
| @nx/esbuild | `dependencies` | `esbuild` |
Every official plugin keeps the `@nx/*` toolchain in `dependencies`;
`peerDependencies` is for the **third-party tool the plugin drives**.
(Full evidence on #141.)
## How
- **#141** — `@nx/*` stay in `dependencies` at `^22.0.0` (a range, not
the exact pin official plugins use, because this plugin releases
independently of Nx — the caret dedupes against the consumer's Nx 22.x).
`wrangler` → `peerDependencies` (`^4.0.0`); the `init` generator already
installs it into the consumer workspace, satisfying the peer.
- **#142** — `exports` map modeled on `@nx/vite`, including the
executor/generator subpaths Nx resolves. `./plugin` and
`./migrations.json` are deferred until the files exist (#136 / #138).
- **#139** — vendored `waitForPortOpen` (drops `@nx/web`) and moved
`tslibVersion` local (drops the `@nx/node/src` deep import). Final deps:
`tslib, @nx/devkit, @nx/js, @nx/node, @nx/vitest`.
## Reviewer notes
- **#139 scoped down deliberately.** No public API exists in Nx 22.7.5
for `determineProjectNameAndRootOptions`, the `ts-solution-setup`
helpers, `getImportPath`, etc. — vendoring those means copying large Nx
internals that will drift, worse than the deep-import risk. Only the two
tiny/stable helpers that also remove a dependency were vendored; the
rest are left for when Nx exposes public APIs or the generators are
reworked (#137/#140).
- **`wrangler` as a peer is verified.**
`require.resolve('wrangler/...')` satisfies `@nx/dependency-checks`, and
the e2e (which runs `serve` from the installed tarball) confirms `init`
provides it.
- **`@nx/node` stays** — the application generator still uses its public
`applicationGenerator`; only its deep `tslibVersion` import was removed.
## Reviewer notes
- **Port-wait failures now surface.** `createAsyncIterable` runs the
serve executor's listener fire-and-forget, so an `await
waitForPortOpen()` rejection previously escaped as an unhandled
rejection (silent hang / opaque crash) rather than failing the task.
It's now wrapped and routed through `error()`, matching the existing
`spawn` error paths. Pre-existing, fixed here since the PR reworks this
module.
- **Vendored poller is now tested.** `wait-for-port-open.spec.ts`
exercises the real retry/connect logic against ephemeral ports (resolve
on connect, retry-then-resolve, reject after retries, immediate reject
on a non-retryable code) — the executor spec mocks the module, so this
is its only direct coverage.
## Tests
- `nx lint nx-cloudflare` (incl. `@nx/dependency-checks`) + `build` +
`test` (10 suites / 100) green; `nx format:check` clean.
- `nx e2e nx-cloudflare-e2e` — **8/8**, validating the `exports` map
does not break Nx's generator/executor resolution from the installed
tarball and that the `wrangler` peer is satisfied.
- `pnpm install --frozen-lockfile` in sync.
## Links
- Closes #141, #142, #139
- Part of #115 b600fba5 feat(nx-cloudflare): generate wrangler.jsonc by default with configFormat option (#127) (#154)
## TL;DR
The application generator emitted `wrangler.toml`, but Cloudflare
recommends `wrangler.jsonc` (C3 scaffolds it, some features are
JSON-only) and JSONC is far safer to edit programmatically — which the
upcoming binding generator depends on. The generator now emits
`wrangler.jsonc` by default and accepts `configFormat: 'jsonc' | 'toml'`
to keep TOML available.
**Files to review (10, +196 / -35):**
| File | Why |
|---|---|
| `src/generators/application/generator.ts` *(start here)* | Renders the
selected config folder; passes `configFileName`, raw `accountId`, and
the `offsetFromRoot`-based `$schema` path to templates. |
| `src/generators/application/files/config/jsonc/wrangler.jsonc__tmpl__`
*(new)* | The default JSONC config template. |
| `src/generators/application/files/config/toml/wrangler.toml__tmpl__` |
TOML template, moved out of `common/` and updated to render `account_id`
inline. |
| `src/generators/application/files/common/vitest.config.ts__tmpl__` |
`poolOptions` `configPath` is now templated to the chosen file. |
| `src/generators/application/schema.json` / `schema.d.ts` | New
`configFormat` option, default `jsonc`. |
| `src/generators/application/generator.spec.ts` | Coverage for both
formats, `account_id` rendering, and the vitest `configPath`. |
## Why
`wrangler.jsonc` is Cloudflare's default and recommended config format.
JSONC also unblocks programmatic editing — the binding/resource
generator (#133) needs to insert bindings into the config, which is safe
in JSON and fragile in TOML.
## How
1. `configFormat` (`jsonc` | `toml`, default `jsonc`) selects which
config template renders.
2. The two config templates move into `files/config/{jsonc,toml}/`; the
generator globs only the selected folder, so exactly one config file is
emitted.
3. `account_id` renders inside each config template via EJS conditionals
(format-specific syntax/placement), replacing the TOML-only
`get-account-id.ts` helper, which is removed.
4. The vitest `poolOptions.configPath` is templated to
`wrangler.<format>` so test wiring tracks the chosen file.
5. The JSONC carries a `$schema` reference, computed with
`offsetFromRoot(projectRoot)` so the path to the workspace-root
`node_modules/wrangler/config-schema.json` is correct at any project
depth.
## Reviewer notes
- **`$schema` resolves to the workspace root, not wrangler's real
install location.** `offsetFromRoot` produces
`../…/node_modules/wrangler/config-schema.json`, which assumes the
standard Nx hoisting. The generator runs against a virtual tree where
wrangler often isn't installed yet, so a dynamic `require.resolve` would
throw — the offset convention is deterministic and matches what C3
emits. In a non-hoisted setup it degrades harmlessly: the editor skips
schema validation and Wrangler ignores `$schema` at runtime.
- **TOML stays fully supported.** `configFormat: 'toml'` reproduces the
previous output (verified by snapshot) — this is additive, not a
removal. TOML gets no `$schema` (no such convention).
- **Executors need no change.** Wrangler auto-discovers
`wrangler.{toml,jsonc,json}`; neither serve nor deploy hardcodes the
config path.
## Tests
`generator.spec.ts` adds coverage for: default JSONC output,
`configFormat: 'toml'` output, `account_id` rendering in each format
(whitespace pinned via snapshots), the `$schema` path at a nested depth,
the `js: true` entry extension, and the vitest `configPath` for each
format. Full suite green — 101 tests, lint, and build all pass; both
template folders ship to `dist`. Run with `nx test nx-cloudflare`.
## Links
- Closes #127
- Part of #115 (nx-cloudflare modernization epic) 712a9ab5 feat(nx-cloudflare)!: drop Next.js support (remove next-build + webpack subsystem) (#126) (#152)
## TL;DR
The Next.js integration was built on the deprecated
`@cloudflare/next-on-pages`; rather than migrate to OpenNext, this drops
it entirely. Removes the `next-build` executor, the vendored webpack
subsystem, the example app, and 14 now-dead dependencies — **46 files,
+52 / −5185**.
**Files to review (the manifests; everything else is bulk deletion):**
| File | Why |
|---|---|
| `packages/nx-cloudflare/package.json` *(start here)* | The slimmed
dependency set — confirm nothing surviving code still needs. |
| `package.json` (root) | Removed the workspace-level Next/webpack
devDeps. |
| `nx.json` | Dropped the `@nx/next` inference plugin + generator
default. |
| `packages/nx-cloudflare/executors.json` | `next-build` entry removed.
|
| `packages/nx-cloudflare/README.md` | Next.js section + feature bullet
removed. |
Breaking change (`!`) — `nx release` cuts a major for `nx-cloudflare`.
## Why
`@cloudflare/next-on-pages` is deprecated and the integration was
experimental. Migrating to OpenNext isn't worth the surface area;
dropping it slims the plugin substantially and unblocks the rest of the
modernization epic (#115).
## How
Pure removal: the `next-build` executor (`src/executors/next-build/**`),
the webpack subsystem (`plugins/with-nx.ts`,
`src/utils/{config,compose-plugins,create-copy-plugin,types}.ts`), and
the `examples/cloudflare-next-app/` app — whose `project.json` was the
only consumer of the deleted executor.
Dependency removal cascaded beyond the five the issue named.
`next-build` was the sole importer of six more (`dotenv`, `semver`,
`@nx/workspace`, `typescript`, `nx`, `fs-extra`), so the plugin's
`@nx/dependency-checks` lint flagged all eleven once the code was gone.
The eight Next/webpack deps at the workspace root and the `@nx/next`
entry in `nx.json` are likewise unused after the example app's removal.
## Reviewer notes
- **The plugin lost 11 deps, not 5.** The six beyond the issue's list
(`dotenv`, `semver`, `@nx/workspace`, `typescript`, `nx`, `fs-extra`)
were only ever imported by `next-build` code — verified zero remaining
imports. The 7 survivors (`tslib`, `wrangler`, `@nx/js`, `@nx/web`,
`@nx/node`, `@nx/devkit`, `@nx/vitest`) are all still imported by the
generators/executors.
- **React deps deliberately kept.**
`react`/`@nx/react`/`@testing-library/react`/`@types/react*` aren't
Next-specific and `react` still has a usage; an orphan-React cleanup is
left for a separate PR.
- **Migration deferred to #138.** #126 mentions a consumer migration to
strip the `next-build` target, but the plugin has no migrations
infrastructure yet — that's #138's scope. Tracked there to keep this a
focused deletion.
## Tests
- `nx lint nx-cloudflare` (incl. `@nx/dependency-checks`) + `build` +
`test` (9 suites / 95) green; the obsolete `next-build`/`with-nx` specs
are gone, `schema-contract.spec.ts` still asserts only `serve`/`deploy`.
- `nx e2e nx-cloudflare-e2e` — **8/8**, validating the slimmed published
tarball still installs and generates via the real install path.
- nx graph loads (no `@nx/next` plugin), `pnpm install
--frozen-lockfile` in sync, `nx format:check` clean, zero dangling
references repo-wide.
## Follow-up
- ⚠️ **External Cloudflare Pages deploy.** The repo's "Cloudflare Pages"
check builds the now-deleted example from the CF dashboard, so it fails
on this PR. Remove that Pages project (or drop it from required checks)
in the dashboard — it's not in the repo's control.
- Orphaned React-ecosystem deps and `runs-on: ${{ matrix.os }}` in
`ci.yml` — separate cleanups.
## Links
- Closes #126
- Part of #115 36e21b6d test(e2e): migrate nx-cloudflare and gonx e2e to createTestProject + Verdaccio (#143) (#151)
Migrates **both** the `nx-cloudflare-e2e` and `gonx-e2e` suites from the
legacy `ensureNxProject` fixture (which copies the built `dist` into a
workspace) to `createTestProject` — creating a real Nx workspace and
installing the plugin from the local Verdaccio registry. This exercises
the actual published-tarball install path consumers use (peerDeps,
package `exports`, executor resolution), which the old fixture never
touched.
Closes #143 (also covers the equivalent gonx migration).
## Changes
- **`e2e-utils/src/lib/create-test-project.ts`** — add
`createTestProject(plugin)` composing the previously-unused
`newNxProject()` + `installPlugin()`. Pin the generated workspace to
this repo's Nx version (not `@latest`). Harden the `create-nx-workspace`
subprocess env (`CI=true`, `GIT_CEILING_DIRECTORIES`, Nx Cloud
disabled).
- **`nx-cloudflare-e2e`** (2 specs) / **`gonx-e2e`** (4 specs) —
`createTestProject(...)` in `beforeAll`; `runNxCommand` → `runCLI`;
`beforeEach`/`afterEach` → `beforeAll`/`afterAll`. Assert via `nx show
project --json` where format-agnostic. go-blueprint keeps its
non-interactive (CI) skip guard.
- **`.verdaccio/config.yml`** — non-proxied `@naxodev/*` rule.
- **`nx.json`** — pin `cli.packageManager` to `pnpm`.
- **`.github/workflows/ci.yml`** — run e2e serially (`--parallel=1`) +
`timeout-minutes` backstop.
## Bugs this surfaced (all invisible to `ensureNxProject`)
1. **Local-registry publish silently failing** (`409 "already present"`)
— Verdaccio proxied `@naxodev/*` from npm and rejected the local
publish, so the `e2e` dist-tag was never created. Fixed by the
non-proxied scope rule.
2. **`create-nx-workspace` git conflict** — detects the outer repo,
skips `git init`, then `git add` fails because the workspace is under
the gitignored `tmp/`. Fixed with `GIT_CEILING_DIRECTORIES`.
3. **`bun: not found` on CI** — Nx PM auto-detection reached a `bun`
code path during `nx-release-publish`; bun is installed locally but
absent on CI runners, crashing the verdaccio task. Pinning
`cli.packageManager: pnpm` removes every bun path.
4. **Parallel e2e hangs on the shared registry** — both suites share one
Verdaccio; running them in parallel races on registry
ownership/teardown. Because installs run through a blocking `execSync`,
a registry torn down mid-run (or a teardown drain that stalls) pins jest
forever (its async timeout can't fire). Fixed by running e2e serially;
the `timeout-minutes` cap is a backstop.
5. **Library generator emits `package.json`-based config** under Nx's
TS-solution setup, not `project.json` — the old file-existence assertion
was format-coupled. Now asserted via the project graph.
## Verification
Reproduced the CI runner environment locally by **hiding `bun` from
`PATH`** and running both suites the way CI now does (`nx run-many -t
e2e -p nx-cloudflare-e2e gonx-e2e --parallel=1`), under a hard `timeout`
to prove the process exits (no hang):
- nx-cloudflare-e2e — **8/8 pass**
- gonx-e2e — **38/38 pass** (4 suites)
- completes in ~7 min, exits cleanly; publish succeeds, no `bun: not
found`, no npmjs fallback.
- `tsc --noEmit` + `eslint --quiet` + `nx format:check` clean.
Part of #115.
---
Note (out of scope, flagging for a follow-up): `ci.yml` hardcodes
`runs-on: ubuntu-latest` instead of `${{ matrix.os }}`, so the
`macos-latest`/`windows-latest` matrix entries actually run on ubuntu —
the cross-OS coverage is currently illusory. dd2ca846 fix(nx-cloudflare): correctness fixes for Wrangler v4, CLI args, and generators (#150)
Clears the Tier-0 correctness bugs from the nx-cloudflare modernization
epic (#115). Each fix is its own commit and was developed test-first.
## What's fixed
**CLI / executors**
- `createCliOptions` no longer drops `false`/`0`/`""`, renders booleans
as `--flag`/`--no-flag`, and repeats array flags instead of
comma-joining (closes #118)
- Removed Wrangler v4-broken / legacy executor options: `nodeCompat`,
`latest`, Workers Sites (`site`/`siteInclude`/`siteExclude`),
`localUpstream`, `upstreamProtocol` (closes #116, #117, #125)
- Added the `dryRun` option to the deploy schema so it matches its type
and docs (closes #119)
- serve & deploy now resolve the same absolute `cwd` via a shared,
tested helper; serve no longer mutates `process.env.PWD`; both use
`spawn()` instead of `fork()` (closes #124)
**Generators**
- `compatibility_date` is pinned to the worker's creation date instead
of a stale hardcoded `2024-01-01` (closes #123)
- `addTargets` fails loud instead of swallowing errors with
`console.error` (closes #121)
- Removed the unused `frontendProject` option that promised proxy setup
it never performed (closes #120)
**Packaging**
- Added the missing `src/index.ts` public barrel so `main`/`typings`
resolve and the generators can be composed programmatically (closes
#122)
## Tests
New unit tests added test-first for every behavior change:
`create-cli-options` (10), executor `schema-contract` (14),
`project-paths` (4), public-API barrel (1), plus a
current-`compatibility_date` generator test. Package suite: **95
passing**, lint clean, build green. 5a507ee0 feat(gonx): evaluate Go build tags during static analysis (#106)
Closes #101, #107, #109, #110, #111, #112.
## What
Adds in-source Go build-constraint evaluation (`//go:build` and legacy
`// +build`) to the gonx static-analysis pipeline. A file gated to a
different GOOS/GOARCH than the host no longer contributes edges to the
project graph.
Now also includes the review-driven follow-ups from the comprehensive PR
review (commit `fix(gonx): address build-tag review findings`) and five
of the seven deferred follow-ups (commit `fix(gonx): apply
S1/S3/S4/S5/S6 review follow-ups` — landed alongside since they touch
the same files).
## Closed by this PR
- #101 — original feature (build-tag evaluation)
- #107 (S1) — literal-union types `GoOS`/`GoArch` with `(string & {})`
escape hatch
- #109 (S3) — `BuildContext.tags` now required, empty-set default
- #110 (S4) — docs/comment nits (gofmt framing, citation, Android in
`unix`, `#108` anchor)
- #111 (S5) — `withConstraint` helper used consistently in spec
- #112 (S6) — test pinning that block-comment constraints are not
directives
## Tracked for future PRs
- #108 (S2) — `cgoEnabled` + `goVersion` on `BuildContext` (feature,
needs design discussion)
- #113 (S7) — freeze default context + cygwin handling (behavior change)
- Test/build import differentiation (#102) and WASM/Jest hardening
(#104) from the original gap list
## Behavioral semantics
| Tag form | Behavior |
|---|---|
| GOOS / GOARCH idents (e.g. `linux`, `amd64`) | Match host context |
| `unix` | Matches Linux, Darwin, BSDs, Solaris, AIX, Android, illumos,
iOS, dragonfly, hurd (Go's full `internal/syslist.UnixOS`) |
| `go1.X` | Always true (no Go compiler to consult; over-include is
safer) |
| `cgo` | Always false (static analysis never runs cgo) |
| Unknown idents | Treated as user tags; false unless in
`BuildContext.tags` |
| Malformed expression | Falls back to include + `logger.warn` with file
path |
When both `//go:build` and `// +build` are present, modern wins per the
Go spec.
## Out of scope
- Filename-based suffixes (`foo_linux.go`) — known gap, separate
follow-up
- A `GoPluginOptions` field to set user tags — `BuildContext.tags`
plumbing is in place internally; plugin-option surface tracked in #108
## Verification
- `nx test gonx` → **250/250 pass** (was 216 → 237 → 248 → 250 across
iterations)
- `tsc --noEmit -p tsconfig.lib.json` → clean
- `tsc --noEmit -p tsconfig.spec.json` → clean
- `nx lint gonx` → 0 errors
- `nx format:check` → clean