@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 +469 -0
- package/dist/cli.js +1173 -0
- package/dist/cli.js.map +1 -0
- package/package.json +63 -0
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.
|