@zukhruf/nx 0.0.0-stage → 0.3.58
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/LICENSE +21 -0
- package/README.md +345 -2
- package/dist/format/plugin.d.ts +12 -0
- package/dist/format/plugin.d.ts.map +1 -0
- package/dist/format/plugin.js +73 -0
- package/dist/format/plugin.js.map +1 -0
- package/dist/test/plugin.d.ts +14 -0
- package/dist/test/plugin.d.ts.map +1 -0
- package/dist/test/plugin.js +151 -0
- package/dist/test/plugin.js.map +1 -0
- package/package.json +58 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ezzabuzaid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,346 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zukhruf/nx
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This package is the Nx part of the monorepo standard. Each workspace copied the same Nx configuration into each project. This package gives that configuration one time, in areas. Each area has its own import path, and the package has no root entry point. Lint stays in `@zukhruf/eslint`, and the Git hooks stay in `@zukhruf/git-hooks`.
|
|
4
|
+
|
|
5
|
+
The words in these documents have one meaning each. See the glossary in [CONTEXT.md](./CONTEXT.md).
|
|
6
|
+
|
|
7
|
+
## Areas
|
|
8
|
+
|
|
9
|
+
| Area | Import path | Status |
|
|
10
|
+
| ---------------- | ---------------------- | --------------------------------------------------------------------------------------------- |
|
|
11
|
+
| format | `@zukhruf/nx/format` | This README |
|
|
12
|
+
| test | `@zukhruf/nx/test` | This README |
|
|
13
|
+
| portless | `@zukhruf/nx/portless` | Candidate (#3336): 14 projects in 3 repos |
|
|
14
|
+
| opentofu, prisma | — | Candidates (#3339): 5 places each |
|
|
15
|
+
| generators | — | Candidates: the workspace generator of `@factory/foundation` (#3337); app kinds later (#3338) |
|
|
16
|
+
|
|
17
|
+
The lint inputs (7 copies) are #3335; their home (`@zukhruf/eslint` or an area here) is still open.
|
|
18
|
+
|
|
19
|
+
## format
|
|
20
|
+
|
|
21
|
+
An Nx workspace formats its files with Prettier. The `format` area is an Nx inference plugin. It gives each project of the workspace two targets: `format`, which writes the files, and `format:check`, which only reads them. You list the plugin one time in `nx.json`. You do not copy targets into each project.
|
|
22
|
+
|
|
23
|
+
## Use it
|
|
24
|
+
|
|
25
|
+
You need Nx 23 or 24 and Prettier 3. `nx`, `@nx/devkit` and `prettier` are peer dependencies.
|
|
26
|
+
|
|
27
|
+
Add the plugin to `plugins` in `nx.json`. Then add `format:check` to the `dependsOn` of `typecheck` that the workspace already has:
|
|
28
|
+
|
|
29
|
+
```jsonc
|
|
30
|
+
{
|
|
31
|
+
"plugins": ["@zukhruf/nx/format"],
|
|
32
|
+
"targetDefaults": {
|
|
33
|
+
"typecheck": {
|
|
34
|
+
"dependsOn": ["lint", "format:check", "..."],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Keep the entries that are already in the list. A `dependsOn` array replaces the array of an earlier source, and only `"..."` keeps the dependencies that `@nx/js/typescript` gives `typecheck`.
|
|
41
|
+
|
|
42
|
+
zukhruf builds this package, so it lists the plugin by path: `"plugins": ["./packages/nx/src/format/plugin.ts"]`. The package name resolves to `dist/`, and a fresh clone has no build before Nx makes the project graph.
|
|
43
|
+
|
|
44
|
+
The plugin does not change `typecheck`. That target belongs to `@nx/js/typescript`, and each workspace adds its own dependencies to it, for example `^sdk`. See [ADR 0001](./docs/adr/0001-inferred-targets-not-copied-defaults.md).
|
|
45
|
+
|
|
46
|
+
## Signature
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// @zukhruf/nx/format
|
|
50
|
+
export const createNodes: CreateNodes;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The plugin takes no options. The target names and the commands are the same in each of the 7 copies that this package replaces, so no option is necessary.
|
|
54
|
+
|
|
55
|
+
## What each target runs
|
|
56
|
+
|
|
57
|
+
| Target | Command | Writes? | Cached | Inputs |
|
|
58
|
+
| -------------- | -------------------------------- | ------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
59
|
+
| `format` | `prettier --write {projectRoot}` | yes | no | — |
|
|
60
|
+
| `format:check` | `prettier --check {projectRoot}` | no | yes | the files of the project; the root `.prettierignore` and `.editorconfig`; the config file that Prettier finds from the workspace root; `prettier` and each Prettier plugin that the config names (`externalDependencies`) |
|
|
61
|
+
|
|
62
|
+
- `format` writes. It runs only when you run it on purpose: `nx run <project>:format`. No target depends on it.
|
|
63
|
+
- `format:check` only reads. It fails when a file is not formatted.
|
|
64
|
+
- A project with no file that Prettier can format passes `format:check`. Prettier 3.9.9 passes a folder whose files are all ignored or of an unknown type. It stops with an error only for a folder with no file, and a project root always has a `project.json` or a `package.json`. Thus the command has no `--no-error-on-unmatched-pattern`.
|
|
65
|
+
- The plugin finds the config file with `resolveConfigFile` of Prettier, as the Prettier CLI does. It can be `.prettierrc`, `prettier.config.mjs` or the `prettier` key of the root `package.json`. That file is an input, so a change to it runs the check again. The Prettier CLI also reads `.editorconfig`, so that file is an input too. When Prettier finds no config file, no config file is an input.
|
|
66
|
+
|
|
67
|
+
To format only your own files, give their paths: `npx prettier --write <files>`. See [ADR 0002](./docs/adr/0002-a-check-never-writes.md).
|
|
68
|
+
|
|
69
|
+
## Override a project
|
|
70
|
+
|
|
71
|
+
A target in the `project.json` of a project replaces the inferred target. Nx applies the plugin first, then `targetDefaults`, then `project.json`. For example, the `infra` project of whatisgoingon formats OpenTofu files:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"targets": {
|
|
76
|
+
"format": { "command": "tofu fmt -recursive" },
|
|
77
|
+
"format:check": { "command": "tofu fmt -check -recursive" }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Leave fixtures out
|
|
83
|
+
|
|
84
|
+
The plugin gives targets only to the projects that Nx finds, by the rule and the matcher of Nx itself:
|
|
85
|
+
|
|
86
|
+
- Each folder with a `project.json` is a project.
|
|
87
|
+
- A folder with a `package.json` is a project only when the workspace globs of the package manager match its path, or when it has a `project.json` too. The globs are `workspaces` in the root `package.json`, `pnpm-workspace.yaml` and `lerna.json`, with their `!` negations.
|
|
88
|
+
- The workspace root is a project only when it has a `project.json`, or its `package.json` has an `nx` key.
|
|
89
|
+
|
|
90
|
+
Thus a fixture outside the workspace globs gets no targets, and you do not add it to `exclude`. The root files stay checked by the `Format` step of `@zukhruf/git-hooks` and by `npx prettier --check .` in CI.
|
|
91
|
+
|
|
92
|
+
A fixture inside the workspace globs is a project of Nx. Use the standard `exclude` of an Nx plugin entry to give it no format targets:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"plugins": [
|
|
97
|
+
{
|
|
98
|
+
"plugin": "@zukhruf/nx/format",
|
|
99
|
+
"exclude": ["packages/connectors/manifest/src/fixtures/**"]
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
context-compiler already does this for `@nx/eslint/plugin`. See [ADR 0003](./docs/adr/0003-which-roots-get-targets.md).
|
|
106
|
+
|
|
107
|
+
## Migrate a workspace
|
|
108
|
+
|
|
109
|
+
1. Add the plugin to `plugins` in `nx.json`.
|
|
110
|
+
2. Delete the `format` and `format:check` entries from `targetDefaults`.
|
|
111
|
+
3. Delete each opt-in: each `"format": {}` and each copied `format` or `format:check` target in a `project.json` or in `nx.targets` of a `package.json`. Keep a target only when it is an override.
|
|
112
|
+
4. Delete `"format"` from `lint.dependsOn` and from `typecheck.dependsOn`.
|
|
113
|
+
5. Add `"format:check"` to `typecheck.dependsOn`.
|
|
114
|
+
|
|
115
|
+
| Workspace | Today | Opt-ins to delete |
|
|
116
|
+
| ---------------- | -------------------------------------------------------------------------------------------- | ---------------------------------- |
|
|
117
|
+
| zukhruf | targetDefault `format` = `--write`; `lint` depends on `format` | 32 `"format": {}` |
|
|
118
|
+
| elt | same as zukhruf | 69 |
|
|
119
|
+
| context-compiler | same as zukhruf | 27 |
|
|
120
|
+
| text2sql | a full `format` target in each project; `typecheck` depends on `format` | 75 |
|
|
121
|
+
| factory | targetDefaults `format` and `format:check`; `typecheck` depends on `lint` and `format:check` | 9 `nx.targets` in `package.json` |
|
|
122
|
+
| whatisgoingon | same as factory; `infra` overrides `format` with `tofu fmt` | 3, and `infra` adds `format:check` |
|
|
123
|
+
| deepagents | one stray `format` target in `packages/compaction` | 1 |
|
|
124
|
+
|
|
125
|
+
## Who uses it
|
|
126
|
+
|
|
127
|
+
| Workspace | Backlog item |
|
|
128
|
+
| ---------------- | ------------------------------- |
|
|
129
|
+
| zukhruf | #3306 (phase 2, first consumer) |
|
|
130
|
+
| elt | #3318 |
|
|
131
|
+
| context-compiler | #3319 |
|
|
132
|
+
| factory | #3320 |
|
|
133
|
+
| whatisgoingon | #3321 |
|
|
134
|
+
| text2sql | #3322 |
|
|
135
|
+
| deepagents | #3323 |
|
|
136
|
+
|
|
137
|
+
The umbrella item is #3317.
|
|
138
|
+
|
|
139
|
+
## test
|
|
140
|
+
|
|
141
|
+
> Status: shipped (#3328).
|
|
142
|
+
|
|
143
|
+
Seven workspaces run their tests with `node --test` through Nx. Each one copies the `test` target, and the flags drifted. The `test` area is an Nx inference plugin. It gives the `test` target to each project that has `node:test` files. You list the plugin one time in `nx.json`. You do not copy the target, and you do not tag the project.
|
|
144
|
+
|
|
145
|
+
### Use the test area
|
|
146
|
+
|
|
147
|
+
You need Nx 23.2.1 or later. The peer range of `nx` and `@nx/devkit` becomes `>=23.2.1 <25`. See [Prerequisites of the test area](#prerequisites-of-the-test-area).
|
|
148
|
+
|
|
149
|
+
Add the plugin to `plugins` in `nx.json`. The options are optional. They apply to each project of the workspace:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"plugins": [
|
|
154
|
+
"@zukhruf/nx/format",
|
|
155
|
+
{
|
|
156
|
+
"plugin": "@zukhruf/nx/test",
|
|
157
|
+
"options": { "timeout": 60000, "flags": [] }
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
zukhruf builds this package, so it lists the plugin by path, as it does for `format`: `"./packages/nx/src/test/plugin.ts"`.
|
|
164
|
+
|
|
165
|
+
### Signature of the test area
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// @zukhruf/nx/test
|
|
169
|
+
export const createNodes: CreateNodes<{
|
|
170
|
+
timeout?: number;
|
|
171
|
+
flags?: string[];
|
|
172
|
+
}>;
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Unlike `format`, this plugin takes options. The repos use different flags, so each difference is a parameter:
|
|
176
|
+
|
|
177
|
+
| Option | Default | Effect |
|
|
178
|
+
| --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
179
|
+
| `timeout` | `60000` | the value of `--test-timeout`, in milliseconds |
|
|
180
|
+
| `flags` | `[]` | more Node flags for each project of the workspace, for example `["--test-force-exit"]` or `["--experimental-test-module-mocks"]` |
|
|
181
|
+
|
|
182
|
+
### Which projects get the test target
|
|
183
|
+
|
|
184
|
+
The plugin finds the test files. It does not read tags or imports. A project root gets the `test` target when all of these are true:
|
|
185
|
+
|
|
186
|
+
- The root owns at least one test file: `*.test.ts`, `*.test.mts`, `*.test.cts`, `*.test.js` or `*.test.mjs`. A file belongs to its nearest project root.
|
|
187
|
+
- The root has no `vitest.config.*`.
|
|
188
|
+
- The root has no `vite.config.*` with a `test` key. `@nx/vitest` uses the same condition (`@nx/vitest/dist/src/plugins/plugin.js:132`).
|
|
189
|
+
- The root is not the workspace root `.`.
|
|
190
|
+
|
|
191
|
+
The project roots are the same as for `format` (see [ADR 0003](./docs/adr/0003-which-roots-get-targets.md)). The plugin adds `**/*.test.{ts,mts,cts,js,mjs}` and `**/{vite,vitest}.config.{js,ts,mjs,mts,cjs,cts}` to the files that it reads. Thus Nx runs the plugin again when a test file or a Vite config changes. The plugin loads Vite from the workspace only for a root with a `vite.config.*`. Nx does not give the plugin the paths in `.gitignore`, so the plugin does not see `dist`.
|
|
192
|
+
|
|
193
|
+
A project with Vitest files and `node:test` files gets no `test` target from this plugin. `@nx/vitest` gives it its target, as today.
|
|
194
|
+
|
|
195
|
+
The 2026-10-29 audit of the 7 repos measured the effect. The rule matches each `node:test` target in use. 11 targets that test nothing go: in context-compiler the connectors `call-history`, `contacts`, `messages`, `slack` and `teams`; in text2sql `backend/inputs`, `harness/runs`, `toolkit/dashboards`, `reports`, `rpc` and `threads`. In text2sql, `.agents` and `.claude` get a target, because they own test files. The `test:node` tag is not necessary. A repo can delete it. See [ADR 0004](./docs/adr/0004-node-test-target-from-test-files.md).
|
|
196
|
+
|
|
197
|
+
### What the test target runs
|
|
198
|
+
|
|
199
|
+
| Part | Value |
|
|
200
|
+
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
201
|
+
| Command | `node --test --no-warnings --test-timeout=<timeout> <flags> --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=test-results.xml {args}` |
|
|
202
|
+
| `cwd` | `{projectRoot}` |
|
|
203
|
+
| `args` | the test selection (see below) |
|
|
204
|
+
| `outputs` | `["{projectRoot}/test-results.xml"]` |
|
|
205
|
+
| `cache` | `false` |
|
|
206
|
+
| `dependsOn` | `["^build"]` |
|
|
207
|
+
|
|
208
|
+
- `--no-warnings`: 5 of the 7 repos use it.
|
|
209
|
+
- `--test-timeout`: zukhruf and deepagents use 60 s, so the default is 60 s. The other repos have no time limit, and that is drift.
|
|
210
|
+
- Two reporters: `spec` to the terminal and `junit` to `test-results.xml`. zukhruf, deepagents and text2sql use both.
|
|
211
|
+
- `^build`: a test imports the built output of the packages that the project depends on. factory, whatisgoingon and text2sql test from the source, so `^build` is sufficient for them. A repo whose tests also need the project's own `build` adds it (see [Prerequisites of the test area](#prerequisites-of-the-test-area)).
|
|
212
|
+
- No cache: no repo caches a `node:test` run today.
|
|
213
|
+
|
|
214
|
+
The base command has no `--test-force-exit` and no `--experimental-test-module-mocks`. Only Docker tests and tests with open handles need `--test-force-exit`. Only deepagents and the text2sql electron app use module mocks. A repo adds these flags with the `flags` option. One project adds them in its own `args` (see [Override the test target of a project](#override-the-test-target-of-a-project)).
|
|
215
|
+
|
|
216
|
+
The test selection is a list of globs:
|
|
217
|
+
|
|
218
|
+
- one glob for each top-level folder that has test files, for example `"src/**/*.test.ts" "eval/**/*.test.ts"`,
|
|
219
|
+
- one glob with no `**` for the test files directly in the project root, for example `"*.test.ts"`, only when such files exist.
|
|
220
|
+
|
|
221
|
+
Thus a file outside these globs does not run, for example a build output or a `test/helpers/*` file with no test file next to it. Node's own default pattern, `**/{test,test/**/*,test-*,*[._-]test}.{js,mjs,cjs,ts,mts,cts}`, matches too much: `dist/spec/test/*`, `test/helpers/*` and `*.e2e.test.ts`.
|
|
222
|
+
|
|
223
|
+
### Run one file or one test
|
|
224
|
+
|
|
225
|
+
`--args` replaces the test selection. Use it to run one file:
|
|
226
|
+
|
|
227
|
+
```sh
|
|
228
|
+
nx run mutex:test --args="src/mutex/mutex.test.ts"
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
A Node flag on the CLI keeps the test selection. Use it to run the tests with a name:
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
nx run mutex:test --test-name-pattern=login
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The command ends with `{args}`. For a command with `{args}`, Nx puts the CLI flags and the positional words first, then `args` (`nx/dist/src/executors/run-commands/run-commands.impl.js:136-142`). Thus `--test-name-pattern=login` comes before the globs, and Node reads it as a flag.
|
|
238
|
+
|
|
239
|
+
Without `{args}`, the order is different. Nx puts a CLI flag after `args` (`run-commands.impl.js:117-119` and `145-162`): only an unknown option that is not on the CLI comes before `args`. Then `nx run mutex:test --test-name-pattern=login` puts the flag after the globs, and Node reads it as a file name.
|
|
240
|
+
|
|
241
|
+
A positional file does not replace the selection. In `nx run mutex:test src/mutex/mutex.test.ts`, Nx adds the file before the selection, and all the tests run. Use `--args`.
|
|
242
|
+
|
|
243
|
+
A probe on Nx 23.2.1 (2026-10-29) showed this, with `args` `DEFAULT` and a command that prints its arguments:
|
|
244
|
+
|
|
245
|
+
| CLI | With `{args}` | Without `{args}` |
|
|
246
|
+
| ------------------------------------ | ----------------------------------- | ----------------------------------- |
|
|
247
|
+
| `--args=X` | `X` | `X` |
|
|
248
|
+
| `--test-name-pattern=login` | `--test-name-pattern=login DEFAULT` | `DEFAULT --test-name-pattern=login` |
|
|
249
|
+
| `pos.test.ts` | `pos.test.ts DEFAULT` | `DEFAULT pos.test.ts` |
|
|
250
|
+
| `--args=X --test-name-pattern=login` | `--test-name-pattern=login X` | — |
|
|
251
|
+
|
|
252
|
+
The selection is in `args` and not in the command, because a flag after a glob does not work. `node --test "x.test.ts" --test-timeout=1` reads the flag as a file name. It stops with exit code 1: `Could not find 'x.test.ts, --test-timeout=1'`. zukhruf has `"src/**/*.test.ts"` at the end of its command today, so a zukhruf run cannot narrow. Also, a glob that matches no file gives exit code 0. Thus a wrong narrowing passes with no test.
|
|
253
|
+
|
|
254
|
+
### Docker tests
|
|
255
|
+
|
|
256
|
+
A project with the tag `test:docker` starts Docker containers in its tests. zukhruf and deepagents already use this tag. The plugin adds three items to the `test` target of such a project:
|
|
257
|
+
|
|
258
|
+
- `--test-global-setup=<path>`. The global setup of `@zukhruf/testing` makes one run of all the test processes, and it removes what a killed run leaves.
|
|
259
|
+
- `--test-force-exit`. The teardown of the global setup is synchronous on purpose, so it runs also under `--test-force-exit` (`packages/testing/src/scripts/docker-global-setup.ts:99-104`).
|
|
260
|
+
- `cache: false`. The result depends on the engine, not only on the files.
|
|
261
|
+
|
|
262
|
+
`cache: false` is the default, not a guarantee. The base target has no cache too. `targetDefaults` and the project files come after the plugin, so a repo can set `cache: true` on both.
|
|
263
|
+
|
|
264
|
+
The plugin selects the path in this order:
|
|
265
|
+
|
|
266
|
+
1. The project's own `src/testing/docker-global-setup.ts`, if it exists. mutex has one: it starts the shared servers before the run. `@zukhruf/testing` has one: its tests use its own source.
|
|
267
|
+
2. Else, `@zukhruf/testing/docker-global-setup`, resolved from the project root with the default conditions. This gives `dist/scripts/docker-global-setup.js`. The plugin writes the path relative to the `cwd` of the target.
|
|
268
|
+
3. If the plugin cannot resolve the package, it stops with an error. It does not run the tests with no global setup.
|
|
269
|
+
|
|
270
|
+
The value must be a path. Node resolves it with `path.resolve(cwd, value)`, so a package name does not work. That is why 5 repos keep a file that only re-exports the global setup. Each repo deletes this file when it migrates. The global setup runs one time for each `node --test` process.
|
|
271
|
+
|
|
272
|
+
The plugin does not select the Docker engine. It never sets or deletes `DOCKER_CONTEXT` or `DOCKER_HOST`, and it never calls the Docker CLI. The user, CI or the repo selects the engine. See [ADR 0005](./docs/adr/0005-docker-tests-get-the-global-setup.md).
|
|
273
|
+
|
|
274
|
+
On an SSH engine, a repo adds `--test-concurrency=1` with `flags`, or `"parallelism": false` in `targetDefaults`. The plugin does not add them.
|
|
275
|
+
|
|
276
|
+
### Override the test target of a project
|
|
277
|
+
|
|
278
|
+
Nx applies the plugin first, then `targetDefaults`, then `project.json` and `package.json`. A later source wins.
|
|
279
|
+
|
|
280
|
+
A flag for each project of the workspace goes into the `flags` option. A flag for one project goes into the `args` of that project, before its own selection. `args` comes after the command, and a flag must come before a glob. For example, the text2sql backends have a time limit of 120 s:
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"targets": {
|
|
285
|
+
"test": {
|
|
286
|
+
"options": { "args": "--test-timeout=120000 \"src/**/*.test.ts\"" }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Node uses the last `--test-timeout`, so the value in `args` wins. A probe on Node 26.10.0 showed this on 2026-10-29: `--test-timeout=50 --test-timeout=60000` passed a test of 300 ms, and the reverse order failed it.
|
|
293
|
+
|
|
294
|
+
text2sql leaves the integration tests out of `test` in the same way: `"args": "\"src/**/!(*.integration).test.ts\""`. Other flags for one project: a time limit of 10 s, `--experimental-test-module-mocks`, `--test-force-exit`, `--env-file`.
|
|
295
|
+
|
|
296
|
+
Do not override the `command`. A command in a project file is a new copy of the target.
|
|
297
|
+
|
|
298
|
+
These rules stay in their repo, because they are of a different kind:
|
|
299
|
+
|
|
300
|
+
- the script of deepagents that selects the Docker context,
|
|
301
|
+
- the tests of elt and context-compiler that run from the workspace root,
|
|
302
|
+
- the list of e2e files of the context-compiler desktop app.
|
|
303
|
+
|
|
304
|
+
A `scripts.test` in a `package.json` replaces the inferred target. Nx runs it with `nx:run-script`. The 4 demo projects of deepagents keep their scripts.
|
|
305
|
+
|
|
306
|
+
### Prerequisites of the test area
|
|
307
|
+
|
|
308
|
+
- Nx 23.2.1 or later. In Nx 23.2.1, a target default with its own `command` replaces the command of a plugin (`nx/dist/src/project-graph/utils/project-configuration/target-defaults.js:201-229`). Nx 23.1.0 does not do this. text2sql uses 23.1.0 today.
|
|
309
|
+
- Delete the `command` of `targetDefaults.test` in `nx.json`. If you keep it, it replaces the command of the plugin.
|
|
310
|
+
- Write each extra dependency with `"..."`, so that `^build` stays. For example, factory writes `["...", "^sdk"]`.
|
|
311
|
+
- If the tests of a project use its own built output, add `build`: `targetDefaults.test.dependsOn: ["...", "build"]`. zukhruf, deepagents, elt and context-compiler need this. For example, the tests of `@zukhruf/nx` resolve `@zukhruf/nx/format` to `dist`. Nx skips a dependency on a target that the project does not have, so this is safe for a project with no `build`.
|
|
312
|
+
|
|
313
|
+
### Migrate a workspace to the test area
|
|
314
|
+
|
|
315
|
+
1. Update `nx` and `@nx/devkit` to 23.2.1 or later.
|
|
316
|
+
2. Add `@zukhruf/nx/test` to `plugins` in `nx.json`. Set `timeout` and `flags` if the repo needs other values.
|
|
317
|
+
3. Delete the `command` of `targetDefaults.test`, and its tag filters. Write each extra dependency with `"..."`.
|
|
318
|
+
4. Delete each `test` target in a `project.json` or `package.json` that the plugin now gives. Move the flags of one project into its `args`.
|
|
319
|
+
5. Add the tag `test:docker` to each project with Docker tests.
|
|
320
|
+
6. Delete the file that only re-exports `@zukhruf/testing/docker-global-setup`. Keep a file that does more, for example the mutex file.
|
|
321
|
+
7. Delete the `test:node` tags.
|
|
322
|
+
8. Compare `nx show projects --withTarget test` before and after. The differences must be the ones in the table.
|
|
323
|
+
|
|
324
|
+
| Workspace | Today | Changes |
|
|
325
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
326
|
+
| zukhruf | `targetDefaults.test` with filters `tag:test:node` and `tag:test:docker`; `dependsOn` `["build", "^build"]`; `--test-force-exit` for each project | 30 tagged projects; 2 Docker projects (mutex, testing) keep their own global setup; `dependsOn` `["...", "build"]`; `flags` `["--test-force-exit"]` if it keeps force-exit for each project |
|
|
327
|
+
| factory | 2 targets: `api` with Docker arguments; `foundation` with its tests in `tests/` | delete `apps/factory/api/src/testing/docker-global-setup.ts`; `dependsOn` `["...", "^sdk"]` |
|
|
328
|
+
| whatisgoingon | 1 target | 1 |
|
|
329
|
+
| deepagents | 11 tagged projects, 3 Docker projects, 4 demo scripts | delete `tools/src/docker-global-setup.ts`; `dependsOn` `["...", "build"]`; module mocks in `flags` or in the `args` of a project; the 4 demo scripts and the Docker context script stay |
|
|
330
|
+
| elt | 55 commands in the projects, 8 with the global setup | delete `docker-global-setup.ts`; `dependsOn` `["...", "build"]`; the runs from the workspace root stay as overrides |
|
|
331
|
+
| context-compiler | 17 targets; the `plugin` project has a no-op `test` that depends on `server:test` | 5 connector targets that test nothing go; `dependsOn` `["...", "build"]`; the desktop e2e files stay as an override; if `plugin` owns no test file, the plugin gives it nothing and its own `test` in `project.json` stays |
|
|
332
|
+
| text2sql | about 24 commands in the projects, and 13 projects on the shared default; Nx 23.1.0 | update Nx; delete `packages/adapters/src/testing/docker-global-setup.ts` and `apps/desktop/backend/src/testing/docker-global-setup.ts`; 6 targets that test nothing go; `.agents` and `.claude` get a target; the backends set `--test-timeout=120000` in `args`; the daemon keeps `["image"]` or uses `"..."` |
|
|
333
|
+
|
|
334
|
+
### Who uses the test area
|
|
335
|
+
|
|
336
|
+
| Workspace | Backlog item |
|
|
337
|
+
| ---------------- | ------------------------------------------- |
|
|
338
|
+
| zukhruf | uses it: `nx.json` lists the plugin by path |
|
|
339
|
+
| factory | #3363 |
|
|
340
|
+
| whatisgoingon | #3364 |
|
|
341
|
+
| deepagents | #3365 |
|
|
342
|
+
| elt | #3366 |
|
|
343
|
+
| context-compiler | #3367 |
|
|
344
|
+
| text2sql | #3368 |
|
|
345
|
+
|
|
346
|
+
The umbrella item is #3328.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CreateNodes } from '@nx/devkit';
|
|
2
|
+
/**
|
|
3
|
+
* Gives each Nx project `format` (writes) and `format:check` (only reads,
|
|
4
|
+
* cached). Any root that a plugin returns becomes a project, and no plugin
|
|
5
|
+
* hook receives the projects that Nx found. So the plugin selects roots with
|
|
6
|
+
* the rule and the matcher of Nx's own package.json plugin: each folder with a
|
|
7
|
+
* `project.json`, and a `package.json` folder only when the package-manager
|
|
8
|
+
* workspace globs match it. It returns targets only; the default plugins name
|
|
9
|
+
* the project.
|
|
10
|
+
*/
|
|
11
|
+
export declare const createNodes: CreateNodes;
|
|
12
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/format/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,WAAW,EAIjB,MAAM,YAAY,CAAC;AAOpB;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,EAAE,WA4CzB,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { basename, dirname, isAbsolute, join, relative } from 'node:path';
|
|
2
|
+
import { createNodesFromFiles, readJsonFile, } from '@nx/devkit';
|
|
3
|
+
import { buildPackageJsonPatterns, buildPackageJsonWorkspacesMatcher, } from '@nx/devkit/internal';
|
|
4
|
+
import { resolveConfig, resolveConfigFile } from 'prettier';
|
|
5
|
+
/**
|
|
6
|
+
* Gives each Nx project `format` (writes) and `format:check` (only reads,
|
|
7
|
+
* cached). Any root that a plugin returns becomes a project, and no plugin
|
|
8
|
+
* hook receives the projects that Nx found. So the plugin selects roots with
|
|
9
|
+
* the rule and the matcher of Nx's own package.json plugin: each folder with a
|
|
10
|
+
* `project.json`, and a `package.json` folder only when the package-manager
|
|
11
|
+
* workspace globs match it. It returns targets only; the default plugins name
|
|
12
|
+
* the project.
|
|
13
|
+
*/
|
|
14
|
+
export const createNodes = [
|
|
15
|
+
'{package.json,**/package.json,project.json,**/project.json}',
|
|
16
|
+
async (configFiles, options, context) => {
|
|
17
|
+
const projectJsonRoots = new Set(configFiles
|
|
18
|
+
.filter((file) => basename(file) === 'project.json')
|
|
19
|
+
.map((file) => dirname(file)));
|
|
20
|
+
const isInWorkspaces = buildPackageJsonWorkspacesMatcher(buildPackageJsonPatterns(context.workspaceRoot, (file) => readJsonFile(join(context.workspaceRoot, file))));
|
|
21
|
+
const rootFiles = configFiles.filter((file) => basename(file) === 'project.json'
|
|
22
|
+
? true
|
|
23
|
+
: !projectJsonRoots.has(dirname(file)) && isInWorkspaces(file));
|
|
24
|
+
const formatCheck = {
|
|
25
|
+
command: 'prettier --check {projectRoot}',
|
|
26
|
+
cache: true,
|
|
27
|
+
inputs: [
|
|
28
|
+
'default',
|
|
29
|
+
'{workspaceRoot}/.prettierignore',
|
|
30
|
+
'{workspaceRoot}/.editorconfig',
|
|
31
|
+
...(await prettierConfigInputs(context.workspaceRoot)),
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
return createNodesFromFiles((file) => ({
|
|
35
|
+
projects: {
|
|
36
|
+
[dirname(file)]: {
|
|
37
|
+
targets: {
|
|
38
|
+
format: { command: 'prettier --write {projectRoot}' },
|
|
39
|
+
'format:check': formatCheck,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}), rootFiles, options, context);
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
async function prettierConfigInputs(workspaceRoot) {
|
|
47
|
+
// `resolveConfigFile` gives null when no config file exists
|
|
48
|
+
// (prettier/index.d.ts:661).
|
|
49
|
+
const file = await resolveConfigFile(join(workspaceRoot, 'package.json'));
|
|
50
|
+
if (file === null) {
|
|
51
|
+
return [{ externalDependencies: ['prettier'] }];
|
|
52
|
+
}
|
|
53
|
+
// `resolveConfig` is typed as nullable (prettier/index.d.ts:649-652), and
|
|
54
|
+
// `Options.plugins` is optional (prettier/index.d.ts:807).
|
|
55
|
+
const plugins = (await resolveConfig(file, { config: file }))?.plugins ?? [];
|
|
56
|
+
return [
|
|
57
|
+
`{workspaceRoot}/${relative(workspaceRoot, file)}`,
|
|
58
|
+
{
|
|
59
|
+
externalDependencies: [
|
|
60
|
+
'prettier',
|
|
61
|
+
...plugins
|
|
62
|
+
.filter((plugin) => typeof plugin === 'string' &&
|
|
63
|
+
!plugin.startsWith('.') &&
|
|
64
|
+
!isAbsolute(plugin))
|
|
65
|
+
.map((plugin) => plugin
|
|
66
|
+
.split('/')
|
|
67
|
+
.slice(0, plugin.startsWith('@') ? 2 : 1)
|
|
68
|
+
.join('/')),
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/format/plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CreateNodes } from '@nx/devkit';
|
|
2
|
+
export interface TestOptions {
|
|
3
|
+
timeout?: number;
|
|
4
|
+
flags?: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Gives the `test` target, a `node --test` run, to each project root that
|
|
8
|
+
* owns test files and is not a Vitest project (ADR 0004). A project with the
|
|
9
|
+
* tag `test:docker` also gets the global setup of `@zukhruf/testing` (ADR
|
|
10
|
+
* 0005). The project roots are the roots of `./format`: a copy of its rule,
|
|
11
|
+
* on the record in docs/copied-code.md.
|
|
12
|
+
*/
|
|
13
|
+
export declare const createNodes: CreateNodes<TestOptions>;
|
|
14
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/test/plugin.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,WAAW,EAIjB,MAAM,YAAY,CAAC;AAMpB,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAOD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,WAAW,CAAC,WAAW,CAwFhD,CAAC"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { basename, dirname, extname, join, relative } from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { createNodesFromFiles, readJsonFile, } from '@nx/devkit';
|
|
14
|
+
import { buildPackageJsonPatterns, buildPackageJsonWorkspacesMatcher, } from '@nx/devkit/internal';
|
|
15
|
+
const testFile = /\.test\.(?:ts|mts|cts|js|mjs)$/;
|
|
16
|
+
const viteConfig = /^vite\.config\.(?:js|ts|mjs|mts|cjs|cts)$/;
|
|
17
|
+
const vitestConfig = /^vitest\.config\.(?:js|ts|mjs|mts|cjs|cts)$/;
|
|
18
|
+
const ownGlobalSetup = 'src/testing/docker-global-setup.ts';
|
|
19
|
+
/**
|
|
20
|
+
* Gives the `test` target, a `node --test` run, to each project root that
|
|
21
|
+
* owns test files and is not a Vitest project (ADR 0004). A project with the
|
|
22
|
+
* tag `test:docker` also gets the global setup of `@zukhruf/testing` (ADR
|
|
23
|
+
* 0005). The project roots are the roots of `./format`: a copy of its rule,
|
|
24
|
+
* on the record in docs/copied-code.md.
|
|
25
|
+
*/
|
|
26
|
+
export const createNodes = [
|
|
27
|
+
'{package.json,**/package.json,project.json,**/project.json,**/*.test.{ts,mts,cts,js,mjs},**/{vite,vitest}.config.{js,ts,mjs,mts,cjs,cts}}',
|
|
28
|
+
async (files, options, context) => {
|
|
29
|
+
// The defaults of ADR 0004: 60 s is the time limit of zukhruf and
|
|
30
|
+
// deepagents, and no repo adds a flag for each project.
|
|
31
|
+
const { timeout = 60_000, flags = [] } = options ?? {};
|
|
32
|
+
const configFiles = files.filter((file) => ['package.json', 'project.json'].includes(basename(file)));
|
|
33
|
+
const projectJsonRoots = new Set(configFiles
|
|
34
|
+
.filter((file) => basename(file) === 'project.json')
|
|
35
|
+
.map((file) => dirname(file)));
|
|
36
|
+
const isInWorkspaces = buildPackageJsonWorkspacesMatcher(buildPackageJsonPatterns(context.workspaceRoot, (file) => readJsonFile(join(context.workspaceRoot, file))));
|
|
37
|
+
const rootFiles = configFiles.filter((file) => basename(file) === 'project.json'
|
|
38
|
+
? true
|
|
39
|
+
: !projectJsonRoots.has(dirname(file)) && isInWorkspaces(file));
|
|
40
|
+
const roots = new Set(rootFiles.map((file) => dirname(file)));
|
|
41
|
+
const owned = new Map();
|
|
42
|
+
for (const file of files.filter((file) => testFile.test(file))) {
|
|
43
|
+
const root = nearestRoot(roots, dirname(file));
|
|
44
|
+
if (root !== undefined && root !== '.') {
|
|
45
|
+
owned.set(root, [...(owned.get(root) ?? []), relative(root, file)]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const vitestRoots = new Set(files
|
|
49
|
+
.filter((file) => vitestConfig.test(basename(file)))
|
|
50
|
+
.map((file) => dirname(file)));
|
|
51
|
+
const viteConfigs = new Map(files
|
|
52
|
+
.filter((file) => viteConfig.test(basename(file)))
|
|
53
|
+
.map((file) => [dirname(file), file]));
|
|
54
|
+
return createNodesFromFiles(async (file) => {
|
|
55
|
+
const root = dirname(file);
|
|
56
|
+
const tests = owned.get(root);
|
|
57
|
+
if (tests === undefined ||
|
|
58
|
+
vitestRoots.has(root) ||
|
|
59
|
+
(await hasViteTest(context.workspaceRoot, viteConfigs.get(root)))) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
const docker = tags(context.workspaceRoot, root, new Set(configFiles)).includes('test:docker');
|
|
63
|
+
const command = [
|
|
64
|
+
'node --test --no-warnings',
|
|
65
|
+
`--test-timeout=${timeout}`,
|
|
66
|
+
...new Set([
|
|
67
|
+
...flags,
|
|
68
|
+
...(docker
|
|
69
|
+
? [
|
|
70
|
+
`--test-global-setup=${globalSetup(context.workspaceRoot, root)}`,
|
|
71
|
+
'--test-force-exit',
|
|
72
|
+
]
|
|
73
|
+
: []),
|
|
74
|
+
]),
|
|
75
|
+
'--test-reporter=spec --test-reporter-destination=stdout',
|
|
76
|
+
'--test-reporter=junit --test-reporter-destination=test-results.xml',
|
|
77
|
+
'{args}',
|
|
78
|
+
].join(' ');
|
|
79
|
+
const test = {
|
|
80
|
+
executor: 'nx:run-commands',
|
|
81
|
+
options: { command, cwd: '{projectRoot}', args: selection(tests) },
|
|
82
|
+
outputs: ['{projectRoot}/test-results.xml'],
|
|
83
|
+
cache: false,
|
|
84
|
+
dependsOn: ['^build'],
|
|
85
|
+
};
|
|
86
|
+
return { projects: { [root]: { targets: { test } } } };
|
|
87
|
+
}, rootFiles, options, context);
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
function nearestRoot(roots, folder) {
|
|
91
|
+
if (roots.has(folder)) {
|
|
92
|
+
return folder;
|
|
93
|
+
}
|
|
94
|
+
return folder === '.' ? undefined : nearestRoot(roots, dirname(folder));
|
|
95
|
+
}
|
|
96
|
+
// One glob for each top-level folder with test files, and one glob with no
|
|
97
|
+
// `**` for the test files directly in the root (ADR 0004).
|
|
98
|
+
function selection(tests) {
|
|
99
|
+
const extensions = new Map();
|
|
100
|
+
for (const test of tests) {
|
|
101
|
+
const slash = test.indexOf('/');
|
|
102
|
+
const folder = slash === -1 ? '' : test.slice(0, slash);
|
|
103
|
+
extensions.set(folder, (extensions.get(folder) ?? new Set()).add(extname(test).slice(1)));
|
|
104
|
+
}
|
|
105
|
+
return [...extensions]
|
|
106
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
107
|
+
.map(([folder, found]) => {
|
|
108
|
+
const extension = found.size === 1
|
|
109
|
+
? [...found].join('')
|
|
110
|
+
: `{${[...found].sort().join(',')}}`;
|
|
111
|
+
return `"${folder === '' ? '' : `${folder}/**/`}*.test.${extension}"`;
|
|
112
|
+
})
|
|
113
|
+
.join(' ');
|
|
114
|
+
}
|
|
115
|
+
// The rule of `@nx/vitest`: a `vite.config.*` whose resolved config has
|
|
116
|
+
// `test` makes a Vitest project (`@nx/vitest/dist/src/plugins/plugin.js:
|
|
117
|
+
// 120-138`). Vite is loaded from the workspace, and only when a root has a
|
|
118
|
+
// `vite.config.*`.
|
|
119
|
+
async function hasViteTest(workspaceRoot, file) {
|
|
120
|
+
if (file === undefined) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
const vite = await import(__rewriteRelativeImportExtension(pathToFileURL(createRequire(join(workspaceRoot, 'package.json')).resolve('vite')).href));
|
|
124
|
+
const config = await vite.resolveConfig({ configFile: join(workspaceRoot, file), mode: 'development' }, 'build');
|
|
125
|
+
return 'test' in config && Boolean(config.test);
|
|
126
|
+
}
|
|
127
|
+
function tags(workspaceRoot, root, configFiles) {
|
|
128
|
+
const projectJson = join(root, 'project.json');
|
|
129
|
+
const packageJson = join(root, 'package.json');
|
|
130
|
+
return [
|
|
131
|
+
...(configFiles.has(projectJson)
|
|
132
|
+
? (readJsonFile(join(workspaceRoot, projectJson))
|
|
133
|
+
.tags ?? [])
|
|
134
|
+
: []),
|
|
135
|
+
...(configFiles.has(packageJson)
|
|
136
|
+
? (readJsonFile(join(workspaceRoot, packageJson)).nx?.tags ?? [])
|
|
137
|
+
: []),
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
// The project's own global setup wins; else the one of `@zukhruf/testing`,
|
|
141
|
+
// resolved from the project root with the default conditions and written
|
|
142
|
+
// relative to the `cwd` of the target (ADR 0005). Node reads the value as a
|
|
143
|
+
// path, so a package name does not work.
|
|
144
|
+
function globalSetup(workspaceRoot, root) {
|
|
145
|
+
const projectRoot = join(workspaceRoot, root);
|
|
146
|
+
if (existsSync(join(projectRoot, ownGlobalSetup))) {
|
|
147
|
+
return ownGlobalSetup;
|
|
148
|
+
}
|
|
149
|
+
return relative(projectRoot, createRequire(join(projectRoot, 'package.json')).resolve('@zukhruf/testing/docker-global-setup'));
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/test/plugin.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zukhruf/nx",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.58",
|
|
4
|
+
"description": "The Nx part of the monorepo standard, one area per subpath. @zukhruf/nx/format gives each project format and a cached format:check, both with Prettier. @zukhruf/nx/test gives each project that has node:test files a test target, a node --test run.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/JanuaryLabs/zukhruf",
|
|
9
|
+
"directory": "packages/nx"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/JanuaryLabs/zukhruf/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/JanuaryLabs/zukhruf/tree/main/packages/nx#readme",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"nx",
|
|
17
|
+
"nx-plugin",
|
|
18
|
+
"inference",
|
|
19
|
+
"createNodes",
|
|
20
|
+
"monorepo",
|
|
21
|
+
"prettier",
|
|
22
|
+
"format",
|
|
23
|
+
"check",
|
|
24
|
+
"node-test",
|
|
25
|
+
"test"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"exports": {
|
|
29
|
+
"./package.json": "./package.json",
|
|
30
|
+
"./format": {
|
|
31
|
+
"@zukhruf/source": "./src/format/plugin.ts",
|
|
32
|
+
"types": "./dist/format/plugin.d.ts",
|
|
33
|
+
"import": "./dist/format/plugin.js",
|
|
34
|
+
"default": "./dist/format/plugin.js"
|
|
35
|
+
},
|
|
36
|
+
"./test": {
|
|
37
|
+
"@zukhruf/source": "./src/test/plugin.ts",
|
|
38
|
+
"types": "./dist/test/plugin.d.ts",
|
|
39
|
+
"import": "./dist/test/plugin.js",
|
|
40
|
+
"default": "./dist/test/plugin.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"!dist/spec/**",
|
|
46
|
+
"!**/*.tsbuildinfo",
|
|
47
|
+
"!**/*.test.*"
|
|
48
|
+
],
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@nx/devkit": ">=23.2.1 <25.0.0",
|
|
54
|
+
"nx": ">=23.2.1 <25.0.0",
|
|
55
|
+
"prettier": "^3.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@zukhruf/testing": "^0.3.58"
|
|
59
|
+
}
|
|
60
|
+
}
|