create-nx-archweave 0.2.2 → 0.3.1
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 +24 -20
- package/dist/bin.js +87 -79
- package/dist/cli/execute.d.ts +11 -1
- package/dist/cli/package-manager.d.ts +5 -15
- package/dist/cli/plan.d.ts +2 -13
- package/dist/index.js +83 -38
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
One command takes you from an empty directory to a configured Nx workspace with:
|
|
4
4
|
|
|
5
|
-
- FSD Lite
|
|
5
|
+
- an FSD Lite lint rule for the frontend app's layers (`warn` or `error` by preset), plus slice generators
|
|
6
|
+
- frontend/backend boundaries as lint errors between the projects it tags, when you pick a backend
|
|
6
7
|
- AI agent rules
|
|
7
8
|
- GitHub templates
|
|
8
|
-
- optional AI PR review (Claude
|
|
9
|
+
- optional AI PR review with the provider you choose (Claude supported; Gemini, GPT and Cursor in beta)
|
|
9
10
|
|
|
10
11
|
```bash
|
|
11
12
|
# npm
|
|
@@ -27,15 +28,12 @@ Anything you don't pass as a flag, the CLI asks you interactively. The `quick-st
|
|
|
27
28
|
|
|
28
29
|
- Node >= 22.18
|
|
29
30
|
- npm, pnpm >= 10, yarn (classic or berry) or bun
|
|
30
|
-
- network access to github.com (Nx's react template is downloaded from there)
|
|
31
|
-
|
|
32
|
-
**react-vite and older package managers.** You can't create react-vite with npm 10 or yarn 1. Both of those fail inside `create-nx-workspace`'s own install of Nx's react template. So the CLI never offers npm 10 or yarn 1 for react-vite. If a flag names one of them, the CLI stops before creating anything.
|
|
33
31
|
|
|
34
32
|
## Choosing a package manager
|
|
35
33
|
|
|
36
34
|
The CLI asks for the package manager too:
|
|
37
35
|
|
|
38
|
-
- It only offers package managers that are installed
|
|
36
|
+
- It only offers package managers that are installed.
|
|
39
37
|
- It preselects the one that launched the CLI: `npx` → npm, `pnpm dlx` → pnpm, `yarn dlx` → yarn, `bunx` → bun.
|
|
40
38
|
- Without a terminal, it uses the launching package manager, else npm.
|
|
41
39
|
|
|
@@ -47,29 +45,35 @@ A few things to know for specific package managers:
|
|
|
47
45
|
|
|
48
46
|
## What it runs
|
|
49
47
|
|
|
50
|
-
The CLI prints its plan up front. Then it runs
|
|
48
|
+
The CLI prints its plan up front. Then it runs the steps below with the package manager you chose.
|
|
51
49
|
|
|
52
|
-
- It never re-implements `create-nx-workspace
|
|
50
|
+
- It never re-implements `create-nx-workspace` or Nx's generators.
|
|
53
51
|
- It never commits for you.
|
|
54
|
-
- `--
|
|
55
|
-
-
|
|
52
|
+
- `--frontend=next` creates the workspace with `--preset=next --appName=web --e2eTestRunner=none` instead, which already holds the app.
|
|
53
|
+
- No path creates an e2e project. See the [getting-started guide](https://github.com/clearlydev/nx-archweave/blob/main/docs/getting-started.md#adding-e2e-tests) to add one with Nx's own generator.
|
|
54
|
+
- pnpm adds a last step that records the build scripts pnpm skipped. bun adds one that lists the install scripts bun blocked (`bun pm untrusted`).
|
|
56
55
|
|
|
57
|
-
Here are the
|
|
56
|
+
Here are the steps for npm and pnpm.
|
|
58
57
|
|
|
59
|
-
**Step 1: create the workspace**
|
|
58
|
+
**Step 1: create the workspace, then the React app**
|
|
60
59
|
|
|
61
60
|
```bash
|
|
62
61
|
# npm
|
|
63
|
-
npx -y create-nx-workspace@
|
|
64
|
-
--
|
|
65
|
-
|
|
62
|
+
npx -y create-nx-workspace@24.0.1 <name> \
|
|
63
|
+
--preset=empty --pm=npm --nxCloud=skip --interactive=false
|
|
64
|
+
npm exec nx -- add @nx/react
|
|
65
|
+
npm exec nx -- g @nx/react:app apps/web --bundler=vite --linter=eslint \
|
|
66
|
+
--style=css --unitTestRunner=vitest --e2eTestRunner=none --routing=false
|
|
66
67
|
|
|
67
68
|
# pnpm
|
|
68
|
-
pnpm dlx create-nx-workspace@
|
|
69
|
-
--
|
|
70
|
-
|
|
69
|
+
pnpm dlx create-nx-workspace@24.0.1 <name> \
|
|
70
|
+
--preset=empty --pm=pnpm --nxCloud=skip --interactive=false
|
|
71
|
+
pnpm nx add @nx/react
|
|
72
|
+
pnpm nx g @nx/react:app apps/web … (same flags)
|
|
71
73
|
```
|
|
72
74
|
|
|
75
|
+
`--preset=empty` downloads nothing and keeps the workspace on that create-nx-workspace's Nx version. `nx add` installs `@nx/react` at the same version.
|
|
76
|
+
|
|
73
77
|
**Step 2: install the plugin**
|
|
74
78
|
|
|
75
79
|
```bash
|
|
@@ -94,13 +98,13 @@ pnpm nx g @clearlydev/nx-archweave:init …
|
|
|
94
98
|
With `--backend=express|nest`, the CLI inserts two official steps before init:
|
|
95
99
|
|
|
96
100
|
1. `@nx/<backend>`, pinned to the workspace's exact nx version
|
|
97
|
-
2. `nx g @nx/<backend>:application apps/api --frontendProject=<detected app
|
|
101
|
+
2. `nx g @nx/<backend>:application apps/api --frontendProject=<detected app> --unitTestRunner=vitest --e2eTestRunner=none` (vitest like the frontend; the generator would otherwise ask)
|
|
98
102
|
|
|
99
103
|
init then wires `shared-types` and `scope:` tags, and writes the boundary constraints (`--applyEslint=true` on this path).
|
|
100
104
|
|
|
101
105
|
When something goes wrong:
|
|
102
106
|
|
|
103
|
-
- If a step fails, the CLI stops right away. It leaves the created directory in place (it never auto-deletes)
|
|
107
|
+
- If a step fails, the CLI stops right away. It leaves the created directory in place (it never auto-deletes), tells you how to recover, and prints the failed step and every step after it as commands you can run yourself. It never re-runs them for you.
|
|
104
108
|
- When stdin isn't a TTY and a required input is missing, the CLI fails fast instead of hanging.
|
|
105
109
|
|
|
106
110
|
## Options
|
package/dist/bin.js
CHANGED
|
@@ -22,9 +22,9 @@ var AI_HOST_CHOICES = [
|
|
|
22
22
|
];
|
|
23
23
|
var REVIEW_PROVIDER_CHOICES = [
|
|
24
24
|
{ id: "claude", label: "Claude" },
|
|
25
|
-
{ id: "gemini", label: "Gemini" },
|
|
26
|
-
{ id: "gpt", label: "GPT" },
|
|
27
|
-
{ id: "cursor", label: "Cursor" }
|
|
25
|
+
{ id: "gemini", label: "Gemini (beta)" },
|
|
26
|
+
{ id: "gpt", label: "GPT (beta)" },
|
|
27
|
+
{ id: "cursor", label: "Cursor (beta)" }
|
|
28
28
|
];
|
|
29
29
|
var REVIEW_PROVIDER_VALUES = REVIEW_PROVIDER_CHOICES.map((provider) => provider.id);
|
|
30
30
|
var AI_HOST_VALUES = AI_HOST_CHOICES.map((host) => host.id);
|
|
@@ -46,12 +46,6 @@ var PLUGIN_CHOICES = [
|
|
|
46
46
|
];
|
|
47
47
|
var PLUGIN_VALUES = PLUGIN_CHOICES.map((plugin) => plugin.id);
|
|
48
48
|
|
|
49
|
-
// src/cli/preflight.ts
|
|
50
|
-
function parseMajor(version) {
|
|
51
|
-
const match = /^\s*v?(\d+)\./.exec(version ?? "");
|
|
52
|
-
return match ? Number(match[1]) : void 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
49
|
// src/cli/package-manager.ts
|
|
56
50
|
var SUPPORTED = ["npm", "pnpm", "yarn", "bun"];
|
|
57
51
|
function isCliPackageManager(name) {
|
|
@@ -71,24 +65,11 @@ function listInstalledManagers(launcher, probe) {
|
|
|
71
65
|
}
|
|
72
66
|
return installed;
|
|
73
67
|
}
|
|
74
|
-
function managerIncompatibility(manager, frontend) {
|
|
75
|
-
const major = parseMajor(manager.version);
|
|
76
|
-
if (frontend !== "react-vite" || major === void 0) return void 0;
|
|
77
|
-
if (manager.name === "npm" && major < 11) return "react-vite needs npm 11 \u2014 npm 10 crashes in Nx's react template";
|
|
78
|
-
if (manager.name === "yarn" && major < 2) return "react-vite needs yarn 2+ \u2014 yarn 1 cannot resolve the template's workspace:* packages";
|
|
79
|
-
return void 0;
|
|
80
|
-
}
|
|
81
68
|
var NOTHING_CREATED = "Nothing was created.";
|
|
82
69
|
async function choosePackageManager(input, ask, tell) {
|
|
83
70
|
const launcher = parseUserAgent(input.userAgent);
|
|
84
71
|
const resolveNamed = (name, source) => {
|
|
85
72
|
const version = launcher?.name === name && launcher.version ? launcher.version : input.probe(name)?.trim() || void 0;
|
|
86
|
-
const reason = managerIncompatibility({ name, version }, input.frontend);
|
|
87
|
-
if (reason) {
|
|
88
|
-
return {
|
|
89
|
-
error: `create-nx-archweave stopped before creating anything: ${name} ${version} cannot create this workspace \u2014 ${reason}. ${INSTALL_ADVICE} ${NOTHING_CREATED}`
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
73
|
return { packageManager: name, source, version };
|
|
93
74
|
};
|
|
94
75
|
if (input.flag) return resolveNamed(input.flag, "flag");
|
|
@@ -105,50 +86,30 @@ async function choosePackageManager(input, ask, tell) {
|
|
|
105
86
|
tell(`${launcher.name} is not supported (${SUPPORTED_TEXT} are) \u2014 choosing from the supported ones instead.`);
|
|
106
87
|
}
|
|
107
88
|
const installed = listInstalledManagers(launcher, input.probe);
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
const unavailable = installed.filter((_, index) => reasons[index] !== void 0);
|
|
111
|
-
const unavailableLines = unavailable.map(
|
|
112
|
-
(manager) => ` ${manager.name.padEnd(6)}${manager.version ?? ""} \u2014 not available: ${reasons[installed.indexOf(manager)]}`
|
|
113
|
-
);
|
|
114
|
-
if (selectable.length === 0) {
|
|
115
|
-
return {
|
|
116
|
-
error: ["create-nx-archweave stopped before creating anything: no installed package manager can create this workspace.", ...unavailableLines, `${INSTALL_ADVICE} ${NOTHING_CREATED}`].join("\n")
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
if (selectable.length === 1) {
|
|
120
|
-
if (unavailable.length > 0) tell(["Package manager:", ...unavailableLines].join("\n"));
|
|
121
|
-
return { packageManager: selectable[0].name, source: unavailable.length > 0 ? "only-compatible" : "only-installed", version: selectable[0].version };
|
|
122
|
-
}
|
|
123
|
-
const defaultIndex = Math.max(0, selectable.findIndex((manager) => manager.name === launcher?.name));
|
|
89
|
+
if (installed.length === 1) return { packageManager: installed[0].name, source: "only-installed", version: installed[0].version };
|
|
90
|
+
const defaultIndex = Math.max(0, installed.findIndex((manager) => manager.name === launcher?.name));
|
|
124
91
|
tell("? Package manager");
|
|
125
|
-
|
|
92
|
+
installed.forEach((manager, index) => {
|
|
126
93
|
const head = ` ${index + 1}) ${manager.name.padEnd(6)}${manager.version ?? ""}`.trimEnd();
|
|
127
94
|
tell(index === defaultIndex ? `${head} (default)` : head);
|
|
128
95
|
});
|
|
129
|
-
unavailableLines.forEach((line) => tell(line));
|
|
130
96
|
for (; ; ) {
|
|
131
|
-
const answer = (await ask(`Choose 1-${
|
|
97
|
+
const answer = (await ask(`Choose 1-${installed.length} [${defaultIndex + 1}]: `)).trim().toLowerCase();
|
|
132
98
|
const byNumber = /^\d+$/.test(answer) ? Number(answer) - 1 : -1;
|
|
133
|
-
const index = answer === "" ? defaultIndex : byNumber >= 0 ? byNumber :
|
|
134
|
-
if (index >= 0 && index <
|
|
135
|
-
return { packageManager:
|
|
99
|
+
const index = answer === "" ? defaultIndex : byNumber >= 0 ? byNumber : installed.findIndex((manager) => manager.name === answer);
|
|
100
|
+
if (index >= 0 && index < installed.length) {
|
|
101
|
+
return { packageManager: installed[index].name, source: "asked", version: installed[index].version };
|
|
136
102
|
}
|
|
137
|
-
|
|
138
|
-
tell(
|
|
139
|
-
blocked ? `${blocked.name} is not available here: ${reasons[installed.indexOf(blocked)]}.` : `Choose a number from 1 to ${selectable.length} (or a name: ${selectable.map((manager) => manager.name).join(", ")}).`
|
|
140
|
-
);
|
|
103
|
+
tell(`Choose a number from 1 to ${installed.length} (or a name: ${installed.map((manager) => manager.name).join(", ")}).`);
|
|
141
104
|
}
|
|
142
105
|
}
|
|
143
106
|
var SUPPORTED_TEXT = "npm, pnpm, yarn and bun";
|
|
144
|
-
var INSTALL_ADVICE = "Use npm 11+, pnpm, yarn 2+ (Corepack: corepack enable) or bun, or pass --package-manager= with one of them.";
|
|
145
107
|
var SOURCE_TEXT = {
|
|
146
108
|
flag: "--package-manager",
|
|
147
109
|
launcher: "the package manager that ran this command",
|
|
148
110
|
default: "default \u2014 no launching package manager detected",
|
|
149
111
|
asked: "chosen",
|
|
150
|
-
"only-installed": "the only one installed"
|
|
151
|
-
"only-compatible": "the only installed one that can create this workspace"
|
|
112
|
+
"only-installed": "the only one installed"
|
|
152
113
|
};
|
|
153
114
|
function describeResolution(resolution) {
|
|
154
115
|
const name = resolution.version ? `${resolution.packageManager} ${resolution.version}` : resolution.packageManager;
|
|
@@ -509,9 +470,7 @@ function renderVersion() {
|
|
|
509
470
|
|
|
510
471
|
// src/cli/plan.ts
|
|
511
472
|
var DEFAULT_PLUGIN_SOURCE = `@clearlydev/nx-archweave@^${CLI_VERSION}`;
|
|
512
|
-
var VALIDATED_CREATE_NX_WORKSPACE_VERSION = "
|
|
513
|
-
var NPM_10_CREATE_WORKSPACE_HINT = `If the npm error above is "Cannot read properties of null (reading 'edgesOut')", that is a known npm 10 bug with the current Nx workspace dependencies \u2014 upgrade npm (npm install -g npm@11), or, if pnpm is installed, re-run with --package-manager=pnpm.`;
|
|
514
|
-
var YARN_1_CREATE_WORKSPACE_HINT = `If the yarn error above is "Couldn't find package \u2026@workspace:*", that is yarn 1 (classic), which cannot resolve the workspace:* packages in Nx's react template \u2014 use yarn 2+ (corepack enable, then yarn set version stable) or re-run with another package manager.`;
|
|
473
|
+
var VALIDATED_CREATE_NX_WORKSPACE_VERSION = "24.0.1";
|
|
515
474
|
function nxArgs(pm) {
|
|
516
475
|
return pm === "npm" ? ["exec", "nx", "--"] : ["nx"];
|
|
517
476
|
}
|
|
@@ -528,10 +487,11 @@ function isYarnBerry(ctx) {
|
|
|
528
487
|
return ctx.readFile(".yarnrc.yml") !== void 0 || /^__metadata:/m.test(ctx.readFile("yarn.lock") ?? "");
|
|
529
488
|
}
|
|
530
489
|
var YARN_PLAN_ENV = { YARN_ENABLE_IMMUTABLE_INSTALLS: "false" };
|
|
490
|
+
var NO_E2E = ["--e2eTestRunner=none"];
|
|
531
491
|
function createWorkspaceSourceArgs(frontend, backend) {
|
|
532
|
-
if (frontend === "react-vite") return ["--
|
|
533
|
-
if (frontend === "next") return ["--preset=next"];
|
|
534
|
-
return [`--preset=${backend}`, "--appName=api"];
|
|
492
|
+
if (frontend === "react-vite") return ["--preset=empty"];
|
|
493
|
+
if (frontend === "next") return ["--preset=next", "--appName=web", ...NO_E2E];
|
|
494
|
+
return [`--preset=${backend}`, "--appName=api", ...NO_E2E, "--unitTestRunner=vitest"];
|
|
535
495
|
}
|
|
536
496
|
function buildNextSteps(input) {
|
|
537
497
|
const lines = [
|
|
@@ -556,7 +516,7 @@ function buildNextSteps(input) {
|
|
|
556
516
|
function buildPlan(input) {
|
|
557
517
|
const pluginSource = input.pluginSource ?? DEFAULT_PLUGIN_SOURCE;
|
|
558
518
|
const workspaceSource = createWorkspaceSourceArgs(input.frontend, input.backend);
|
|
559
|
-
const aiAgentArgs = input.aiHosts.map((host) => `--aiAgents=${AI_HOST_CHOICES.find((choice) => choice.id === host)?.nxAgent ?? host}`);
|
|
519
|
+
const aiAgentArgs = input.aiHosts.length === 0 ? ["--aiAgents=none"] : input.aiHosts.map((host) => `--aiAgents=${AI_HOST_CHOICES.find((choice) => choice.id === host)?.nxAgent ?? host}`);
|
|
560
520
|
const pm = input.packageManager;
|
|
561
521
|
const nxPrefix = nxArgs(pm);
|
|
562
522
|
const initArgs = [
|
|
@@ -572,8 +532,10 @@ function buildPlan(input) {
|
|
|
572
532
|
// Step 2 (the plugin install) always dirties the fresh workspace's tree
|
|
573
533
|
// (package.json + lockfile), so init's clean-tree refusal would otherwise
|
|
574
534
|
// always fire here. This is orchestration knowledge, not policy: the CLI
|
|
575
|
-
// knows what its own previous step did.
|
|
576
|
-
|
|
535
|
+
// knows what its own previous step did. For the same reason the report's note about
|
|
536
|
+
// reverting earlier user changes doesn't apply: the only changes are that install's.
|
|
537
|
+
"--allowDirty=true",
|
|
538
|
+
"--dirtyFromCaller=true"
|
|
577
539
|
];
|
|
578
540
|
if (input.review !== void 0) {
|
|
579
541
|
initArgs.push(`--review=${input.review}`);
|
|
@@ -608,10 +570,9 @@ function buildPlan(input) {
|
|
|
608
570
|
"--nxCloud=skip",
|
|
609
571
|
"--interactive=false"
|
|
610
572
|
],
|
|
611
|
-
cwd: null
|
|
612
|
-
...input.frontend === "react-vite" && pm === "npm" ? { failureHint: NPM_10_CREATE_WORKSPACE_HINT } : {},
|
|
613
|
-
...input.frontend === "react-vite" && pm === "yarn" ? { failureHint: YARN_1_CREATE_WORKSPACE_HINT } : {}
|
|
573
|
+
cwd: null
|
|
614
574
|
},
|
|
575
|
+
...input.frontend === "react-vite" ? reactAppSteps(input.name, pm) : [],
|
|
615
576
|
{
|
|
616
577
|
id: "install-plugin",
|
|
617
578
|
description: `Install ${pluginSource}`,
|
|
@@ -728,11 +689,31 @@ function listBlockedScriptsStep(workspaceName) {
|
|
|
728
689
|
failureHint: `The workspace itself is complete \u2014 every earlier step finished; only listing the blocked install scripts failed. Inside "${workspaceName}", run bun pm untrusted to see them.`
|
|
729
690
|
};
|
|
730
691
|
}
|
|
692
|
+
function reactAppSteps(workspaceName, pm) {
|
|
693
|
+
return [
|
|
694
|
+
{
|
|
695
|
+
id: "add-react-plugin",
|
|
696
|
+
description: "Add @nx/react with nx add (at the workspace nx version)",
|
|
697
|
+
command: pm,
|
|
698
|
+
args: [...nxArgs(pm), "add", "@nx/react"],
|
|
699
|
+
cwd: workspaceName
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
id: "create-web-app",
|
|
703
|
+
description: "Create the React + Vite app in apps/web with the official @nx/react generator",
|
|
704
|
+
command: pm,
|
|
705
|
+
args: [...nxArgs(pm), "g", "@nx/react:app", "apps/web", ...REACT_APP_OPTIONS],
|
|
706
|
+
cwd: workspaceName
|
|
707
|
+
}
|
|
708
|
+
];
|
|
709
|
+
}
|
|
710
|
+
var REACT_APP_OPTIONS = ["--bundler=vite", "--linter=eslint", "--style=css", "--unitTestRunner=vitest", ...NO_E2E, "--routing=false"];
|
|
711
|
+
var API_APP_OPTIONS = ["--unitTestRunner=vitest", ...NO_E2E];
|
|
731
712
|
function backendSteps(backend, workspaceName, pm, initCommand) {
|
|
732
713
|
const nxG = nxGenerateCommand(pm);
|
|
733
|
-
const
|
|
714
|
+
const apiExists = (ctx) => ctx.listDir("apps").includes("api");
|
|
734
715
|
const standaloneError = `this workspace has no apps/ directory \u2014 CLI backend wiring needs the frontend under apps/. Create the api app with the official @nx generator, then run \`${nxG} @clearlydev/nx-archweave:add-backend\` yourself. Finish setup inside "${workspaceName}" with: ${initCommand}`;
|
|
735
|
-
const
|
|
716
|
+
const apiExistsError = `the workspace already has an apps/api \u2014 wire it with \`${nxG} @clearlydev/nx-archweave:add-backend --backend=${backend}\`, or remove it and run the official generator yourself. Either way, finish setup inside "${workspaceName}" with: ${initCommand}`;
|
|
736
717
|
return [
|
|
737
718
|
{
|
|
738
719
|
id: "install-backend-plugin",
|
|
@@ -742,9 +723,7 @@ function backendSteps(backend, workspaceName, pm, initCommand) {
|
|
|
742
723
|
cwd: workspaceName,
|
|
743
724
|
finalizeArgs: (ctx) => {
|
|
744
725
|
if (ctx.listDir("apps").length === 0) return { error: standaloneError };
|
|
745
|
-
if (
|
|
746
|
-
return backend === "express" ? { skip: "the workspace template already ships the Express api" } : { error: nestConflictError };
|
|
747
|
-
}
|
|
726
|
+
if (apiExists(ctx)) return { error: apiExistsError };
|
|
748
727
|
const raw = ctx.readFile("package.json");
|
|
749
728
|
if (raw === void 0) return { error: "could not read the created workspace's package.json to pin the @nx version" };
|
|
750
729
|
try {
|
|
@@ -763,14 +742,12 @@ function backendSteps(backend, workspaceName, pm, initCommand) {
|
|
|
763
742
|
command: pm,
|
|
764
743
|
// Since Nx 20 the positional is a directory relative to the workspace root —
|
|
765
744
|
// a bare `api` would land at the root instead of beside the frontend.
|
|
766
|
-
args: [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api"],
|
|
745
|
+
args: [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", ...API_APP_OPTIONS],
|
|
767
746
|
cwd: workspaceName,
|
|
768
747
|
finalizeArgs: (ctx) => {
|
|
769
748
|
const all = ctx.listDir("apps");
|
|
770
749
|
if (all.length === 0) return { error: standaloneError };
|
|
771
|
-
if (all.includes("api")) {
|
|
772
|
-
return backend === "express" ? { skip: "the workspace template already ships the Express api" } : { error: nestConflictError };
|
|
773
|
-
}
|
|
750
|
+
if (all.includes("api")) return { error: apiExistsError };
|
|
774
751
|
const apps = all.filter((name) => name !== "api" && name !== "e2e" && !name.endsWith("-e2e"));
|
|
775
752
|
if (apps.length === 0) return { error: "no frontend app directory found under apps/ in the created workspace" };
|
|
776
753
|
if (apps.length > 1) return { error: `multiple app directories found under apps/ (${apps.join(", ")}) \u2014 cannot pick --frontendProject` };
|
|
@@ -786,7 +763,7 @@ function backendSteps(backend, workspaceName, pm, initCommand) {
|
|
|
786
763
|
}
|
|
787
764
|
return apps[0];
|
|
788
765
|
})();
|
|
789
|
-
return [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", `--frontendProject=${frontendProject}
|
|
766
|
+
return [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", `--frontendProject=${frontendProject}`, ...API_APP_OPTIONS];
|
|
790
767
|
}
|
|
791
768
|
}
|
|
792
769
|
];
|
|
@@ -971,7 +948,7 @@ function createStepContext(workspacePath) {
|
|
|
971
948
|
}
|
|
972
949
|
};
|
|
973
950
|
}
|
|
974
|
-
function executePlan(plan, exec, invokedFromDir, ctxFactory = createStepContext) {
|
|
951
|
+
function executePlan(plan, exec, invokedFromDir, ctxFactory = createStepContext, now = Date.now) {
|
|
975
952
|
const completed = [];
|
|
976
953
|
for (const step of plan.steps) {
|
|
977
954
|
const cwd = step.cwd === null ? invokedFromDir : join2(invokedFromDir, step.cwd);
|
|
@@ -991,16 +968,18 @@ function executePlan(plan, exec, invokedFromDir, ctxFactory = createStepContext)
|
|
|
991
968
|
}
|
|
992
969
|
args = finalized;
|
|
993
970
|
}
|
|
971
|
+
const startedAt = now();
|
|
994
972
|
const result = exec(step.command, args, plan.env ? { cwd, env: plan.env } : { cwd });
|
|
973
|
+
const durationMs = now() - startedAt;
|
|
995
974
|
if (result.status !== 0) {
|
|
996
975
|
if (step.optional) {
|
|
997
976
|
const optionalFailure = result.status === null ? "it did not start or was stopped (no exit code)" : `exit code: ${result.status} (its own error output is printed above)`;
|
|
998
|
-
completed.push({ step, result, args, optionalFailure });
|
|
977
|
+
completed.push({ step, result, args, optionalFailure, durationMs });
|
|
999
978
|
continue;
|
|
1000
979
|
}
|
|
1001
|
-
return { ok: false, completed, failedStep: step, failedResult: result };
|
|
980
|
+
return { ok: false, completed, failedStep: step, failedResult: result, failedArgs: args, failedDurationMs: durationMs };
|
|
1002
981
|
}
|
|
1003
|
-
completed.push({ step, result, args });
|
|
982
|
+
completed.push({ step, result, args, durationMs });
|
|
1004
983
|
}
|
|
1005
984
|
return { ok: true, completed };
|
|
1006
985
|
}
|
|
@@ -1036,9 +1015,37 @@ function renderExecuteReport(report, plan, workspaceName) {
|
|
|
1036
1015
|
...!report.finalizeError && failedStep.failureHint ? [` ${failedStep.failureHint}`] : [],
|
|
1037
1016
|
"",
|
|
1038
1017
|
`The "${workspaceName}" directory (if it was created) was left in place \u2014 create-nx-archweave never deletes it automatically.`,
|
|
1039
|
-
`Inspect the error above
|
|
1018
|
+
`Inspect the error above and fix the issue. Then either run the remaining steps below yourself, or remove the directory and start over.`,
|
|
1019
|
+
"",
|
|
1020
|
+
...renderRemainingSteps(report, plan, workspaceName)
|
|
1040
1021
|
].join("\n");
|
|
1041
1022
|
}
|
|
1023
|
+
function renderStepTimings(report) {
|
|
1024
|
+
const rows = report.completed.filter((c) => c.durationMs !== void 0).map((c) => [c.step.id, c.durationMs]);
|
|
1025
|
+
if (!report.ok && report.failedDurationMs !== void 0) rows.push([`${report.failedStep.id} (failed)`, report.failedDurationMs]);
|
|
1026
|
+
return ["Step timings:", ...rows.map(([id, ms]) => ` ${(ms / 1e3).toFixed(1).padStart(7)}s ${id}`)].join("\n");
|
|
1027
|
+
}
|
|
1028
|
+
function renderRemainingSteps(report, plan, workspaceName) {
|
|
1029
|
+
const start = plan.steps.indexOf(report.failedStep);
|
|
1030
|
+
const remaining = start === -1 ? [report.failedStep] : plan.steps.slice(start);
|
|
1031
|
+
const lines = [`Remaining steps, in order (printed only \u2014 create-nx-archweave does not run them again):`];
|
|
1032
|
+
const env = Object.entries(plan.env ?? {});
|
|
1033
|
+
if (env.length > 0) lines.push(` Run them with ${env.map(([key, value]) => `${key}=${value}`).join(" ")} set, as the CLI does.`);
|
|
1034
|
+
remaining.forEach((step, index) => {
|
|
1035
|
+
const ranArgs = step === report.failedStep ? report.failedArgs : void 0;
|
|
1036
|
+
const args = ranArgs ?? step.args;
|
|
1037
|
+
const where = step.cwd === null ? "in the directory you ran create-nx-archweave from" : `inside "${workspaceName}"`;
|
|
1038
|
+
lines.push(` ${index + 1}. ${step.description}${step.optional ? " (optional)" : ""} \u2014 ${where}:`);
|
|
1039
|
+
lines.push(` ${[step.command, ...args].map(shellQuote).join(" ")}`);
|
|
1040
|
+
if (step.finalizeArgs && ranArgs === void 0) {
|
|
1041
|
+
lines.push(" (the CLI works out this step's exact arguments when it runs \u2014 check them against the workspace first)");
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
return lines;
|
|
1045
|
+
}
|
|
1046
|
+
function shellQuote(word) {
|
|
1047
|
+
return /^[\w@%+=:,./^~-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
1048
|
+
}
|
|
1042
1049
|
|
|
1043
1050
|
// src/bin.ts
|
|
1044
1051
|
var NonInteractiveInputError = class extends Error {
|
|
@@ -1126,7 +1133,6 @@ async function main(argv) {
|
|
|
1126
1133
|
{
|
|
1127
1134
|
flag: args.packageManager,
|
|
1128
1135
|
userAgent: process.env["npm_config_user_agent"],
|
|
1129
|
-
frontend: inputs.frontend,
|
|
1130
1136
|
interactive,
|
|
1131
1137
|
probe: readVersion
|
|
1132
1138
|
},
|
|
@@ -1186,6 +1192,8 @@ async function main(argv) {
|
|
|
1186
1192
|
}
|
|
1187
1193
|
const report = executePlan(plan, createProcessExec(), process.cwd());
|
|
1188
1194
|
console.log(renderExecuteReport(report, plan, name));
|
|
1195
|
+
if (process.env["CREATE_NX_ARCHWEAVE_TIMINGS"] === "1") console.log(`
|
|
1196
|
+
${renderStepTimings(report)}`);
|
|
1189
1197
|
return report.ok ? 0 : 1;
|
|
1190
1198
|
}
|
|
1191
1199
|
function readVersion(manager) {
|
package/dist/cli/execute.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface CompletedStep {
|
|
|
26
26
|
skipped?: string;
|
|
27
27
|
/** An optional step that failed (the run went on): the exit status, or why it never ran. */
|
|
28
28
|
optionalFailure?: string;
|
|
29
|
+
/** How long the step's command ran; absent when it never ran. */
|
|
30
|
+
durationMs?: number;
|
|
29
31
|
}
|
|
30
32
|
export type ExecuteReport = {
|
|
31
33
|
ok: true;
|
|
@@ -35,8 +37,12 @@ export type ExecuteReport = {
|
|
|
35
37
|
completed: CompletedStep[];
|
|
36
38
|
failedStep: PlanStep;
|
|
37
39
|
failedResult: ExecResult;
|
|
40
|
+
/** The args the failed step ran with (after finalizeArgs); absent when it never executed. */
|
|
41
|
+
failedArgs?: string[];
|
|
38
42
|
/** Set when finalizeArgs refused — the step never executed. */
|
|
39
43
|
finalizeError?: string;
|
|
44
|
+
/** How long the failed step's command ran; absent when it never ran. */
|
|
45
|
+
failedDurationMs?: number;
|
|
40
46
|
};
|
|
41
47
|
/** Real StepContext over the created workspace (tests inject their own). */
|
|
42
48
|
export declare function createStepContext(workspacePath: string): StepContext;
|
|
@@ -47,9 +53,13 @@ export declare function createStepContext(workspacePath: string): StepContext;
|
|
|
47
53
|
* `optional` step is the exception: its failure is recorded and the run goes on. This function itself never deletes anything;
|
|
48
54
|
* it only reports.
|
|
49
55
|
*/
|
|
50
|
-
export declare function executePlan(plan: CreatePlan, exec: PlanExec, invokedFromDir: string, ctxFactory?: (workspacePath: string) => StepContext): ExecuteReport;
|
|
56
|
+
export declare function executePlan(plan: CreatePlan, exec: PlanExec, invokedFromDir: string, ctxFactory?: (workspacePath: string) => StepContext, now?: () => number): ExecuteReport;
|
|
51
57
|
/** Renders the final message printed after `executePlan` returns — success prints the
|
|
52
58
|
* plan's next-steps guidance; failure explains which step failed and how to recover,
|
|
53
59
|
* explicitly noting the created directory is left in place (never auto-deleted). */
|
|
54
60
|
export declare function renderExecuteReport(report: ExecuteReport, plan: CreatePlan, workspaceName: string): string;
|
|
61
|
+
/** How long each step that ran took, slowest work first in plan order: printed when
|
|
62
|
+
* CREATE_NX_ARCHWEAVE_TIMINGS=1 (the e2e scripts set it to see where a slow run spends its
|
|
63
|
+
* time). */
|
|
64
|
+
export declare function renderStepTimings(report: ExecuteReport): string;
|
|
55
65
|
//# sourceMappingURL=execute.d.ts.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Ask } from './prompts.ts';
|
|
2
|
-
import type { CliFrontend } from './types.ts';
|
|
3
2
|
/** The managers this CLI builds workspaces with. */
|
|
4
3
|
export type CliPackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
|
|
5
4
|
export declare function isCliPackageManager(name: string): name is CliPackageManager;
|
|
@@ -24,14 +23,7 @@ export type Probe = (name: CliPackageManager) => string | undefined;
|
|
|
24
23
|
* launched this command. The launcher's own version is used instead of probing it again.
|
|
25
24
|
*/
|
|
26
25
|
export declare function listInstalledManagers(launcher: Launcher | undefined, probe: Probe): InstalledManager[];
|
|
27
|
-
|
|
28
|
-
* Why this manager cannot create the chosen workspace, or undefined when it can.
|
|
29
|
-
* Both known cases fail inside create-nx-workspace's own install of
|
|
30
|
-
* Nx's react template, so a pick is never allowed — an unknown version is not held
|
|
31
|
-
* against the manager.
|
|
32
|
-
*/
|
|
33
|
-
export declare function managerIncompatibility(manager: InstalledManager, frontend: CliFrontend): string | undefined;
|
|
34
|
-
export type PackageManagerSource = 'flag' | 'launcher' | 'default' | 'asked' | 'only-installed' | 'only-compatible';
|
|
26
|
+
export type PackageManagerSource = 'flag' | 'launcher' | 'default' | 'asked' | 'only-installed';
|
|
35
27
|
export interface PackageManagerResolution {
|
|
36
28
|
packageManager: CliPackageManager;
|
|
37
29
|
source: PackageManagerSource;
|
|
@@ -42,17 +34,15 @@ export interface ChooseInput {
|
|
|
42
34
|
/** `--package-manager`, already validated by parse-args. */
|
|
43
35
|
flag: CliPackageManager | undefined;
|
|
44
36
|
userAgent: string | undefined;
|
|
45
|
-
frontend: CliFrontend;
|
|
46
37
|
/** stdin is a TTY — only then is a question asked. */
|
|
47
38
|
interactive: boolean;
|
|
48
39
|
probe: Probe;
|
|
49
40
|
}
|
|
50
41
|
/**
|
|
51
|
-
* Flag > (interactive) the question > launcher > npm.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* anything is created.
|
|
55
|
-
* refused the same way.
|
|
42
|
+
* Flag > (interactive) the question > launcher > npm. Every supported manager can create
|
|
43
|
+
* every workspace (npm 10 and yarn 1 included, measured 2026-10-25 on the empty-preset
|
|
44
|
+
* react-vite path and on next). Non-interactive runs launched by an unsupported manager are
|
|
45
|
+
* refused before anything is created.
|
|
56
46
|
*/
|
|
57
47
|
export declare function choosePackageManager(input: ChooseInput, ask: Ask, tell: (line: string) => void): Promise<PackageManagerResolution | {
|
|
58
48
|
error: string;
|
package/dist/cli/plan.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface StepContext {
|
|
|
35
35
|
listDir(relativePath: string): string[];
|
|
36
36
|
}
|
|
37
37
|
export interface PlanStep {
|
|
38
|
-
id: 'create-workspace' | 'install-plugin' | 'install-backend-plugin' | 'create-api-app' | 'run-init' | 'settle-build-approvals' | 'list-blocked-scripts' | 'install-ai-plugin';
|
|
38
|
+
id: 'create-workspace' | 'add-react-plugin' | 'create-web-app' | 'install-plugin' | 'install-backend-plugin' | 'create-api-app' | 'run-init' | 'settle-build-approvals' | 'list-blocked-scripts' | 'install-ai-plugin';
|
|
39
39
|
description: string;
|
|
40
40
|
command: string;
|
|
41
41
|
args: string[];
|
|
@@ -47,7 +47,7 @@ export interface PlanStep {
|
|
|
47
47
|
* @nx/<backend> version pin and --frontendProject are only knowable after
|
|
48
48
|
* step 1 ran). Returning { error } stops the plan before executing the
|
|
49
49
|
* step; { skip } records the step as satisfied without executing it
|
|
50
|
-
* (
|
|
50
|
+
* (express already declared, so there is nothing to align). */
|
|
51
51
|
finalizeArgs?: (ctx: StepContext) => string[] | {
|
|
52
52
|
error: string;
|
|
53
53
|
} | {
|
|
@@ -72,17 +72,6 @@ export interface CreatePlan {
|
|
|
72
72
|
* automatic commit — that stays outside what this CLI does). */
|
|
73
73
|
nextSteps: string[];
|
|
74
74
|
}
|
|
75
|
-
/** npm 10 (bundled with Node 22) crashes inside create-nx-workspace's own install
|
|
76
|
-
* of the react template's dependency tree — an arborist bug in its peer-set
|
|
77
|
-
* resolution, reproduced on every npm 10 release up to 10.9.9 and absent in npm 11
|
|
78
|
-
* (E2E CI finding, 2026-10-13). The next preset installs fine on npm 10
|
|
79
|
-
* (2026-10-14), so only react-vite + npm is affected: package-manager.ts never lets a
|
|
80
|
-
* known npm 10 pick it, and the create step keeps this hint for the case the version
|
|
81
|
-
* could not be read. */
|
|
82
|
-
export declare const NPM_10_CREATE_WORKSPACE_HINT: string;
|
|
83
|
-
/** yarn 1 cannot resolve the `workspace:*` packages Nx's react template declares
|
|
84
|
-
* (measured 2026-10-15); the same guard and fallback as npm 10 above. */
|
|
85
|
-
export declare const YARN_1_CREATE_WORKSPACE_HINT: string;
|
|
86
75
|
/** `pnpm nx g` / `npm exec nx -- g` — how the user runs a generator in the new workspace. */
|
|
87
76
|
export declare function nxGenerateCommand(pm: CliPackageManager): string;
|
|
88
77
|
export declare function isYarnBerry(ctx: Pick<StepContext, 'readFile'>): boolean;
|
package/dist/index.js
CHANGED
|
@@ -11,9 +11,9 @@ var AI_HOST_CHOICES = [
|
|
|
11
11
|
];
|
|
12
12
|
var REVIEW_PROVIDER_CHOICES = [
|
|
13
13
|
{ id: "claude", label: "Claude" },
|
|
14
|
-
{ id: "gemini", label: "Gemini" },
|
|
15
|
-
{ id: "gpt", label: "GPT" },
|
|
16
|
-
{ id: "cursor", label: "Cursor" }
|
|
14
|
+
{ id: "gemini", label: "Gemini (beta)" },
|
|
15
|
+
{ id: "gpt", label: "GPT (beta)" },
|
|
16
|
+
{ id: "cursor", label: "Cursor (beta)" }
|
|
17
17
|
];
|
|
18
18
|
var REVIEW_PROVIDER_VALUES = REVIEW_PROVIDER_CHOICES.map((provider) => provider.id);
|
|
19
19
|
var AI_HOST_VALUES = AI_HOST_CHOICES.map((host) => host.id);
|
|
@@ -38,12 +38,6 @@ var PLUGIN_VALUES = PLUGIN_CHOICES.map((plugin) => plugin.id);
|
|
|
38
38
|
// src/cli/parse-args.ts
|
|
39
39
|
import { parseArgs as parseNodeArgs } from "node:util";
|
|
40
40
|
|
|
41
|
-
// src/cli/preflight.ts
|
|
42
|
-
function parseMajor(version) {
|
|
43
|
-
const match = /^\s*v?(\d+)\./.exec(version ?? "");
|
|
44
|
-
return match ? Number(match[1]) : void 0;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
41
|
// src/cli/package-manager.ts
|
|
48
42
|
var SUPPORTED = ["npm", "pnpm", "yarn", "bun"];
|
|
49
43
|
function isCliPackageManager(name) {
|
|
@@ -385,9 +379,7 @@ function renderVersion() {
|
|
|
385
379
|
|
|
386
380
|
// src/cli/plan.ts
|
|
387
381
|
var DEFAULT_PLUGIN_SOURCE = `@clearlydev/nx-archweave@^${CLI_VERSION}`;
|
|
388
|
-
var VALIDATED_CREATE_NX_WORKSPACE_VERSION = "
|
|
389
|
-
var NPM_10_CREATE_WORKSPACE_HINT = `If the npm error above is "Cannot read properties of null (reading 'edgesOut')", that is a known npm 10 bug with the current Nx workspace dependencies \u2014 upgrade npm (npm install -g npm@11), or, if pnpm is installed, re-run with --package-manager=pnpm.`;
|
|
390
|
-
var YARN_1_CREATE_WORKSPACE_HINT = `If the yarn error above is "Couldn't find package \u2026@workspace:*", that is yarn 1 (classic), which cannot resolve the workspace:* packages in Nx's react template \u2014 use yarn 2+ (corepack enable, then yarn set version stable) or re-run with another package manager.`;
|
|
382
|
+
var VALIDATED_CREATE_NX_WORKSPACE_VERSION = "24.0.1";
|
|
391
383
|
function nxArgs(pm) {
|
|
392
384
|
return pm === "npm" ? ["exec", "nx", "--"] : ["nx"];
|
|
393
385
|
}
|
|
@@ -404,10 +396,11 @@ function isYarnBerry(ctx) {
|
|
|
404
396
|
return ctx.readFile(".yarnrc.yml") !== void 0 || /^__metadata:/m.test(ctx.readFile("yarn.lock") ?? "");
|
|
405
397
|
}
|
|
406
398
|
var YARN_PLAN_ENV = { YARN_ENABLE_IMMUTABLE_INSTALLS: "false" };
|
|
399
|
+
var NO_E2E = ["--e2eTestRunner=none"];
|
|
407
400
|
function createWorkspaceSourceArgs(frontend, backend) {
|
|
408
|
-
if (frontend === "react-vite") return ["--
|
|
409
|
-
if (frontend === "next") return ["--preset=next"];
|
|
410
|
-
return [`--preset=${backend}`, "--appName=api"];
|
|
401
|
+
if (frontend === "react-vite") return ["--preset=empty"];
|
|
402
|
+
if (frontend === "next") return ["--preset=next", "--appName=web", ...NO_E2E];
|
|
403
|
+
return [`--preset=${backend}`, "--appName=api", ...NO_E2E, "--unitTestRunner=vitest"];
|
|
411
404
|
}
|
|
412
405
|
function buildNextSteps(input) {
|
|
413
406
|
const lines = [
|
|
@@ -432,7 +425,7 @@ function buildNextSteps(input) {
|
|
|
432
425
|
function buildPlan(input) {
|
|
433
426
|
const pluginSource = input.pluginSource ?? DEFAULT_PLUGIN_SOURCE;
|
|
434
427
|
const workspaceSource = createWorkspaceSourceArgs(input.frontend, input.backend);
|
|
435
|
-
const aiAgentArgs = input.aiHosts.map((host) => `--aiAgents=${AI_HOST_CHOICES.find((choice) => choice.id === host)?.nxAgent ?? host}`);
|
|
428
|
+
const aiAgentArgs = input.aiHosts.length === 0 ? ["--aiAgents=none"] : input.aiHosts.map((host) => `--aiAgents=${AI_HOST_CHOICES.find((choice) => choice.id === host)?.nxAgent ?? host}`);
|
|
436
429
|
const pm = input.packageManager;
|
|
437
430
|
const nxPrefix = nxArgs(pm);
|
|
438
431
|
const initArgs = [
|
|
@@ -448,8 +441,10 @@ function buildPlan(input) {
|
|
|
448
441
|
// Step 2 (the plugin install) always dirties the fresh workspace's tree
|
|
449
442
|
// (package.json + lockfile), so init's clean-tree refusal would otherwise
|
|
450
443
|
// always fire here. This is orchestration knowledge, not policy: the CLI
|
|
451
|
-
// knows what its own previous step did.
|
|
452
|
-
|
|
444
|
+
// knows what its own previous step did. For the same reason the report's note about
|
|
445
|
+
// reverting earlier user changes doesn't apply: the only changes are that install's.
|
|
446
|
+
"--allowDirty=true",
|
|
447
|
+
"--dirtyFromCaller=true"
|
|
453
448
|
];
|
|
454
449
|
if (input.review !== void 0) {
|
|
455
450
|
initArgs.push(`--review=${input.review}`);
|
|
@@ -484,10 +479,9 @@ function buildPlan(input) {
|
|
|
484
479
|
"--nxCloud=skip",
|
|
485
480
|
"--interactive=false"
|
|
486
481
|
],
|
|
487
|
-
cwd: null
|
|
488
|
-
...input.frontend === "react-vite" && pm === "npm" ? { failureHint: NPM_10_CREATE_WORKSPACE_HINT } : {},
|
|
489
|
-
...input.frontend === "react-vite" && pm === "yarn" ? { failureHint: YARN_1_CREATE_WORKSPACE_HINT } : {}
|
|
482
|
+
cwd: null
|
|
490
483
|
},
|
|
484
|
+
...input.frontend === "react-vite" ? reactAppSteps(input.name, pm) : [],
|
|
491
485
|
{
|
|
492
486
|
id: "install-plugin",
|
|
493
487
|
description: `Install ${pluginSource}`,
|
|
@@ -604,11 +598,31 @@ function listBlockedScriptsStep(workspaceName) {
|
|
|
604
598
|
failureHint: `The workspace itself is complete \u2014 every earlier step finished; only listing the blocked install scripts failed. Inside "${workspaceName}", run bun pm untrusted to see them.`
|
|
605
599
|
};
|
|
606
600
|
}
|
|
601
|
+
function reactAppSteps(workspaceName, pm) {
|
|
602
|
+
return [
|
|
603
|
+
{
|
|
604
|
+
id: "add-react-plugin",
|
|
605
|
+
description: "Add @nx/react with nx add (at the workspace nx version)",
|
|
606
|
+
command: pm,
|
|
607
|
+
args: [...nxArgs(pm), "add", "@nx/react"],
|
|
608
|
+
cwd: workspaceName
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
id: "create-web-app",
|
|
612
|
+
description: "Create the React + Vite app in apps/web with the official @nx/react generator",
|
|
613
|
+
command: pm,
|
|
614
|
+
args: [...nxArgs(pm), "g", "@nx/react:app", "apps/web", ...REACT_APP_OPTIONS],
|
|
615
|
+
cwd: workspaceName
|
|
616
|
+
}
|
|
617
|
+
];
|
|
618
|
+
}
|
|
619
|
+
var REACT_APP_OPTIONS = ["--bundler=vite", "--linter=eslint", "--style=css", "--unitTestRunner=vitest", ...NO_E2E, "--routing=false"];
|
|
620
|
+
var API_APP_OPTIONS = ["--unitTestRunner=vitest", ...NO_E2E];
|
|
607
621
|
function backendSteps(backend, workspaceName, pm, initCommand) {
|
|
608
622
|
const nxG = nxGenerateCommand(pm);
|
|
609
|
-
const
|
|
623
|
+
const apiExists = (ctx) => ctx.listDir("apps").includes("api");
|
|
610
624
|
const standaloneError = `this workspace has no apps/ directory \u2014 CLI backend wiring needs the frontend under apps/. Create the api app with the official @nx generator, then run \`${nxG} @clearlydev/nx-archweave:add-backend\` yourself. Finish setup inside "${workspaceName}" with: ${initCommand}`;
|
|
611
|
-
const
|
|
625
|
+
const apiExistsError = `the workspace already has an apps/api \u2014 wire it with \`${nxG} @clearlydev/nx-archweave:add-backend --backend=${backend}\`, or remove it and run the official generator yourself. Either way, finish setup inside "${workspaceName}" with: ${initCommand}`;
|
|
612
626
|
return [
|
|
613
627
|
{
|
|
614
628
|
id: "install-backend-plugin",
|
|
@@ -618,9 +632,7 @@ function backendSteps(backend, workspaceName, pm, initCommand) {
|
|
|
618
632
|
cwd: workspaceName,
|
|
619
633
|
finalizeArgs: (ctx) => {
|
|
620
634
|
if (ctx.listDir("apps").length === 0) return { error: standaloneError };
|
|
621
|
-
if (
|
|
622
|
-
return backend === "express" ? { skip: "the workspace template already ships the Express api" } : { error: nestConflictError };
|
|
623
|
-
}
|
|
635
|
+
if (apiExists(ctx)) return { error: apiExistsError };
|
|
624
636
|
const raw = ctx.readFile("package.json");
|
|
625
637
|
if (raw === void 0) return { error: "could not read the created workspace's package.json to pin the @nx version" };
|
|
626
638
|
try {
|
|
@@ -639,14 +651,12 @@ function backendSteps(backend, workspaceName, pm, initCommand) {
|
|
|
639
651
|
command: pm,
|
|
640
652
|
// Since Nx 20 the positional is a directory relative to the workspace root —
|
|
641
653
|
// a bare `api` would land at the root instead of beside the frontend.
|
|
642
|
-
args: [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api"],
|
|
654
|
+
args: [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", ...API_APP_OPTIONS],
|
|
643
655
|
cwd: workspaceName,
|
|
644
656
|
finalizeArgs: (ctx) => {
|
|
645
657
|
const all = ctx.listDir("apps");
|
|
646
658
|
if (all.length === 0) return { error: standaloneError };
|
|
647
|
-
if (all.includes("api")) {
|
|
648
|
-
return backend === "express" ? { skip: "the workspace template already ships the Express api" } : { error: nestConflictError };
|
|
649
|
-
}
|
|
659
|
+
if (all.includes("api")) return { error: apiExistsError };
|
|
650
660
|
const apps = all.filter((name) => name !== "api" && name !== "e2e" && !name.endsWith("-e2e"));
|
|
651
661
|
if (apps.length === 0) return { error: "no frontend app directory found under apps/ in the created workspace" };
|
|
652
662
|
if (apps.length > 1) return { error: `multiple app directories found under apps/ (${apps.join(", ")}) \u2014 cannot pick --frontendProject` };
|
|
@@ -662,7 +672,7 @@ function backendSteps(backend, workspaceName, pm, initCommand) {
|
|
|
662
672
|
}
|
|
663
673
|
return apps[0];
|
|
664
674
|
})();
|
|
665
|
-
return [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", `--frontendProject=${frontendProject}
|
|
675
|
+
return [...nxArgs(pm), "g", `@nx/${backend}:application`, "apps/api", `--frontendProject=${frontendProject}`, ...API_APP_OPTIONS];
|
|
666
676
|
}
|
|
667
677
|
}
|
|
668
678
|
];
|
|
@@ -768,7 +778,7 @@ function createStepContext(workspacePath) {
|
|
|
768
778
|
}
|
|
769
779
|
};
|
|
770
780
|
}
|
|
771
|
-
function executePlan(plan, exec, invokedFromDir, ctxFactory = createStepContext) {
|
|
781
|
+
function executePlan(plan, exec, invokedFromDir, ctxFactory = createStepContext, now = Date.now) {
|
|
772
782
|
const completed = [];
|
|
773
783
|
for (const step of plan.steps) {
|
|
774
784
|
const cwd = step.cwd === null ? invokedFromDir : join2(invokedFromDir, step.cwd);
|
|
@@ -788,16 +798,18 @@ function executePlan(plan, exec, invokedFromDir, ctxFactory = createStepContext)
|
|
|
788
798
|
}
|
|
789
799
|
args = finalized;
|
|
790
800
|
}
|
|
801
|
+
const startedAt = now();
|
|
791
802
|
const result = exec(step.command, args, plan.env ? { cwd, env: plan.env } : { cwd });
|
|
803
|
+
const durationMs = now() - startedAt;
|
|
792
804
|
if (result.status !== 0) {
|
|
793
805
|
if (step.optional) {
|
|
794
806
|
const optionalFailure = result.status === null ? "it did not start or was stopped (no exit code)" : `exit code: ${result.status} (its own error output is printed above)`;
|
|
795
|
-
completed.push({ step, result, args, optionalFailure });
|
|
807
|
+
completed.push({ step, result, args, optionalFailure, durationMs });
|
|
796
808
|
continue;
|
|
797
809
|
}
|
|
798
|
-
return { ok: false, completed, failedStep: step, failedResult: result };
|
|
810
|
+
return { ok: false, completed, failedStep: step, failedResult: result, failedArgs: args, failedDurationMs: durationMs };
|
|
799
811
|
}
|
|
800
|
-
completed.push({ step, result, args });
|
|
812
|
+
completed.push({ step, result, args, durationMs });
|
|
801
813
|
}
|
|
802
814
|
return { ok: true, completed };
|
|
803
815
|
}
|
|
@@ -833,9 +845,43 @@ function renderExecuteReport(report, plan, workspaceName) {
|
|
|
833
845
|
...!report.finalizeError && failedStep.failureHint ? [` ${failedStep.failureHint}`] : [],
|
|
834
846
|
"",
|
|
835
847
|
`The "${workspaceName}" directory (if it was created) was left in place \u2014 create-nx-archweave never deletes it automatically.`,
|
|
836
|
-
`Inspect the error above
|
|
848
|
+
`Inspect the error above and fix the issue. Then either run the remaining steps below yourself, or remove the directory and start over.`,
|
|
849
|
+
"",
|
|
850
|
+
...renderRemainingSteps(report, plan, workspaceName)
|
|
837
851
|
].join("\n");
|
|
838
852
|
}
|
|
853
|
+
function renderStepTimings(report) {
|
|
854
|
+
const rows = report.completed.filter((c) => c.durationMs !== void 0).map((c) => [c.step.id, c.durationMs]);
|
|
855
|
+
if (!report.ok && report.failedDurationMs !== void 0) rows.push([`${report.failedStep.id} (failed)`, report.failedDurationMs]);
|
|
856
|
+
return ["Step timings:", ...rows.map(([id, ms]) => ` ${(ms / 1e3).toFixed(1).padStart(7)}s ${id}`)].join("\n");
|
|
857
|
+
}
|
|
858
|
+
function renderRemainingSteps(report, plan, workspaceName) {
|
|
859
|
+
const start = plan.steps.indexOf(report.failedStep);
|
|
860
|
+
const remaining = start === -1 ? [report.failedStep] : plan.steps.slice(start);
|
|
861
|
+
const lines = [`Remaining steps, in order (printed only \u2014 create-nx-archweave does not run them again):`];
|
|
862
|
+
const env = Object.entries(plan.env ?? {});
|
|
863
|
+
if (env.length > 0) lines.push(` Run them with ${env.map(([key, value]) => `${key}=${value}`).join(" ")} set, as the CLI does.`);
|
|
864
|
+
remaining.forEach((step, index) => {
|
|
865
|
+
const ranArgs = step === report.failedStep ? report.failedArgs : void 0;
|
|
866
|
+
const args = ranArgs ?? step.args;
|
|
867
|
+
const where = step.cwd === null ? "in the directory you ran create-nx-archweave from" : `inside "${workspaceName}"`;
|
|
868
|
+
lines.push(` ${index + 1}. ${step.description}${step.optional ? " (optional)" : ""} \u2014 ${where}:`);
|
|
869
|
+
lines.push(` ${[step.command, ...args].map(shellQuote).join(" ")}`);
|
|
870
|
+
if (step.finalizeArgs && ranArgs === void 0) {
|
|
871
|
+
lines.push(" (the CLI works out this step's exact arguments when it runs \u2014 check them against the workspace first)");
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
return lines;
|
|
875
|
+
}
|
|
876
|
+
function shellQuote(word) {
|
|
877
|
+
return /^[\w@%+=:,./^~-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/cli/preflight.ts
|
|
881
|
+
function parseMajor(version) {
|
|
882
|
+
const match = /^\s*v?(\d+)\./.exec(version ?? "");
|
|
883
|
+
return match ? Number(match[1]) : void 0;
|
|
884
|
+
}
|
|
839
885
|
export {
|
|
840
886
|
AI_HOST_CHOICES,
|
|
841
887
|
AI_HOST_PROBES,
|
|
@@ -845,14 +891,12 @@ export {
|
|
|
845
891
|
DEFAULT_AI_HOSTS,
|
|
846
892
|
FRONTEND_VALUES,
|
|
847
893
|
LOCALE_VALUES,
|
|
848
|
-
NPM_10_CREATE_WORKSPACE_HINT,
|
|
849
894
|
PLUGIN_CHOICES,
|
|
850
895
|
PLUGIN_VALUES,
|
|
851
896
|
PNPM_PENDING_BUILD_PLACEHOLDER,
|
|
852
897
|
PRESET_VALUES,
|
|
853
898
|
REVIEW_PROVIDER_CHOICES,
|
|
854
899
|
REVIEW_PROVIDER_VALUES,
|
|
855
|
-
YARN_1_CREATE_WORKSPACE_HINT,
|
|
856
900
|
aiHostsQuestion,
|
|
857
901
|
buildPlan,
|
|
858
902
|
childEnv,
|
|
@@ -869,6 +913,7 @@ export {
|
|
|
869
913
|
parseReviewProviderAnswer,
|
|
870
914
|
pendingBuildDecisions,
|
|
871
915
|
renderExecuteReport,
|
|
916
|
+
renderStepTimings,
|
|
872
917
|
renderUsage,
|
|
873
918
|
renderVersion,
|
|
874
919
|
reviewProviderConflict,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nx-archweave",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Create a ready-to-work Nx workspace in one command: FSD Lite lint rule, boundaries, AI agent rules, templates and AI PR review",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "clearlydev (https://github.com/clearlydev)",
|
|
7
7
|
"engines": {
|