@mnci/cli 1.1.0

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.
package/README.md ADDED
@@ -0,0 +1,469 @@
1
+ # @mnci/cli
2
+
3
+ > A **thin CLI over what Nx already ships**: an opinionated one-command Nx
4
+ > monorepo with automatic commit-message versioning, instead of hand-rolling
5
+ > templates, configs and CI engines.
6
+
7
+ ## The thesis
8
+
9
+ Most of what a monorepo tool needs to hand-roll — a template engine, a shared
10
+ config package, a custom CI engine, a dependency-injection step for published
11
+ packages, a doctor/drift-sync system to keep it all consistent — already has a
12
+ first-party (or established community) Nx equivalent:
13
+
14
+ | Hand-rolled elsewhere | This CLI uses instead |
15
+ | ------------------------------------------------ | ----------------------------------------------------------- |
16
+ | Template engine + per-project config files | `create-nx-workspace --preset=ts` + `nx g` plugin generators |
17
+ | A shared toolchain package for configs | The configs the Nx generators emit (one root ESLint/tsconfig) |
18
+ | A custom multi-step CI engine | `nx affected -t lint,test,build` + `nx release` (~60-line pipeline) |
19
+ | A dist-package dependency injector | `nx release` updates dependent versions natively |
20
+ | Hand-written Azure Function templates | `@nx/node:application` (plain Node app) + a thin Azure Functions v4 overlay |
21
+ | doctor/drift sync of tool-owned files | Nothing to drift: this CLI owns 5 small files, Nx owns the rest |
22
+
23
+ ## Commands (deliberately just two)
24
+
25
+ ```sh
26
+ mnci2 new my-repo # create a monorepo (prompts scope + registry)
27
+ mnci2 new my-repo --yes --registry npm --scope @my
28
+
29
+ cd my-repo
30
+ mnci2 add react-app web # @nx/react (Vite + Jest)
31
+ mnci2 add node-app svc # @nx/node (plain Node app, esbuild)
32
+ mnci2 add node-function-app api # @nx/node + an Azure Functions v4 overlay
33
+ mnci2 add npm-lib sdk # @nx/js publishable lib -> packages/
34
+ mnci2 add internal-lib utils # @nx/js private lib -> libs/
35
+
36
+ # Python (@mnci/nx-python-pip — pip + Ruff + pytest + PyPA build/twine, no uv)
37
+ mnci2 add python-app svc # app -> apps/ (wheel, zipped into the drop)
38
+ mnci2 add python-function-app fn # Azure Functions (Python v2) -> apps/
39
+ mnci2 add python-lib shared # publishable -> python-packages/ (twine upload)
40
+ mnci2 add python-internal-lib core # private shared lib -> libs/
41
+ ```
42
+
43
+ Everything else is plain Nx, surfaced as a small curated set of root scripts —
44
+ each a single cross-platform command:
45
+
46
+ | Script | Runs |
47
+ | --------------------- | ------------------------------- |
48
+ | `npm run build` | `nx run-many -t build` |
49
+ | `npm run lint` | `nx run-many -t lint` |
50
+ | `npm run test` | `nx run-many -t test` |
51
+ | `npm run affected` | `nx affected -t lint,test,build` (vs `main`) |
52
+ | `npm run graph` | `nx graph` |
53
+ | `npm run release:preview` | `nx release --dry-run` |
54
+ | `prepare` | `husky` (commit-msg lint hook) |
55
+
56
+ ## What `new` actually does
57
+
58
+ 1. `npx create-nx-workspace@latest <name> --preset=ts` — npm workspaces +
59
+ TypeScript project references. Libraries get **no `project.json`**; targets
60
+ are inferred from each project's tsconfig/package.json.
61
+ 2. Patches `nx.json` with the release opinion (the only config Nx has no
62
+ default for): independent versioning from **conventional commits**,
63
+ `{projectName}@{version}` tags, **tag-only git** (`commit: false`) — nothing
64
+ is ever pushed to `main`; future runs resolve versions from tag names.
65
+ 3. Writes `.npmrc` (Azure Artifacts feed or public npm — scope routing makes
66
+ accidental public publishes impossible), `commitlint.config.mjs`, a husky
67
+ `commit-msg` hook, the chosen CI provider's pipeline file(s)
68
+ (`azure-pipelines.yml` and/or `.github/workflows/ci.yml`, `--ci`, default
69
+ `azure`), and the curated root scripts.
70
+ 4. Installs the chosen **stack** (see below), `husky` + `@commitlint/*` for
71
+ real, so versions resolve at generation time.
72
+
73
+ ## Stack: two choices asked up front
74
+
75
+ `mnci2 new` (run bare, or with flags) asks two questions — the linter and the
76
+ test runner. Each is stored where every later `mnci2 add` honours it, so the
77
+ whole workspace stays one stack:
78
+
79
+ | Question | Options | Default | Stored as / honoured via |
80
+ | -------------- | ------------------ | ------- | ------------------------ |
81
+ | `--linter` | `eslint` \| `oxlint` | `eslint` | `nx.json` generator `linter` default (`none` for oxlint) + a typed `oxlint.config.mts` + `oxfmt.config.mts` (both from the [oxc-standard](https://github.com/JohnDeved/ox-standard) preset) + the `oxlint` / `oxfmt` root scripts |
82
+ | `--test-runner`| `jest` \| `vitest` | `jest` | `nx.json` generator `unitTestRunner` default; the hand-built function app follows it too |
83
+
84
+ TypeScript is not a question — every workspace runs the **dual compiler** from
85
+ [Nx's TS 7 guide](https://nx.dev/docs/technologies/typescript/guides/typescript-7):
86
+ `typescript` resolves to a TS 6 package (keeping the programmatic API that Nx's
87
+ graph/plugins, Vite, typescript-eslint and the editor need) while
88
+ `@typescript/native` provides TS 7's native `tsc`. The inferred
89
+ `typecheck`/`build` tasks then run on the **fast TS 7 compiler**, and Nx keeps
90
+ analysing config through the TS 6 API — no target rewiring, frozen per repo by
91
+ the lockfile. (A plain `typescript@7` install would break Nx, since TS 7 ships
92
+ no programmatic API yet; the two aliases are what make it work.)
93
+
94
+ - **Linter**: ESLint is a per-project Nx target; oxlint is a single
95
+ workspace-wide binary. Either way `npm run lint` (and the CI) is
96
+ linter-agnostic, so nothing downstream branches. Under oxlint a publishable
97
+ package's private-lib import needs no dependency-check override (that rule is
98
+ ESLint-only), so none is written.
99
+ - The oxlint stack installs **[oxc-standard](https://github.com/JohnDeved/ox-standard)**
100
+ (which brings `oxlint` + `oxfmt`) and generates two `.mts` configs that use
101
+ its **JavaScript Standard Style** preset: `oxlint.config.mts` *extends*
102
+ `oxc-standard`'s rule set (unicorn + React + react-perf + TypeScript + oxc),
103
+ and `oxfmt.config.mts` mirrors its formatting (no semicolons, single quotes,
104
+ 2-space, `es5` trailing commas, avoid arrow parens). That gives the full
105
+ Standard experience — **linting *and* formatting** — since oxlint (a linter)
106
+ does not enforce layout.
107
+ - Formatting is `npm run format` (write) and `npm run format:check` (CI-safe,
108
+ no writes). Nx generators emit semicolon/double-quote code, so run
109
+ `npm run format` once after scaffolding to normalise a new workspace to
110
+ Standard Style. (CI stays lint/test/build only — formatting is left as a
111
+ local/pre-commit step so the pipeline needs no linter-specific branch.)
112
+ - **Test runner**: passed straight to the `@nx/*` generators; the function app
113
+ gets a matching `jest.config.mjs` (+ ts-jest) or `vitest.config.ts`.
114
+
115
+ ## Layout convention = release scoping
116
+
117
+ | Directory | Contents | Released? |
118
+ | ------------------ | ------------------------------------------------- | ---------------------------- |
119
+ | `apps/` | React / Node / Python apps (plain or Azure Functions) | Never (packed into the drop) |
120
+ | `packages/` | Publishable npm libraries (rollup-bundled) | Yes — `nx release`, per-package tags |
121
+ | `python-packages/` | Publishable Python packages (hatchling wheels) | Yes — `twine upload` (Azure Artifacts) |
122
+ | `libs/` | Internal libraries (TS or Python), never published | Never |
123
+
124
+ No custom tags, no stamp file — the directory (and, for npm, the `private` flag)
125
+ are the whole model. Publishable Python packages get their own
126
+ `python-packages/` dir so the npm `nx release` (`packages/*`) is never entangled
127
+ with Python publishing.
128
+
129
+ Every kind builds to its own Nx-default output location (`apps/<name>/dist`,
130
+ `packages/<name>/dist`, ...) — v2 does no post-generation build-output
131
+ rewiring for any kind. `mnci2 add` is pure delegation to the official
132
+ generators; each one's own default is left exactly as-is.
133
+
134
+ ## Published packages CAN depend on internal libraries
135
+
136
+ Import an internal lib from an npm-lib directly — and do **not** add it to the
137
+ npm-lib's `dependencies` (npm workspaces links every workspace member into the
138
+ root `node_modules` regardless):
139
+
140
+ ```ts
141
+ // packages/sdk/src/lib/sdk.ts
142
+ import { utils } from '@demo/utils' // libs/utils — private, never published
143
+ ```
144
+
145
+ It works because npm-libs are **rollup** bundles: `@nx/rollup`'s `withNx`
146
+ externalizes exactly what the manifest declares (`dependencies` +
147
+ `peerDependencies`), so real npm deps stay external and declared, while the
148
+ undeclared internal lib is compiled from source INTO the bundle — the private
149
+ name never reaches the published `package.json`. Trade-off: the published
150
+ output is a single bundle (no per-file deep imports).
151
+
152
+ React apps go the other way (Vite bundles everything by default), and the e2e
153
+ proves both directions for real: unlike the published `npm-lib`, which must
154
+ keep real npm dependencies **external** (declared, not bundled) for the
155
+ published tarball to install correctly downstream, a `react-app` build has no
156
+ install step at deploy/runtime, so it inlines **everything** — the private
157
+ internal lib AND real npm dependencies alike.
158
+
159
+ Node apps (`node-app`/`node-function-app`) are a third case: `@nx/node:application`'s
160
+ esbuild build is **non-bundled** — it transpiles each file individually and
161
+ mirrors the workspace tree into `dist`, so nothing is ever textually inlined.
162
+ A private internal lib is compiled by its own `tsc` build and copied into
163
+ `dist` at its own path (resolved by a real `require` at run time, the same
164
+ way npm workspaces resolve it during development); a real npm dependency
165
+ stays a real `require` too, resolved from `node_modules` — present locally,
166
+ or installed at deploy time (see "How Node apps work" below).
167
+
168
+ Cross-project imports (`@scope/lib`) resolve through **TypeScript project
169
+ references** under `--preset=ts`, and those references are maintained by
170
+ `nx sync`, not by the generators. `mnci2 add` runs `nx sync` for you right
171
+ after generation — but references also go stale **any time you hand-edit a
172
+ file to add a new cross-project import** later (nothing about that is an
173
+ `mnci2 add`, so that step can't catch it). For that case every generated
174
+ workspace sets `sync.applyChanges: true` in `nx.json`: `--preset=ts` already
175
+ registers the `@nx/js:typescript-sync` generator on the `build`/`typecheck`
176
+ targets, so instead of just *prompting* ("Would you like to sync the
177
+ identified changes?") on your next `nx build`/`typecheck`/`affected`, Nx fixes
178
+ the references **automatically** — no prompt, no manual `npx nx sync`. A
179
+ brand-new package may still need one VSCode window reload to be picked up by
180
+ the TypeScript server.
181
+
182
+ `applyChanges` only affects *interactive* runs, by design: CI always runs sync
183
+ generators in dry-run mode and fails instead of silently patching an ephemeral
184
+ checkout that never gets committed. That's what the pipeline's `nx sync:check`
185
+ step (below) surfaces early — if it fails, run `npx nx sync` locally and
186
+ commit the result.
187
+
188
+ ## CI (Azure Pipelines and/or GitHub Actions, any agent OS)
189
+
190
+ `mnci2 new` asks which CI provider(s) to write a pipeline file for (`--ci`,
191
+ default `azure`): `azure` writes `azure-pipelines.yml`, `github` writes
192
+ `.github/workflows/ci.yml`, `both` writes both — pick `github` for a
193
+ GitHub-hosted repo, or `both` while migrating between the two. Whichever
194
+ provider(s), the pipeline does the **exact same thing**: both files are built
195
+ from the same shared guard scripts (`overlay.ts`'s `PYTHON_INSTALL_GUARD`,
196
+ `PACK_APPS_GUARD`, `releaseGuard`), so they can never drift on what CI
197
+ actually runs — only the provider's own syntax differs.
198
+
199
+ The pipeline contains **no bash and no PowerShell**: every step is a built-in
200
+ task/action or a single-line `git`/`npm`/`npx`/`node` command that `cmd.exe`
201
+ and `sh` execute identically, so it runs unchanged on Linux, macOS and Windows
202
+ agents. The build agent/runner is your choice at `mnci2 new` (`--agent`,
203
+ default `ubuntu-latest`): on Azure a Microsoft-hosted image
204
+ (`ubuntu-`/`windows-`/`macos-…`) becomes `pool.vmImage`, anything else a
205
+ self-hosted `pool.name`; on GitHub the same value is passed straight through
206
+ as `runs-on:` (GitHub's own hosted runner labels already match the common
207
+ Azure vmImage names, and a self-hosted label is just as valid there).
208
+
209
+ Every run (PR and main) first does `nx sync:check` (fails fast and clearly if
210
+ the workspace wasn't synced+committed locally — see above), then one
211
+ `nx run-many -t lint,test,build`. Pushes to `main` then:
212
+
213
+ - **Pack all apps** — each app's `package` target zips its build output into
214
+ `dist/drop/<type>-<name>.zip` (e.g. `node-function-app-api.zip`,
215
+ `react-app-web.zip`); the whole `dist/drop` is published as the **`drop`**
216
+ artifact.
217
+ - **Tag the run per app** *(Azure only)* — one build tag per zip, **exactly**
218
+ `<type>-<name>` (derived from the zip filenames, so the tag can never drift
219
+ from the artifact). A classic Azure release/CD pipeline keys its trigger off
220
+ these; GitHub Actions has no equivalent mechanism, so the `drop` artifact
221
+ (one zip per app inside it) is the portable substitute there.
222
+ - **Release — version, tag and publish** — one `npx nx release --yes` for both
223
+ npm (`packages/*`) and Python (`python-packages/*`): version bump from
224
+ conventional commits → `{projectName}@{version}` git tag pushed to `main`
225
+ (tag-only, never a commit) → publish to the feed (npm via `.npmrc`, Python via
226
+ `twine` when an Azure feed is configured — installed from the generated
227
+ `requirements-dev.txt`, no uv, no Poetry). Reuses the base64 `PAT`, decoded to
228
+ the raw token twine needs for the Python publish. Skipped cleanly when there
229
+ is nothing to release. A guarded step installs the fixed Python toolchain
230
+ (`ruff`/`pytest`/`build`/`twine`) before any Python target runs, skipped
231
+ cleanly on a workspace with no Python projects.
232
+
233
+ **npm auth** is the base64 `PAT`, read the same way on both providers but from
234
+ a different place: on Azure Pipelines, a **variable group**
235
+ (`--variable-group`, default `Build`) exposes it as `$(PAT)`; on GitHub
236
+ Actions it's a plain repository (or environment) **secret** named `PAT`, read
237
+ as `${{ secrets.PAT }}` — GitHub has no "variable group" concept, so unlike
238
+ Azure this needs no CLI-collected name, just a secret you create once in the
239
+ repo settings. Either way it's mapped as `env` on the npm steps and read by
240
+ the root `.npmrc`'s `_password` block — the PAT value never lands in a file.
241
+ No `npmAuthenticate@0` task (it would overwrite the hand-set password).
242
+
243
+ On Azure, two one-time grants are required (project admin): **Contribute** on
244
+ the repo for the *Project Collection Build Service* account (tag push), and
245
+ **publish** rights on the feed for the PAT's owner. On GitHub, the workflow's
246
+ `permissions: contents: write` is what lets its own checkout token push the
247
+ release tag — no separate grant, but the job still needs that permission
248
+ line (already generated) and, for a fork-based PR, GitHub disables
249
+ write permissions by default (not a concern for pushes to `main` from the
250
+ repo itself, which is the only case this pipeline ever releases from).
251
+
252
+ **The one PAT, two different encodings — read this before wiring a third
253
+ protocol.** The same `PAT` value (`$(PAT)` on Azure, `secrets.PAT` on GitHub)
254
+ is base64-encoded throughout — that's the raw value Azure Artifacts' "Connect
255
+ to feed" instructions give you. npm's `.npmrc` `_password` field expects
256
+ exactly that pre-encoded form, so it's used as-is. `twine`/pypi basic auth, by
257
+ contrast, wants the **raw** token — so the shared `releaseGuard` fragment
258
+ (`overlay.ts`, used by both `azurePipelinesYaml` and `githubActionsYaml`)
259
+ explicitly *decodes* the same `PAT`
260
+ (`Buffer.from(process.env.PAT, 'base64').toString()`) before handing it to
261
+ `TWINE_PASSWORD`. Both are correct for their protocol today, but it's an easy
262
+ trap to get backwards: if you ever wire a third registry protocol, check
263
+ whether it wants the pre-encoded or the raw form before assuming either
264
+ convention.
265
+
266
+ ## Dependency & risk notes
267
+
268
+ Being upfront about what mnci2 leans on, so it's a conscious trade-off rather
269
+ than a surprise:
270
+
271
+ - **One unofficial, small-team third-party Nx plugin carries real weight**:
272
+ [`oxc-standard`](https://github.com/JohnDeved/ox-standard) (the oxlint/oxfmt
273
+ StandardJS preset) — not `@nx`-scoped/officially maintained. Neither Azure
274
+ Function generation nor any Python kind pulls in a *third-party* Nx plugin:
275
+ Node apps use the **official** `@nx/node:application` plus a small
276
+ hand-written Azure Functions v4 file overlay (see "How Node apps work"
277
+ below), and Python uses **`@mnci/nx-python-pip`** — a real Nx plugin this
278
+ project built and maintains itself (`libs/nx-python-pip` in this same
279
+ monorepo), after `@nxlv/python` (the previous Python plugin) turned out to
280
+ require `uv`, which the company standardizing on this tool does not use,
281
+ and no maintained alternative supports pip. That trades third-party risk
282
+ for a different, real one: **this project now owns a second package's
283
+ maintenance surface** (generators, executors, its own release cycle) —
284
+ worth being explicit about, since it did not exist before this migration.
285
+ If `oxc-standard` stalls or breaks compatibility with a future Nx major,
286
+ that surface needs a real maintenance response, not just a version bump.
287
+ - **The TS7 dual-compiler aliases pin a very new, fast-moving dependency.**
288
+ TypeScript 7's native compiler is recent; `TS_COMPILER_DEPENDENCIES` pins
289
+ `npm:typescript@^7.0.2` / `npm:@typescript/typescript6@^6.0.2` specifically
290
+ because the alias trick is what makes it work at all today (see "Stack"
291
+ above). A semver-compatible upstream release could still change behavior or
292
+ break the alias before the rest of the ecosystem (Nx, typescript-eslint)
293
+ catches up — worth a periodic re-check, not a "set and forget."
294
+
295
+ ## Known gaps (accepted for the experiment)
296
+
297
+ - No `doctor`/`resurrect`/`spell` — out of scope until the model is proven.
298
+ - Azure Functions Core Tools is only needed for **local** `func start` — never
299
+ for `mnci2 add node-function-app`/`python-function-app` generation, since
300
+ neither shells out to the `func` CLI.
301
+ - Function-app *deployment* (e.g. `AzureFunctionApp@2`) is not wired into the
302
+ pipeline; the `node-function-app-<name>.zip`/`python-function-app-<name>.zip`
303
+ inside the published `drop` artifact is the deploy input. Deploying it means
304
+ Azure's Oryx build installing real dependencies (`npm install`/`pip install`)
305
+ from the zipped manifest — no `node_modules`/venv is bundled.
306
+ - Changelog files are off (unpushable under the tag-only model); the git tag
307
+ history is the changelog for now.
308
+ - **No lock file for Python** — plain pip has none, matching the company
309
+ standard this migration was for. A published wheel's `Requires-Dist` mirrors
310
+ whatever specifier the `pyproject.toml` declares (e.g. `tomli>=2.0.0`)
311
+ verbatim, not a resolved/pinned version the way `uv.lock` would have
312
+ produced. `requirements-dev.txt` (the fixed `ruff`/`pytest`/`build`/`twine`
313
+ toolchain) is unpinned for the same reason — pin it by hand if the
314
+ workspace needs reproducible CI tool versions.
315
+ - venv management is left to the user (same spirit as never managing
316
+ `node_modules` beyond `npm install`): `mnci2` neither creates nor activates
317
+ one. CI installs `requirements-dev.txt` straight into whatever `python3`
318
+ resolves to on the agent; locally, create your own with `python3 -m venv`.
319
+
320
+ ## How Node apps work (plain `@nx/node:application`, no Azure Functions plugin)
321
+
322
+ `node-app` and `node-function-app` are both the **official**
323
+ `@nx/node:application` generator (`--bundler=esbuild --framework=none`) — no
324
+ third-party Azure Functions plugin, and no post-generation build-output
325
+ rewiring. `node-function-app` is exactly that generator plus a hand-written
326
+ Azure Functions v4 file overlay, the same split `python-app`/
327
+ `python-function-app` already use:
328
+
329
+ - `build` = the generator's own `@nx/esbuild:esbuild` target, **non-bundled**
330
+ (`bundle: false`): it transpiles each file individually and mirrors the
331
+ workspace tree into `apps/<name>/dist` (e.g.
332
+ `apps/<name>/dist/apps/<name>/src/main.js`), plus a `dist/main.js` shim that
333
+ `require`s the compiled entry — verified empirically, and the one thing that
334
+ makes `main.js` a stable, generator-provided deploy entry point regardless
335
+ of the nested path. A private internal lib is compiled by its own `tsc`
336
+ build and copied into `dist` at its own path; a real npm dependency stays a
337
+ real `require`, resolved from `node_modules`.
338
+ - `test`/`lint` = the generator's own targets (`--unitTestRunner`/`--linter`
339
+ passed straight through, same as every other kind) — unlike the old
340
+ function app, nothing needs hand-wiring here.
341
+ - `package` (added by `mnci2 add`, not the generator) zips `apps/<name>/dist`
342
+ into `dist/drop/node-app-<name>.zip` (`node-app`) — for `node-function-app`
343
+ it additionally zips in `host.json` and the repaired `package.json` into
344
+ `dist/drop/node-function-app-<name>.zip`. No `node_modules` is bundled
345
+ either way: for the function app, Azure's Oryx build installs real
346
+ dependencies from the zipped `package.json` at deploy time — the exact same
347
+ model `python-function-app` already relies on for `requirements.txt`
348
+ (verified empirically: a plain `npm install` in a simulated deploy folder,
349
+ with no bundled `node_modules`, resolves and runs correctly once the
350
+ dependency is declared).
351
+ - **`node-function-app` overlay**: `@azure/functions` is installed for real
352
+ (a plain `@nx/node:application` app has no Azure dependency by default,
353
+ unlike a plugin-generated one), an HTTP-triggered `app.http(...)` sample
354
+ (v4 programming model) is written under `src/functions/`, `host.json` is
355
+ added, and the manifest is repaired — `main: 'main.js'` (the dist shim) and
356
+ `@azure/functions` added to `dependencies` for Azure's deploy-time install
357
+ to find.
358
+ - **Convention** (both kinds): `src/main.ts` is the esbuild entry — add one
359
+ import per function file you create under `src/functions/`, or it won't be
360
+ reachable (and thus won't be transpiled into `dist`).
361
+
362
+ ## How React apps work (one build per environment)
363
+
364
+ A React SPA bakes its config in at **build time** (`import.meta.env.VITE_*`),
365
+ so it needs a separate build per environment. `add react-app` wires that up
366
+ with Vite's own **modes**:
367
+
368
+ - Scaffolds `.env.dev`, `.env.uat`, `.env.prod` — put each environment's public
369
+ `VITE_*` config there (these values ship in the browser bundle, so they are
370
+ public by definition; real secrets never belong here). The files are
371
+ committed (an allow-rule keeps them out of `.gitignore`).
372
+ - Adds `build-dev` / `build-uat` / `build-prod` targets, each
373
+ `vite build --mode <env> --outDir dist-<env>`, so every environment gets its
374
+ own compiled-in config. The default inferred `build` (single build) stays
375
+ for local dev and the CI verify step.
376
+ - `package` builds all three and zips each into
377
+ `dist/drop/react-app-<name>-<env>.zip` — **one artifact per environment**.
378
+
379
+ CI needs no change: the per-app tag step derives one build tag per zip, so you
380
+ get `react-app-<name>-dev` / `-uat` / `-prod`, and the classic release pipeline
381
+ deploys each environment from its own artifact + tag. Need different
382
+ environments? Edit `REACT_ENVIRONMENTS` in the generator.
383
+
384
+ ## Python (`@mnci/nx-python-pip` — pip + Ruff + pytest + PyPA `build`/`twine`, no uv)
385
+
386
+ Python is the first non-JS language, and follows the same philosophy as every
387
+ other kind — pure delegation to a real Nx plugin generator — except the
388
+ plugin is one this project built and maintains itself:
389
+ [`@mnci/nx-python-pip`](../nx-python-pip) (`libs/nx-python-pip` in this same
390
+ monorepo). No maintained, Nx-23-compatible Python plugin supports pip
391
+ (verified empirically: the previous plugin, `@nxlv/python`, ships only uv and
392
+ Poetry providers; every alternative found on npm is either the same
393
+ uv/Poetry architecture or years stale), so rather than keep hand-authoring
394
+ Python projects forever inside `add/python.ts` (the position this repo was in
395
+ right after dropping `@nxlv/python`), the generation logic was extracted into
396
+ a proper, independently testable, independently publishable Nx plugin —
397
+ `add/python.ts` now just calls `nx g @mnci/nx-python-pip:<kind>`, the same
398
+ shape as `react-app`/`node-app`/`npm-lib`.
399
+
400
+ `@mnci/nx-python-pip` ships real `@nx/devkit` generators (`application`,
401
+ `library`, `internal-library`, `function-application`) and real TypeScript
402
+ executors (`build`, `test`, `lint`, `publish`) — not `nx:run-commands`
403
+ wrappers — so `nx-release-publish`'s `dryRun` arrives as a genuine typed
404
+ executor option (`nx release publish --dry-run` sets it automatically for
405
+ every custom executor, no argv-parsing trick needed), and internal-lib
406
+ vendoring resolves a dependency's location via the real Nx **project graph**,
407
+ not a hard-coded `libs/<name>` path. `mnci2 add python-*` installs it like any
408
+ other npm devDependency (`npm install --save-dev @mnci/nx-python-pip` —
409
+ no `nx.json` `plugins` registration needed, since its generators/executors
410
+ are explicit, not inference-based) and writes exactly one file itself:
411
+ `requirements-dev.txt` at the workspace root (the fixed `ruff`/`pytest`/
412
+ `build`/`twine` toolchain — install with `python3 -m pip install -r
413
+ requirements-dev.txt`), since the plugin is a generic Nx plugin with no
414
+ opinion on how its own runtime dependencies land on a machine. There is **no
415
+ stack question** — Ruff (lint + format) and pytest are the standard, so they
416
+ are always used, invoked as `python3 -m <tool>` everywhere (not a
417
+ hard-coded venv path), so the exact same command works whether or not a venv
418
+ is activated.
419
+
420
+ | Kind | Location | Build / deploy |
421
+ | ---- | -------- | -------------- |
422
+ | `python-app` | `apps/<name>` | `python -m build` wheel (the plugin's `build` executor), zipped by mnci2 into `dist/drop/python-app-<name>.zip` |
423
+ | `python-function-app` | `apps/<name>` | Azure Functions **v2** (`function_app.py` + `host.json` + `requirements.txt`); no `pyproject.toml`/wheel — the **source** is zipped by mnci2 into `dist/drop/python-function-app-<name>.zip` (no `func` CLI needed to generate) |
424
+ | `python-lib` | `python-packages/<name>` | publishable wheel; the plugin's `publish` executor (`twine upload --skip-existing`) |
425
+ | `python-internal-lib` | `libs/<name>` | private shared code, lint + test only — no build/package target of its own |
426
+
427
+ - **Apps** get a `package` target — mnci2's own CI packaging convention, not
428
+ a generic plugin concern — merged into the plugin-written `project.json`
429
+ after generation, fitting the existing CI unchanged: the pipeline's
430
+ `apps/*` pack step tags them `python-app-<name>` / `python-function-app-
431
+ <name>` just like the TS apps.
432
+ - **Internal-lib vendoring** replaces `@nxlv/python`'s `bundleLocalDependencies`:
433
+ plain pip has no bundled-local-dependency feature, so a project that imports
434
+ a workspace-internal Python library needs a hand-added `vendor` entry (under
435
+ `[tool.mnci-python-pip]`) in its own `pyproject.toml` (the pip-world
436
+ counterpart of hand-wiring a `dependencies = [...]` entry — neither mnci2
437
+ nor the plugin wires cross-project Python dependencies automatically). The
438
+ plugin's `build` executor reads that entry, resolves the named project's
439
+ root via the **Nx project graph**, copies its module into a staged copy of
440
+ the consuming project, and builds from there — so the resulting wheel
441
+ contains the vendored module as a real top-level package. Verified
442
+ empirically that this does **not** reproduce the old `@nxlv/python` bug
443
+ where combining a vendored internal lib and a real external dependency on
444
+ the same project silently dropped the external one from the wheel's
445
+ metadata — both survive correctly.
446
+ - **Release** is unified with npm: `nx release` scopes both `packages/*` and
447
+ `python-packages/*` in one flat project list (deliberately not two named
448
+ `release.groups` — Nx hard-errors the whole release when an explicit group
449
+ matches zero projects, a real failure mode for a Python-only or npm-only
450
+ workspace, verified empirically), so a Python package is **versioned from
451
+ conventional commits and tagged** `{projectName}@{version}` exactly like an
452
+ npm one — its `pyproject.toml` version bumps, tag-only (never a commit).
453
+ The plugin's `library` generator sets a project-level
454
+ `release.version.versionActions` override pointing at
455
+ `@mnci/nx-python-pip/release/version-actions` (a `VersionActions`
456
+ implementation — six methods, verified empirically against a real `nx
457
+ release version --dry-run`), which wins over the workspace's default
458
+ (npm's) `versionActions` for that one project. **Publishing** reuses the
459
+ registry: an Azure Artifacts feed is **multi-protocol**, so the same
460
+ org/project/feed serves Python — the release step exports `TWINE_*` (URL +
461
+ the base64 `PAT` decoded to the raw token twine needs, no second secret) and
462
+ `nx release` publishes the wheels with `twine`. (On a public-npm workspace a
463
+ Python package is still versioned + tagged, but publishing it needs
464
+ user-provided `TWINE_*` — e.g. a PyPI token.)
465
+ - **CI** also runs `nx run-many -t lint,test,build`, so Python's ruff `lint`
466
+ target runs alongside the JS build even on the oxlint stack (whose
467
+ `npm run lint` only covers JS). A guarded pipeline step installs
468
+ `requirements-dev.txt` first, skipped cleanly when the workspace has no
469
+ Python projects.