@mnci/cli 4.0.5 → 4.0.7

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.
Files changed (3) hide show
  1. package/dist/cli.js +2075 -2036
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -38,7 +38,7 @@ module.exports = __toCommonJS(cli_exports);
38
38
  var import_commander = require("commander");
39
39
 
40
40
  // src/commands/add.ts
41
- var import_node_path10 = require("path");
41
+ var import_node_path11 = require("path");
42
42
  var import_prompts5 = require("@inquirer/prompts");
43
43
 
44
44
  // src/nx.ts
@@ -192,2167 +192,2206 @@ function assertValidProjectName(name, label) {
192
192
  }
193
193
 
194
194
  // src/commands/add/flutter.ts
195
- var import_node_path3 = require("path");
195
+ var import_node_path4 = require("path");
196
196
 
197
197
  // src/commands/add/shared.ts
198
+ var import_node_fs3 = require("fs");
199
+ var import_node_path3 = require("path");
200
+
201
+ // src/overlay.ts
198
202
  var import_node_fs2 = require("fs");
199
203
  var import_node_path2 = require("path");
200
- function hasPlugin(workspaceRoot, packageName) {
201
- const manifest = readJson((0, import_node_path2.join)(workspaceRoot, "package.json"));
202
- const installed = { ...manifest.dependencies, ...manifest.devDependencies };
203
- return Object.hasOwn(installed, packageName);
204
- }
205
- function ensurePlugin(workspaceRoot, packageName) {
206
- if (hasPlugin(workspaceRoot, packageName)) {
207
- return;
204
+ var DEFAULT_STACK = { testRunner: "jest" };
205
+ var TS_COMPILER_DEPENDENCIES = {
206
+ "@typescript/native": "npm:typescript@^7.0.2",
207
+ typescript: "npm:@typescript/typescript6@^6.0.2"
208
+ };
209
+ function registryUrl(registry) {
210
+ if (registry.kind === "azure-artifacts") {
211
+ return `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}/npm/registry/`;
208
212
  }
209
- logger.step(`Installing Nx plugin ${packageName}`);
210
- runNx(["add", packageName], workspaceRoot);
211
- }
212
- function markPrivate(manifestPath) {
213
- const manifest = readJson(manifestPath);
214
- writeFileEnsured(manifestPath, toJson({ ...manifest, private: true }));
215
- }
216
- function markPublic(manifestPath) {
217
- const manifest = readJson(manifestPath);
218
- writeFileEnsured(manifestPath, toJson({ ...manifest, publishConfig: { access: "public" } }));
213
+ return void 0;
219
214
  }
220
- var WRONG_TYPES_PATH = "./dist/index.esm.d.ts";
221
- var ACTUAL_TYPES_PATH = "./dist/src/index.d.ts";
222
- var DECLARATION_MAP_EXCLUSION = "!**/*.d.ts.map";
223
- function repairPublishableManifest(manifestPath) {
224
- const manifest = readJson(manifestPath);
225
- if (manifest.types === WRONG_TYPES_PATH) {
226
- manifest.types = ACTUAL_TYPES_PATH;
227
- }
228
- const dot = manifest.exports?.["."];
229
- if (typeof dot === "object" && dot.types === WRONG_TYPES_PATH) {
230
- dot.types = ACTUAL_TYPES_PATH;
231
- }
232
- if (manifest.files && !manifest.files.includes(DECLARATION_MAP_EXCLUSION)) {
233
- manifest.files.push(DECLARATION_MAP_EXCLUSION);
215
+ function npmrcContent(registry, scope) {
216
+ if (registry.kind === "npm") {
217
+ return `; Publish authentication for the public npm registry.
218
+ ;
219
+ ; NODE_AUTH_TOKEN is exported by the generated CI's release step; an unset value
220
+ ; is harmless locally, so no token is needed for day-to-day work.
221
+ //registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN}
222
+
223
+ ; There is deliberately NO '${scope}:registry' line here. npmjs.org is already
224
+ ; the default registry, so routing the scope to it would change nothing \u2014 and
225
+ ; presenting that as protection against an accidental public publish would be
226
+ ; false, because the public registry is the intended target. (An earlier version
227
+ ; of this file made exactly that claim while emitting no routing line at all.)
228
+ ; To keep a scoped package off npmjs.org, use a private feed: generate with
229
+ ; --registry azure-artifacts, which does route the scope.
230
+ `;
234
231
  }
235
- writeFileEnsured(manifestPath, toJson(manifest));
232
+ const feedUrl = registryUrl(registry);
233
+ const feedKey = feedUrl.replace(/^https:/, "");
234
+ const feedShortKey = feedKey.replace("npm/registry/", "npm/");
235
+ return `; Publish + resolution routing for this workspace's own scope.
236
+ ;
237
+ ; '${scope}:registry' sends BOTH resolution and 'npm publish' of ${scope}/* to the
238
+ ; feed: npm prefers a scope's registry over the global one when publishing a
239
+ ; scoped package, so a ${scope}/* package cannot reach npmjs.org by accident.
240
+ ;
241
+ ; Only the scope is routed, on purpose. A global 'registry=' would send every
242
+ ; install through the feed as well, so 'npm ci' would need feed auth just to fetch
243
+ ; public packages.
244
+ ${scope}:registry=${feedUrl}
245
+
246
+ ; Feed credentials. PAT is the BASE64 value Azure Artifacts' "Connect to feed"
247
+ ; instructions give you, which is exactly what _password expects, so it is used
248
+ ; as-is. (twine wants the RAW token; the CI release step decodes it there.)
249
+ ; Azure ignores the username, and npm requires an email it never uses.
250
+ ;
251
+ ; NOT _authToken, and that is measured rather than assumed. The feed answers an
252
+ ; unauthenticated PUT to the publish endpoint with:
253
+ ; www-authenticate: Bearer authorization_uri=https://login.windows.net/<tenant>,
254
+ ; Basic realm="...", TFS-Federated
255
+ ; so its Bearer scheme wants an Entra ID access token, NOT a PAT. npm sends
256
+ ; _authToken verbatim as a Bearer header, so a PAT there is rejected with
257
+ ; "Unable to authenticate, your authentication token seems to be invalid". A PAT
258
+ ; authenticates through the Basic scheme, which is username/_password. Note the
259
+ ; Packaging REST API DOES accept a PAT as Bearer - do not generalise from it.
260
+ ;
261
+ ; Both path forms are keyed because npm walks only UP a URL when matching.
262
+ ${feedKey}:username=AzureArtifacts
263
+ ${feedKey}:_password=\${PAT}
264
+ ${feedKey}:email=npm-requires-this-and-never-uses-it
265
+ ${feedShortKey}:username=AzureArtifacts
266
+ ${feedShortKey}:_password=\${PAT}
267
+ ${feedShortKey}:email=npm-requires-this-and-never-uses-it
268
+ `;
236
269
  }
237
- function ensureAdmZip(workspaceRoot) {
238
- if (hasPlugin(workspaceRoot, "adm-zip")) {
239
- return;
240
- }
241
- logger.step("Installing the app packager (adm-zip)");
242
- if (runShell(
243
- "npm",
244
- ["install", "--save-dev", "adm-zip", "--no-audit", "--no-fund"],
245
- workspaceRoot
246
- ) !== 0) {
247
- throw new Error("npm install of adm-zip failed");
248
- }
270
+ function releaseConfig(ci) {
271
+ const githubReleases = ci === "github";
272
+ return {
273
+ projectsRelationship: "independent",
274
+ projects: ["packages/*", "python-packages/*", "!tag:type:go-lib"],
275
+ releaseTag: { pattern: "{projectName}@{version}" },
276
+ git: { commit: false, tag: true, push: githubReleases },
277
+ version: {
278
+ conventionalCommits: true,
279
+ fallbackCurrentVersionResolver: "disk",
280
+ // Build only what is being released. Without this, @nx/js:lib's generator
281
+ // defaults the pre-version command to building EVERY project, so a broken
282
+ // (or merely slow) app build would block releasing unrelated packages.
283
+ // Set here at `new` time it wins: the generator only fills this in when
284
+ // absent (it spreads the existing release.version over its default). Both
285
+ // globs are listed; `nx run-many` no-ops cleanly when one matches nothing.
286
+ preVersionCommand: "npx nx run-many -t build --projects=packages/*,python-packages/*"
287
+ },
288
+ changelog: githubReleases ? {
289
+ workspaceChangelog: false,
290
+ projectChangelogs: { createRelease: "github", file: false }
291
+ } : { workspaceChangelog: false }
292
+ };
249
293
  }
250
- function defaultScope(workspaceRoot) {
251
- const { name } = readJson((0, import_node_path2.join)(workspaceRoot, "package.json"));
252
- const base = (name.startsWith("@") ? name.slice(1) : name).split("/", 1)[0];
253
- return `@${base}`;
294
+ var SYNC_CONFIG = { applyChanges: true };
295
+ var ESLINT_PLUGIN_CONFIG = {
296
+ plugin: "@nx/eslint/plugin",
297
+ options: { targetName: "lint" }
298
+ };
299
+ function pluginName(entry) {
300
+ return typeof entry === "string" ? entry : entry.plugin;
254
301
  }
255
- function addNxTargets(manifestPath, newTargets) {
256
- const manifest = fileExists(manifestPath) ? readJson(manifestPath) : {};
257
- const nx = manifest.nx ?? {};
258
- const targets = nx.targets ?? {};
259
- writeFileEnsured(
260
- manifestPath,
261
- toJson({ ...manifest, nx: { ...nx, targets: { ...targets, ...newTargets } } })
262
- );
302
+ function withEslintPlugin(nxJson) {
303
+ const plugins = nxJson.plugins ?? [];
304
+ const registered = plugins.some((entry) => pluginName(entry) === ESLINT_PLUGIN_CONFIG.plugin);
305
+ return registered ? { ...nxJson, plugins } : { ...nxJson, plugins: [...plugins, ESLINT_PLUGIN_CONFIG] };
263
306
  }
264
- var ESLINT_CONFIG_FILENAMES = [
265
- "eslint.config.js",
266
- "eslint.config.mjs",
267
- "eslint.config.cjs",
268
- "eslint.config.ts",
269
- "eslint.config.mts",
270
- "eslint.config.cts"
307
+ var SHARED_GLOBAL_INPUTS = [
308
+ "{workspaceRoot}/eslint.config.mjs",
309
+ "{workspaceRoot}/tsconfig.base.json",
310
+ "{workspaceRoot}/package.json"
271
311
  ];
272
- function removeGeneratedEslintConfig(workspaceRoot, projectRoot) {
273
- for (const filename of ESLINT_CONFIG_FILENAMES) {
274
- (0, import_node_fs2.rmSync)((0, import_node_path2.join)(workspaceRoot, projectRoot, filename), { force: true });
275
- }
276
- (0, import_node_fs2.rmSync)((0, import_node_path2.join)(workspaceRoot, ".vscode"), { recursive: true, force: true });
277
- }
278
- function findCodeWorkspaceFile(workspaceRoot) {
279
- try {
280
- const entry = (0, import_node_fs2.readdirSync)(workspaceRoot).find((file) => file.endsWith(".code-workspace"));
281
- return entry ? (0, import_node_path2.join)(workspaceRoot, entry) : void 0;
282
- } catch {
283
- return void 0;
284
- }
285
- }
286
- function projectTask(name, kind) {
287
- const script = `${name}:${kind}`;
288
- const base = { label: `${name}: ${kind}`, type: "npm", script, problemMatcher: [] };
289
- return kind === "start" ? { ...base, isBackground: true } : { ...base, group: kind };
290
- }
291
- function registerProjectCommands(workspaceRoot, name, commands) {
292
- const scripts = {
293
- [`${name}:qa`]: `nx run ${name}:lint && nx run ${name}:test`
294
- };
295
- if (commands.build) {
296
- scripts[`${name}:build`] = `nx run ${name}:build`;
297
- }
298
- if (commands.start) {
299
- scripts[`${name}:start`] = commands.start;
300
- }
301
- const manifestPath = (0, import_node_path2.join)(workspaceRoot, "package.json");
302
- const manifest = readJson(manifestPath);
303
- const existingScripts = manifest.scripts ?? {};
304
- writeFileEnsured(
305
- manifestPath,
306
- toJson({ ...manifest, scripts: { ...existingScripts, ...scripts } })
307
- );
308
- const codeWorkspacePath = findCodeWorkspaceFile(workspaceRoot);
309
- if (!codeWorkspacePath) {
310
- return;
311
- }
312
- const workspaceFile = readCodeWorkspace(codeWorkspacePath) ?? {};
313
- const label = (task) => task.label ?? "";
314
- const existingTasks = (workspaceFile.tasks?.tasks ?? []).filter(
315
- (task) => !label(task).startsWith(`${name}: `)
316
- );
317
- const newTasks = [
318
- projectTask(name, "qa"),
319
- ...commands.build ? [projectTask(name, "build")] : [],
320
- ...commands.start ? [projectTask(name, "start")] : []
321
- ];
322
- writeFileEnsured(
323
- codeWorkspacePath,
324
- toJson({
325
- ...workspaceFile,
326
- tasks: {
327
- version: workspaceFile.tasks?.version ?? "2.0.0",
328
- tasks: [...existingTasks, ...newTasks]
329
- }
330
- })
331
- );
332
- }
333
-
334
- // src/commands/add/flutter.ts
335
- function addProjectJsonTargets(projectJsonPath, newTargets) {
336
- const project = readJson(projectJsonPath);
337
- const targets = project.targets ?? {};
338
- writeFileEnsured(projectJsonPath, toJson({ ...project, targets: { ...targets, ...newTargets } }));
339
- }
340
- function flutterAppStartTarget(name) {
312
+ function withSharedGlobals(nxJson) {
313
+ const namedInputs = nxJson.namedInputs ?? {};
314
+ const existing = namedInputs.sharedGlobals ?? [];
315
+ const missing = SHARED_GLOBAL_INPUTS.filter((entry) => !existing.includes(entry));
341
316
  return {
342
- executor: "nx:run-commands",
343
- continuous: true,
344
- options: { command: "flutter run -d chrome", cwd: `apps/${name}` }
317
+ ...nxJson,
318
+ namedInputs: { ...namedInputs, sharedGlobals: [...existing, ...missing] }
345
319
  };
346
320
  }
347
- function ensureFlutter(workspaceRoot) {
348
- if (runShell("flutter", ["--version"], workspaceRoot) !== 0) {
349
- throw new Error(
350
- "Flutter not found. Install the Flutter SDK first (3.27+, for Dart 3.6+ pub workspaces): https://docs.flutter.dev/get-started/install"
351
- );
352
- }
353
- }
354
- function nxFlutterPluginSpec() {
355
- return process.env.MNCI_NX_FLUTTER_SPEC ?? "@mnci/nx-flutter";
321
+ function withReleaseConfig(nxJson, ci) {
322
+ return { ...nxJson, defaultBase: "main", release: releaseConfig(ci) };
356
323
  }
357
- function ensureNxFlutterPlugin(workspaceRoot) {
358
- if (hasPlugin(workspaceRoot, "@mnci/nx-flutter")) {
359
- return;
360
- }
361
- const spec = nxFlutterPluginSpec();
362
- logger.step(`Installing the Flutter toolchain plugin (${spec})`);
363
- if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
364
- throw new Error("npm install of @mnci/nx-flutter failed");
365
- }
366
- }
367
- function prepareFlutter(workspaceRoot) {
368
- ensureFlutter(workspaceRoot);
369
- ensureNxFlutterPlugin(workspaceRoot);
370
- }
371
- function addFlutterApp(workspaceRoot, name) {
372
- prepareFlutter(workspaceRoot);
373
- ensureAdmZip(workspaceRoot);
374
- runNx(["g", "@mnci/nx-flutter:application", name, "--no-interactive"], workspaceRoot);
375
- addProjectJsonTargets((0, import_node_path3.join)(workspaceRoot, "apps", name, "project.json"), {
376
- start: flutterAppStartTarget(name)
377
- });
378
- registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
379
- }
380
- function addFlutterLib(workspaceRoot, name) {
381
- prepareFlutter(workspaceRoot);
382
- runNx(["g", "@mnci/nx-flutter:library", name, "--no-interactive"], workspaceRoot);
383
- registerProjectCommands(workspaceRoot, name, { build: false });
384
- }
385
- function addFlutterInternalLib(workspaceRoot, name) {
386
- prepareFlutter(workspaceRoot);
387
- runNx(["g", "@mnci/nx-flutter:internal-library", name, "--no-interactive"], workspaceRoot);
388
- registerProjectCommands(workspaceRoot, name, { build: false });
389
- }
390
-
391
- // src/commands/add/go.ts
392
- var import_node_fs3 = require("fs");
393
- var import_node_path4 = require("path");
394
- function ensureGo(workspaceRoot) {
395
- if (runShell("go", ["version"], workspaceRoot) !== 0) {
396
- throw new Error("Go not found. Install Go 1.21+ first: https://go.dev/dl/");
397
- }
398
- }
399
- function warnIfNoGolangciLint(workspaceRoot) {
400
- if (runShell("golangci-lint", ["--version"], workspaceRoot) !== 0) {
401
- logger.warn(
402
- "golangci-lint not found \u2014 the generated lint target needs it. Install: https://golangci-lint.run/welcome/install/"
403
- );
404
- }
405
- }
406
- function nxGoPluginSpec() {
407
- return process.env.MNCI_NX_GO_SPEC ?? "@nx-go/nx-go";
408
- }
409
- function ensureNxGoPlugin(workspaceRoot) {
410
- if (hasPlugin(workspaceRoot, "@nx-go/nx-go")) {
411
- return;
412
- }
413
- const spec = nxGoPluginSpec();
414
- logger.step(`Installing the Go toolchain plugin (${spec})`);
415
- if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
416
- throw new Error("npm install of @nx-go/nx-go failed");
417
- }
324
+ var COMMITLINT_CONFIG = `export default { extends: ['@commitlint/config-conventional'] }
325
+ `;
326
+ var COMMIT_MSG_HOOK = `npx --no -- commitlint --edit "$1"
327
+ `;
328
+ var ESLINT_CONFIG_VERSION = "^0.1.0";
329
+ function eslintConfigSpec() {
330
+ return process.env.MNCI_ESLINT_CONFIG_SPEC ?? ESLINT_CONFIG_VERSION;
418
331
  }
419
- function ensureGoModule(workspaceRoot) {
420
- if (fileExists((0, import_node_path4.join)(workspaceRoot, "go.mod"))) {
421
- return;
422
- }
423
- logger.step("Bootstrapping the workspace Go module (single root go.mod)");
424
- runNx(["g", "@nx-go/nx-go:init", "--no-interactive"], workspaceRoot);
425
- runNx(["g", "@nx-go/nx-go:convert-to-one-mod", "--no-interactive"], workspaceRoot);
332
+ var ESLINT_VERSION = "^10.8.0";
333
+ function devcontainerJson(workspaceName) {
334
+ return `${toJson({
335
+ name: workspaceName,
336
+ image: `mcr.microsoft.com/devcontainers/typescript-node:${NODE_VERSION}-bookworm`,
337
+ features: {
338
+ "ghcr.io/devcontainers/features/python:1": { version: "3.12" },
339
+ "ghcr.io/devcontainers/features/go:1": { version: "latest" }
340
+ },
341
+ // `npm ci` first: every guard after it runs through the workspace's own
342
+ // scripts and Nx, which do not exist until the install completes.
343
+ postCreateCommand: [
344
+ // Same pin the workflow applies, for the same reason — a container whose
345
+ // npm major differs from CI's resolves dependencies differently, which is
346
+ // precisely the drift this file exists to remove.
347
+ `npm install -g npm@${NPM_VERSION}`,
348
+ "npm ci",
349
+ "npm run python:install",
350
+ GOLANGCI_LINT_INSTALL_GUARD,
351
+ FLUTTER_SDK_INSTALL_GUARD
352
+ ].join(" && "),
353
+ // The same recommendations the `.code-workspace` file carries, so opening
354
+ // the folder in a container suggests the identical toolset.
355
+ customizations: {
356
+ vscode: { extensions: VSCODE_RECOMMENDED_EXTENSIONS }
357
+ }
358
+ })}
359
+ `;
426
360
  }
427
- function goModulePath(workspaceRoot) {
428
- try {
429
- const contents = (0, import_node_fs3.readFileSync)((0, import_node_path4.join)(workspaceRoot, "go.mod"), "utf8");
430
- return /^module\s+(\S+)/m.exec(contents)?.[1];
431
- } catch {
432
- return void 0;
361
+ var ROOT_LINT_TARGET = {
362
+ executor: "nx:run-commands",
363
+ cache: true,
364
+ options: {
365
+ command: [
366
+ // `--cache` is the one ESLint speed option that actually pays here:
367
+ // measured best-of-3 on this repo, an unchanged re-run drops from 9,546ms
368
+ // to 2,835ms (3.4x). `--concurrency=auto` was measured too and REJECTED —
369
+ // it came out 8% SLOWER, because per-worker TypeScript type-service
370
+ // startup costs more than the parallelism saves. The cache file is
371
+ // per-machine and gitignored.
372
+ "eslint . --cache",
373
+ '--ignore-pattern "apps/**"',
374
+ '--ignore-pattern "libs/**"',
375
+ '--ignore-pattern "packages/**"',
376
+ '--ignore-pattern "python-packages/**"',
377
+ "--ignore-pattern package-lock.json"
378
+ ].join(" "),
379
+ cwd: "."
433
380
  }
434
- }
435
- function addProjectJsonTargets2(projectJsonPath, newTargets) {
436
- const project = readJson(projectJsonPath);
437
- const targets = project.targets ?? {};
438
- writeFileEnsured(projectJsonPath, toJson({ ...project, targets: { ...targets, ...newTargets } }));
439
- }
440
- function goTestTarget() {
441
- return { executor: "@nx-go/nx-go:test" };
442
- }
443
- function goLintTarget() {
444
- return {
445
- executor: "@nx-go/nx-go:lint",
446
- parallelism: false,
447
- options: { linter: "golangci-lint", args: ["run"] }
448
- };
449
- }
450
- function goBuildTarget(name) {
451
- return {
452
- executor: "@nx-go/nx-go:build",
453
- outputs: [`{workspaceRoot}/dist/apps/${name}`],
454
- options: { outputPath: `../../dist/apps/${name}/${name}` }
455
- };
456
- }
457
- function goPackageTarget(tag, name) {
458
- const zip = `dist/drop/${tag}-${name}.zip`;
459
- const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFolder('dist/apps/${name}');z.writeZip('${zip}')"`;
460
- return {
461
- executor: "nx:run-commands",
462
- dependsOn: ["build"],
463
- outputs: [`{workspaceRoot}/${zip}`],
464
- options: { command }
381
+ };
382
+ function reactExpressPeerOverride(manifest) {
383
+ const declared = {
384
+ ...manifest.dependencies,
385
+ ...manifest.devDependencies
465
386
  };
387
+ return declared.express === void 0 ? {} : { "@nx/react": { express: "$express" } };
466
388
  }
467
- function goStartTarget(name) {
389
+ var ESLINT_PEER_OVERRIDES = {
390
+ "eslint-plugin-jsx-a11y": { eslint: "$eslint" },
391
+ // Scoped to EACH named parent, not top-level and not `nx` alone.
392
+ //
393
+ // `nx` alone was the first attempt and it did not work: the e2e's audit step
394
+ // still reported all six advisories in a freshly generated workspace, because
395
+ // `@nx/js`, `@nx/eslint`, `@nx/eslint-plugin` and `@nx/workspace` are their own
396
+ // dependents there, and an override scoped to `nx` never reaches a sibling's
397
+ // dependencies. Those four are exactly the packages npm audit named.
398
+ //
399
+ // Top-level would be worse than wrong. A tree with `minimatch@3` legitimately
400
+ // carries `brace-expansion@1.1.18` (and `@2.1.4` elsewhere) — this repo has
401
+ // both — and forcing those to v5 breaks them. So the blast radius is one
402
+ // dependency edge per named parent, and a test asserts there is no top-level
403
+ // entry.
404
+ nx: { "brace-expansion": "^5.0.9" },
405
+ "@nx/js": { "brace-expansion": "^5.0.9" },
406
+ "@nx/eslint": { "brace-expansion": "^5.0.9" },
407
+ "@nx/eslint-plugin": { "brace-expansion": "^5.0.9" },
408
+ "@nx/workspace": { "brace-expansion": "^5.0.9" },
409
+ // `postcss` → `nanoid` reaches a generated workspace through `@nx/rollup`,
410
+ // which every publishable `npm-lib` gets. GHSA-2v37-7h3g-55p8 is high, fixed in
411
+ // 3.3.18, and the fix is a patch — so unlike the entry above this one is not
412
+ // even a trade, just a version nobody had bumped yet.
413
+ postcss: { nanoid: "^3.3.18" }
414
+ };
415
+ var SECURITY_OVERRIDES = {
416
+ "brace-expansion@4.0.0 - 5.0.8": "^5.0.9"
417
+ };
418
+ function eslintToolchainDependencies(nxVersion) {
468
419
  return {
469
- executor: "nx:run-commands",
470
- continuous: true,
471
- options: { command: "go run .", cwd: `apps/${name}` }
420
+ eslint: ESLINT_VERSION,
421
+ "@nx/eslint": nxVersion,
422
+ "@nx/eslint-plugin": nxVersion,
423
+ "@mnci/eslint-config": eslintConfigSpec()
472
424
  };
473
425
  }
474
- function prepareGo(workspaceRoot) {
475
- ensureGo(workspaceRoot);
476
- warnIfNoGolangciLint(workspaceRoot);
477
- ensureNxGoPlugin(workspaceRoot);
478
- ensureGoModule(workspaceRoot);
479
- }
480
- function addGoApp(workspaceRoot, name) {
481
- prepareGo(workspaceRoot);
482
- ensureAdmZip(workspaceRoot);
483
- runNx(
484
- [
485
- "g",
486
- "@nx-go/nx-go:application",
487
- `apps/${name}`,
488
- `--name=${name}`,
489
- "--tags=type:go-app",
490
- "--no-interactive"
491
- ],
492
- workspaceRoot
493
- );
494
- addProjectJsonTargets2((0, import_node_path4.join)(workspaceRoot, "apps", name, "project.json"), {
495
- build: goBuildTarget(name),
496
- test: goTestTarget(),
497
- lint: goLintTarget(),
498
- package: goPackageTarget("go-app", name),
499
- start: goStartTarget(name)
500
- });
501
- registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
426
+ var RETIRED_FORMATTER_FILES = [
427
+ ".prettierrc",
428
+ ".prettierrc.json",
429
+ ".prettierrc.mjs",
430
+ ".prettierignore",
431
+ "oxlint.config.ts",
432
+ ".oxfmtrc.json"
433
+ ];
434
+ function withoutRetiredFormatterDependencies(devDeps) {
435
+ const retired = /* @__PURE__ */ new Set([
436
+ "prettier",
437
+ "eslint-config-prettier",
438
+ "oxlint",
439
+ "oxfmt",
440
+ "@mnci/oxlint-config"
441
+ ]);
442
+ return Object.fromEntries(Object.entries(devDeps).filter(([name]) => !retired.has(name)));
502
443
  }
503
- function addGoFunctionApp(workspaceRoot, name) {
504
- prepareGo(workspaceRoot);
505
- ensureAdmZip(workspaceRoot);
506
- runNx(
507
- [
508
- "g",
509
- "@nx-go/nx-go:application",
510
- `apps/${name}`,
511
- `--name=${name}`,
512
- "--tags=type:go-function-app",
513
- "--no-interactive"
514
- ],
515
- workspaceRoot
516
- );
517
- addProjectJsonTargets2((0, import_node_path4.join)(workspaceRoot, "apps", name, "project.json"), {
518
- build: goBuildTarget(name),
519
- test: goTestTarget(),
520
- lint: goLintTarget(),
521
- package: goPackageTarget("go-function-app", name)
522
- });
523
- registerProjectCommands(workspaceRoot, name, { build: true });
444
+ var ESLINT_BLOCK_INVENTORY = `// WHAT IS IN HERE. Each line is one config block, by the \`name\` it carries.
445
+ //
446
+ // mnci/ignores paths never linted (dist, coverage, .venv, \u2026)
447
+ // mnci/base JS/TS correctness \u2014 @eslint/js, eslint-plugin-unicorn,
448
+ // -promise, -n, -unused-imports
449
+ // typescript-eslint/* typescript-eslint's own recommended blocks
450
+ // mnci/typescript* TS rules on top of them, no type information needed
451
+ // mnci/type-aware* the rules that DO read types (no-floating-promises and
452
+ // friends), scoped to {apps,libs,packages}/*/src
453
+ // mnci/import-graph import cycles \u2014 eslint-plugin-import-x
454
+ // mnci/react JSX/TSX \u2014 @eslint-react/eslint-plugin,
455
+ // eslint-plugin-react-hooks, -react-refresh, -jsx-a11y
456
+ // mnci/regexp* regex correctness \u2014 eslint-plugin-regexp
457
+ // mnci/json mnci/jsonc mnci/json5
458
+ // eslint-plugin-jsonc \u2014 comments are allowed in .jsonc
459
+ // and tsconfig.json, forbidden in plain .json
460
+ // mnci/yaml* eslint-plugin-yml \u2014 your CI pipeline files
461
+ // mnci/toml/base* eslint-plugin-toml, PARSER ONLY: a malformed
462
+ // pyproject.toml is a syntax error, nothing is styled
463
+ // mnci/markdown @eslint/markdown
464
+ // mnci/css @eslint/css
465
+ // mnci/html @html-eslint/eslint-plugin
466
+ // mnci/tests *.spec/*.test relaxations \u2014 eslint-plugin-jest
467
+ // (Vitest's globals too; the two stacks share them)
468
+ // mnci/nx-dependency-checks @nx/eslint-plugin, on publishable packages' manifests
469
+ // mnci/standard JavaScript Standard Style as ESLint rules \u2014 the
470
+ // whole formatting opinion. Composed LAST, on
471
+ // purpose: nothing may follow that disables it.
472
+ //
473
+ // To list them as ESLint actually resolves them: npx eslint --inspect-config
474
+ `;
475
+ var ESLINT_CONFIG = `import mnci from '@mnci/eslint-config'
476
+
477
+ ${ESLINT_BLOCK_INVENTORY}//
478
+ // TO OVERRIDE a rule, append a block AFTER the spread \u2014 later blocks win, so one
479
+ // of your own beats anything above it. Give it a name, so the inspector shows
480
+ // where the change came from:
481
+ //
482
+ // export default [
483
+ // ...mnci({ workspaceRoot: import.meta.dirname }),
484
+ // {
485
+ // name: 'local/legacy-app-allows-any',
486
+ // files: ['apps/legacy/**/*.ts'],
487
+ // rules: { '@typescript-eslint/no-explicit-any': 'off' }
488
+ // }
489
+ // ]
490
+ //
491
+ // Do NOT edit @mnci/eslint-config inside node_modules, and do not fork it: it is
492
+ // a dependency, so \`npm update\` brings rule fixes in the way it brings any
493
+ // other. An override here survives that; an edit to the package does not.
494
+ //
495
+ // FORMATTING IS LINTING HERE. There is no Prettier, no oxfmt and no
496
+ // \`format:check\` \u2014 \`npm run lint\` reports indentation, quotes and spacing as
497
+ // ordinary errors, and \`npm run format\` is \`eslint . --fix\`. So do not add a
498
+ // formatter: whichever one you pick will disagree with the \`mnci/standard\`
499
+ // block above, and because a formatter runs on save it wins silently, leaving
500
+ // \`lint\` to fail on files you never edited by hand.
501
+ //
502
+ // That also applies to the editor. Installing a Prettier or oxfmt extension is
503
+ // enough on its own \u2014 neither needs a config file, and with none present they
504
+ // format against their own defaults (semicolons, double quotes), which is the
505
+ // inverse of Standard.
506
+ export default mnci({ workspaceRoot: import.meta.dirname })
507
+ `;
508
+ var VSCODE_RECOMMENDED_EXTENSIONS = [
509
+ "dbaeumer.vscode-eslint",
510
+ "nrwl.angular-console",
511
+ "firsttris.vscode-jest-runner"
512
+ ];
513
+ var FORMATTED_LANGUAGES = [
514
+ "javascript",
515
+ "javascriptreact",
516
+ "typescript",
517
+ "typescriptreact",
518
+ "json",
519
+ "jsonc",
520
+ "yaml",
521
+ "markdown",
522
+ "css",
523
+ "html",
524
+ // TOML is here for PARSING, not formatting, and the distinction is measured
525
+ // rather than assumed: `eslint --fix` leaves a badly-laid-out `.toml`
526
+ // byte-identical (the TOML block is `flat/base`, parser only — `flat/standard`
527
+ // reported six errors on the `pyproject.toml` mnci itself generates), but it
528
+ // DOES report `Parsing error: ...` on a malformed one.
529
+ //
530
+ // So the entry buys two things. `eslint.validate` gains toml, so a broken
531
+ // `pyproject.toml` is flagged in the editor rather than at the next CI run.
532
+ // And pinning the formatter to ESLint means format-on-save does nothing to the
533
+ // file instead of handing it to whichever TOML formatter the user happens to
534
+ // have installed — the same reasoning as every other entry here: nothing
535
+ // reformats against an opinion no gate checks.
536
+ //
537
+ // Impossible under the Prettier stack, which is why it is only here now:
538
+ // `npx prettier` on a `.toml` exits with "No parser could be inferred".
539
+ "toml"
540
+ ];
541
+ function vscodeSettings() {
542
+ const formatter = "dbaeumer.vscode-eslint";
543
+ const eslintLanguages = [...FORMATTED_LANGUAGES];
544
+ return {
545
+ "eslint.validate": eslintLanguages,
546
+ "editor.codeActionsOnSave": {
547
+ "source.fixAll.eslint": "explicit"
548
+ },
549
+ "editor.defaultFormatter": formatter,
550
+ "editor.formatOnSave": true,
551
+ // Every language spelled out, and the global default above is NOT enough on
552
+ // its own — which is the bug this fixes rather than a belt-and-braces habit.
553
+ //
554
+ // VS Code resolves a language-specific setting ahead of a general one, and it
555
+ // does that comparison BEFORE scope. So a `[typescript]` block in someone's
556
+ // USER settings — left over from any other project — outranks this file's
557
+ // workspace-level `editor.defaultFormatter`, and format-on-save quietly uses
558
+ // that other formatter instead — and nothing reports it, because the gate
559
+ // and the editor are looking at different tools. That is how it was found:
560
+ // reported from a real workspace, not deduced from the docs.
561
+ //
562
+ // Reported from a real workspace where `.ts` files were not being formatted
563
+ // while `.json`/`.jsonc`/`.yaml` were — exactly the three that had explicit
564
+ // entries here and nothing else did.
565
+ ...Object.fromEntries(
566
+ FORMATTED_LANGUAGES.map((language) => [
567
+ `[${language}]`,
568
+ { "editor.defaultFormatter": formatter }
569
+ ])
570
+ )
571
+ };
524
572
  }
525
- function addGoLib(workspaceRoot, name) {
526
- prepareGo(workspaceRoot);
527
- runNx(
528
- [
529
- "g",
530
- "@nx-go/nx-go:library",
531
- `packages/${name}`,
532
- `--name=${name}`,
533
- "--tags=type:go-lib",
534
- "--no-interactive"
535
- ],
536
- workspaceRoot
537
- );
538
- addProjectJsonTargets2((0, import_node_path4.join)(workspaceRoot, "packages", name, "project.json"), {
539
- test: goTestTarget(),
540
- lint: goLintTarget()
541
- });
542
- registerProjectCommands(workspaceRoot, name, { build: false });
543
- const module2 = goModulePath(workspaceRoot);
544
- if (module2) {
545
- logger.step(`Import it as ${module2}/packages/${name}`);
546
- }
573
+ var LAUNCH_CONFIG_PREFIX = "mnci: ";
574
+ var LAUNCH_CONFIGURATIONS = ["build", "test", "lint", "typecheck"];
575
+ function launchConfigurations(workspaceName) {
576
+ return LAUNCH_CONFIGURATIONS.map((script, index) => ({
577
+ type: "node-terminal",
578
+ request: "launch",
579
+ name: `${LAUNCH_CONFIG_PREFIX}${script}`,
580
+ command: `npm run ${script}`,
581
+ // Scoped by folder name rather than a bare ${workspaceFolder}: that variable is
582
+ // ambiguous once a second folder is added to the workspace, and VS Code then
583
+ // refuses to resolve it.
584
+ cwd: `\${workspaceFolder:${workspaceName}}`,
585
+ // One shared group keeps the four together in the dropdown; a per-entry group
586
+ // would make four groups of one. `order` holds them in verify order.
587
+ presentation: { group: "mnci", order: index + 1 }
588
+ }));
547
589
  }
548
- function addGoInternalLib(workspaceRoot, name) {
549
- prepareGo(workspaceRoot);
550
- runNx(
551
- [
552
- "g",
553
- "@nx-go/nx-go:library",
554
- `libs/${name}`,
555
- `--name=${name}`,
556
- "--tags=type:go-internal-lib",
557
- "--no-interactive"
558
- ],
559
- workspaceRoot
590
+ function vscodeWorkspace(workspaceName, existingTasks, existingLaunch, existingSettings) {
591
+ const userConfigurations = (existingLaunch?.configurations ?? []).filter(
592
+ (configuration) => !String(configuration.name ?? "").startsWith(LAUNCH_CONFIG_PREFIX)
560
593
  );
561
- addProjectJsonTargets2((0, import_node_path4.join)(workspaceRoot, "libs", name, "project.json"), {
562
- test: goTestTarget(),
563
- lint: goLintTarget()
564
- });
565
- registerProjectCommands(workspaceRoot, name, { build: false });
566
- const module2 = goModulePath(workspaceRoot);
567
- if (module2) {
568
- logger.step(`Import it as ${module2}/libs/${name}`);
569
- }
570
- }
571
-
572
- // src/commands/add/node.ts
573
- var import_node_path5 = require("path");
574
- function ensurePlugin2(workspaceRoot, packageName) {
575
- if (hasPlugin(workspaceRoot, packageName)) {
576
- return;
577
- }
578
- logger.step(`Installing Nx plugin ${packageName}`);
579
- runNx(["add", packageName], workspaceRoot);
580
- }
581
- function runNodeApp(workspaceRoot, name, stack, framework) {
582
- ensurePlugin2(workspaceRoot, "@nx/node");
583
- runNx(
584
- [
585
- "g",
586
- "@nx/node:application",
587
- `apps/${name}`,
588
- "--bundler=esbuild",
589
- `--unitTestRunner=${stack.testRunner}`,
590
- "--linter=none",
591
- "--e2eTestRunner=none",
592
- `--framework=${framework}`,
593
- "--no-interactive"
594
- ],
595
- workspaceRoot
594
+ const settings = { ...existingSettings, ...vscodeSettings() };
595
+ return JSON.stringify(
596
+ {
597
+ folders: [{ path: ".", name: workspaceName }],
598
+ settings,
599
+ extensions: { recommendations: VSCODE_RECOMMENDED_EXTENSIONS },
600
+ tasks: {
601
+ version: existingTasks?.version ?? "2.0.0",
602
+ tasks: existingTasks?.tasks ?? []
603
+ },
604
+ launch: {
605
+ version: existingLaunch?.version ?? "0.2.0",
606
+ configurations: [...launchConfigurations(workspaceName), ...userConfigurations]
607
+ }
608
+ },
609
+ null,
610
+ 2
596
611
  );
597
612
  }
598
- function nodeAppPackageTarget(name) {
599
- const zip = `dist/drop/node-app-${name}.zip`;
600
- const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFolder('apps/${name}/dist');z.writeZip('${zip}')"`;
613
+ var ROOT_SCRIPTS = {
614
+ build: "nx run-many -t build",
615
+ lint: "nx run-many -t lint",
616
+ test: "nx run-many -t test",
617
+ typecheck: "nx run-many -t typecheck",
618
+ affected: "nx affected -t lint,typecheck,test,build",
619
+ graph: "nx graph",
620
+ "release:preview": "nx release --dry-run",
621
+ prepare: "husky"
622
+ };
623
+ function rootScripts() {
601
624
  return {
602
- executor: "nx:run-commands",
603
- dependsOn: ["build"],
604
- outputs: [`{workspaceRoot}/${zip}`],
605
- options: { command }
606
- };
607
- }
608
- function addNodeApp(workspaceRoot, name, stack, framework = "none") {
609
- runNodeApp(workspaceRoot, name, stack, framework);
610
- ensureAdmZip(workspaceRoot);
611
- addNxTargets((0, import_node_path5.join)(workspaceRoot, "apps", name, "package.json"), {
612
- package: nodeAppPackageTarget(name)
613
- });
614
- removeGeneratedEslintConfig(workspaceRoot, `apps/${name}`);
615
- registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:serve` });
616
- }
617
- function ensureAzureFunctionsPackage(workspaceRoot) {
618
- if (hasPlugin(workspaceRoot, "@azure/functions")) {
619
- return;
620
- }
621
- logger.step("Installing @azure/functions");
622
- if (runShell("npm", ["install", "@azure/functions", "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
623
- throw new Error("npm install of @azure/functions failed");
624
- }
625
- }
626
- var NODE_FUNCTION_APP_MAIN = `// esbuild only includes what is reachable from here, so add one import per
627
- // function file you create under src/functions/.
628
- import './functions/hello'
629
- `;
630
- var NODE_FUNCTION_APP_HOST_JSON = `{
631
- "version": "2.0",
632
- "extensionBundle": {
633
- "id": "Microsoft.Azure.Functions.ExtensionBundle",
634
- "version": "[4.*, 5.0.0)"
635
- }
636
- }
637
- `;
638
- var NODE_FUNCTION_APP_HELLO = `import { app } from '@azure/functions'
639
- import type { HttpRequest, HttpResponseInit } from '@azure/functions'
640
- import { buildGreeting } from './greeting'
641
-
642
- async function hello(request: HttpRequest): Promise<HttpResponseInit> {
643
- const name = request.query.get('name') ?? 'world'
644
- return { body: buildGreeting(name) }
645
- }
646
-
647
- app.http('hello', {
648
- methods: ['GET'],
649
- authLevel: 'anonymous',
650
- route: 'hello',
651
- handler: hello,
652
- })
653
- `;
654
- var NODE_FUNCTION_APP_GREETING = `export function buildGreeting (name: string): string {
655
- return 'Hello, ' + name + '!'
656
- }
657
- `;
658
- var NODE_FUNCTION_APP_GREETING_SPEC = `import { buildGreeting } from './greeting'
659
-
660
- describe('buildGreeting', () => {
661
- it('greets a name', () => {
662
- expect(buildGreeting('world')).toBe('Hello, world!')
663
- })
664
- })
665
- `;
666
- function repairNodeFunctionAppManifest(nodeFunctionAppRoot, workspaceRoot) {
667
- const manifestPath = (0, import_node_path5.join)(nodeFunctionAppRoot, "package.json");
668
- const manifest = readJson(manifestPath);
669
- const azureFunctionsVersion = readJson(
670
- (0, import_node_path5.join)(workspaceRoot, "node_modules/@azure/functions/package.json")
671
- ).version;
672
- const dependencies = manifest.dependencies ?? {};
673
- writeFileEnsured(
674
- manifestPath,
675
- toJson({
676
- ...manifest,
677
- main: "dist/main.js",
678
- dependencies: { ...dependencies, "@azure/functions": `^${azureFunctionsVersion}` }
679
- })
680
- );
625
+ ...ROOT_SCRIPTS,
626
+ // ESLint is the formatter, so `format` is `--fix` and there is no separate
627
+ // `format:check`: `lint` already reports formatting as ordinary errors.
628
+ // Carrying a second script that ran the same tool twice would just make a
629
+ // CI run slower for no extra coverage.
630
+ format: "eslint . --fix --cache",
631
+ "python:install": `${PYTHON_INSTALL_GUARD} && ${PYTHON_WORKSPACE_INSTALL_GUARD}`
632
+ };
681
633
  }
682
- function nodeFunctionAppPackageTarget(name) {
683
- const zip = `dist/drop/node-function-app-${name}.zip`;
684
- const root = `apps/${name}`;
685
- const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFolder('${root}/dist','dist');z.addLocalFile('${root}/host.json');z.addLocalFile('${root}/package.json');z.writeZip('${zip}')"`;
634
+ function generatorDefaults(stack) {
635
+ const shared = {
636
+ // `none`, not `eslint`, and the workspace is still fully linted. The root
637
+ // config plus `@nx/eslint/plugin` ({@link ESLINT_PLUGIN_CONFIG}) give every
638
+ // project its `lint` target; `eslint` here would only make the generator
639
+ // scaffold a per-project config mnci deletes anyway — and drag in
640
+ // `eslint-plugin-import@2.31.0`, which peer-caps at ESLint 9 and breaks the
641
+ // install outright on this workspace's ESLint 10.
642
+ linter: "none",
643
+ unitTestRunner: stack.testRunner
644
+ };
686
645
  return {
687
- executor: "nx:run-commands",
688
- dependsOn: ["build"],
689
- outputs: [`{workspaceRoot}/${zip}`],
690
- options: { command }
646
+ "@nx/react:application": shared,
647
+ "@nx/react:library": shared,
648
+ "@nx/js:library": shared
691
649
  };
692
650
  }
693
- function nodeFunctionAppStartTarget(name) {
651
+ function mnciConfig(options) {
694
652
  return {
695
- executor: "nx:run-commands",
696
- dependsOn: ["build"],
697
- continuous: true,
698
- options: { command: "func start", cwd: `apps/${name}` }
653
+ // Persisted so `mnci upgrade` can name the `<name>.code-workspace` file it
654
+ // rewrites. Its absence is why upgrade used to write a file literally called
655
+ // `undefined.code-workspace` and therefore never refreshed the real one —
656
+ // see `resolveWorkspaceName` in `commands/upgrade.ts`, which still needs a
657
+ // fallback chain for workspaces generated before this field existed.
658
+ workspaceName: options.workspaceName,
659
+ scope: options.scope,
660
+ registry: options.registry,
661
+ agent: options.agent,
662
+ variableGroup: options.variableGroup,
663
+ ci: options.ci,
664
+ stack: { testRunner: options.stack.testRunner }
699
665
  };
700
666
  }
701
- function addNodeFunctionApp(workspaceRoot, name, stack) {
702
- runNodeApp(workspaceRoot, name, stack, "none");
703
- ensureAzureFunctionsPackage(workspaceRoot);
704
- const nodeFunctionAppRoot = (0, import_node_path5.join)(workspaceRoot, "apps", name);
705
- writeFileEnsured((0, import_node_path5.join)(nodeFunctionAppRoot, "src/main.ts"), NODE_FUNCTION_APP_MAIN);
706
- writeFileEnsured((0, import_node_path5.join)(nodeFunctionAppRoot, "src/functions/hello.ts"), NODE_FUNCTION_APP_HELLO);
707
- writeFileEnsured(
708
- (0, import_node_path5.join)(nodeFunctionAppRoot, "src/functions/greeting.ts"),
709
- NODE_FUNCTION_APP_GREETING
710
- );
711
- writeFileEnsured(
712
- (0, import_node_path5.join)(nodeFunctionAppRoot, "src/functions/greeting.spec.ts"),
713
- NODE_FUNCTION_APP_GREETING_SPEC
714
- );
715
- writeFileEnsured((0, import_node_path5.join)(nodeFunctionAppRoot, "host.json"), NODE_FUNCTION_APP_HOST_JSON);
716
- repairNodeFunctionAppManifest(nodeFunctionAppRoot, workspaceRoot);
717
- ensureAdmZip(workspaceRoot);
718
- addNxTargets((0, import_node_path5.join)(nodeFunctionAppRoot, "package.json"), {
719
- package: nodeFunctionAppPackageTarget(name),
720
- start: nodeFunctionAppStartTarget(name)
721
- });
722
- removeGeneratedEslintConfig(workspaceRoot, `apps/${name}`);
723
- registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
724
- }
725
-
726
- // src/commands/add/npmLib.ts
727
- var import_node_path6 = require("path");
728
- async function addNpmLib(workspaceRoot, name, options, kindProvided, stack) {
729
- const scope = options.scope ?? (kindProvided ? defaultScope(workspaceRoot) : await promptText("npm scope for the published package", defaultScope(workspaceRoot)));
730
- runNx(
731
- [
732
- "g",
733
- "@nx/js:lib",
734
- `packages/${name}`,
735
- "--publishable",
736
- `--importPath=${scope}/${name}`,
737
- "--bundler=rollup",
738
- `--unitTestRunner=${stack.testRunner}`,
739
- "--linter=none",
740
- "--no-interactive"
741
- ],
742
- workspaceRoot
743
- );
744
- const manifestPath = (0, import_node_path6.join)(workspaceRoot, "packages", name, "package.json");
745
- markPublic(manifestPath);
746
- repairPublishableManifest(manifestPath);
747
- removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
748
- registerProjectCommands(workspaceRoot, name, { build: true });
667
+ function readMnciConfig(workspaceRoot) {
668
+ const nxJson = readJson((0, import_node_path2.join)(workspaceRoot, "nx.json"));
669
+ return nxJson.mnci ?? {};
749
670
  }
750
-
751
- // src/commands/add/python.ts
752
- var import_node_fs4 = require("fs");
753
- var import_node_path7 = require("path");
754
- function ensurePython(workspaceRoot) {
755
- const hasPython = runShell("python3", ["--version"], workspaceRoot) === 0 || runShell("python", ["--version"], workspaceRoot) === 0;
756
- if (!hasPython) {
757
- throw new Error(
758
- "Python not found. Install Python 3.9+ first: https://www.python.org/downloads/"
759
- );
671
+ function pythonPublishUrl(registry) {
672
+ if (registry.kind === "azure-artifacts") {
673
+ return `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}/pypi/upload/`;
760
674
  }
675
+ return void 0;
761
676
  }
762
- function pythonPipPluginSpec() {
763
- return process.env.MNCI2_PYTHON_PIP_SPEC ?? "@mnci/nx-python-pip";
764
- }
765
- function ensurePythonPipPlugin(workspaceRoot) {
766
- if (hasPlugin(workspaceRoot, "@mnci/nx-python-pip")) {
767
- return;
768
- }
769
- const spec = pythonPipPluginSpec();
770
- logger.step(`Installing the Python toolchain plugin (${spec})`);
771
- if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
772
- throw new Error("npm install of @mnci/nx-python-pip failed");
773
- }
677
+ var PYTHON_INSTALL_GUARD = `node -e "if(!require('node:fs').existsSync('requirements-dev.txt')){console.log('No Python projects - skipping.');process.exit(0)}const py=process.platform==='win32'?'python':'python3';process.exit(require('node:child_process').spawnSync(py+' -m pip install -r requirements-dev.txt',{stdio:'inherit',shell:true}).status ?? 1)"`;
678
+ var PYTHON_WORKSPACE_INSTALL_GUARD = `node -e "const fs=require('node:fs'),path=require('node:path');const editableDirs=[...fs.globSync('apps/*/pyproject.toml'),...fs.globSync('python-packages/*/pyproject.toml'),...fs.globSync('libs/*/pyproject.toml')].map((p)=>path.dirname(p));const requirementsFiles=fs.globSync('apps/*/requirements.txt');if(editableDirs.length===0&&requirementsFiles.length===0){console.log('No Python projects - skipping.');process.exit(0)}const args=['-m','pip','install','--quiet',...editableDirs.flatMap((d)=>['-e',d]),...requirementsFiles.flatMap((f)=>['-r',f])];const py=process.platform==='win32'?'python':'python3';process.exit(require('node:child_process').spawnSync(py,args,{stdio:'inherit'}).status ?? 1)"`;
679
+ var PIP_AUDIT_GUARD = `node -e "if(!require('node:fs').existsSync('requirements-dev.txt')){console.log('No Python projects - skipping.');process.exit(0)}const py=process.platform==='win32'?'python':'python3';require('node:child_process').spawnSync(py,['-m','pip_audit'],{stdio:'inherit'});process.exit(0)"`;
680
+ var GO_MODULE_DOWNLOAD_GUARD = `node -e "if(!require('node:fs').existsSync('go.mod')){console.log('No Go projects - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('go',['mod','download'],{stdio:'inherit'}).status ?? 1)"`;
681
+ var GOLANGCI_LINT_INSTALL_GUARD = `node -e "const fs=require('node:fs'),cp=require('node:child_process');if(!fs.existsSync('go.mod')){console.log('No Go projects - skipping.');process.exit(0)}if(cp.spawnSync('golangci-lint',['--version'],{stdio:'ignore'}).status===0){console.log('golangci-lint already installed - skipping.');process.exit(0)}process.exit(cp.spawnSync('go',['install','github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest'],{stdio:'inherit'}).status ?? 1)"`;
682
+ var GO_TOOL_PATH_PRELUDE = "const fs=require('node:fs'),cp=require('node:child_process');if(!fs.existsSync('go.mod')){console.log('No Go projects - skipping.');process.exit(0)}const r=cp.spawnSync('go',['env','GOPATH'],{encoding:'utf8'});if(r.status!==0){console.log('Could not resolve GOPATH - skipping.');process.exit(0)}const bin=require('node:path').join(r.stdout.trim(),'bin');";
683
+ var GO_TOOL_PATH_AZURE = `node -e "${GO_TOOL_PATH_PRELUDE}console.log('##vso[task.prependpath]'+bin)"`;
684
+ var GO_TOOL_PATH_GITHUB = `node -e "${GO_TOOL_PATH_PRELUDE}if(!process.env.GITHUB_PATH){console.log('Not running in GitHub Actions - skipping.');process.exit(0)}fs.appendFileSync(process.env.GITHUB_PATH,bin+${String.raw`'\n'`})"`;
685
+ var FLUTTER_SDK_VERSION = "3.44.8";
686
+ var NODE_VERSION = "24";
687
+ var NPM_VERSION = "11";
688
+ var FLUTTER_SDK_DIRECTORY_EXPRESSION = `require('node:path').join(require('node:os').homedir(),'.mnci-flutter-${FLUTTER_SDK_VERSION}')`;
689
+ var FLUTTER_SDK_INSTALL_GUARD = `node -e "const fs=require('node:fs'),cp=require('node:child_process');if(!fs.existsSync('pubspec.yaml')){console.log('No Flutter projects - skipping.');process.exit(0)}if(cp.spawnSync('flutter',['--version'],{stdio:'ignore',shell:process.platform==='win32'}).status===0){console.log('Flutter SDK already on PATH - skipping.');process.exit(0)}const sdk=${FLUTTER_SDK_DIRECTORY_EXPRESSION};if(fs.existsSync(sdk)){console.log('Flutter SDK already installed at '+sdk+' - skipping.');process.exit(0)}process.exit(cp.spawnSync('git',['clone','--depth','1','--branch','${FLUTTER_SDK_VERSION}','https://github.com/flutter/flutter.git',sdk],{stdio:'inherit'}).status ?? 1)"`;
690
+ var FLUTTER_TOOL_PATH_PRELUDE = `const fs=require('node:fs'),path=require('node:path');if(!fs.existsSync('pubspec.yaml')){console.log('No Flutter projects - skipping.');process.exit(0)}const sdk=${FLUTTER_SDK_DIRECTORY_EXPRESSION};if(!fs.existsSync(sdk)){console.log('Flutter SDK was not installed by mnci (already on PATH) - skipping.');process.exit(0)}const bin=path.join(sdk,'bin');`;
691
+ var FLUTTER_TOOL_PATH_AZURE = `node -e "${FLUTTER_TOOL_PATH_PRELUDE}console.log('##vso[task.prependpath]'+bin)"`;
692
+ var FLUTTER_TOOL_PATH_GITHUB = `node -e "${FLUTTER_TOOL_PATH_PRELUDE}if(!process.env.GITHUB_PATH){console.log('Not running in GitHub Actions - skipping.');process.exit(0)}fs.appendFileSync(process.env.GITHUB_PATH,bin+${String.raw`'\n'`})"`;
693
+ var FLUTTER_PUB_GET_GUARD = `node -e "const fs=require('node:fs');if(!fs.existsSync('pubspec.yaml')){console.log('No Flutter projects - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('flutter',['pub','get'],{stdio:'inherit',shell:process.platform==='win32'}).status ?? 1)"`;
694
+ var NPM_AUDIT_STEP = `node -e "const cp=require('node:child_process');const r=cp.spawnSync('npm',['audit','--json'],{encoding:'utf8',shell:process.platform==='win32',maxBuffer:33554432});let d;try{d=JSON.parse(r.stdout)}catch{console.log('npm audit produced no JSON (exit '+r.status+') - not blocking on a broken audit.');process.exit(0)}const all=Object.values(d.vulnerabilities||{});const BLOCK=['critical','high','moderate'];const act=all.filter(v=>v.fixAvailable&&BLOCK.includes(v.severity));const rest=all.filter(v=>!act.includes(v));for(const v of rest)console.log(' note ['+v.severity+'] '+v.name+(v.fixAvailable?' - fix available, below the blocking threshold':' - NO fix available upstream, nothing to do here'));if(act.length===0){console.log('npm audit - '+all.length+' advisory(ies), none actionable at moderate or above.');process.exit(0)}for(const v of act)console.log(' BLOCKING ['+v.severity+'] '+v.name+' - fix available'+(v.fixAvailable&&v.fixAvailable.isSemVerMajor?' (semver-major)':''));console.log('Each has a published fix. Add a targeted overrides entry in package.json rather than npm audit fix --force.');process.exit(1)"`;
695
+ var VERIFY_TARGETS = "lint,typecheck,test,build";
696
+ var AFFECTED_OR_ALL_GUARD = `node -e "const cp=require('node:child_process');const T='${VERIFY_TARGETS}';const all=()=>process.exit(cp.spawnSync('npx nx run-many -t '+T,{stdio:'inherit',shell:true}).status ?? 1);const ref=process.env.GITHUB_BASE_REF||process.env.SYSTEM_PULLREQUEST_TARGETBRANCH||'';if(!ref){console.log('Not a pull request - verifying EVERY project.');all()}const target=ref.replace('refs/heads/','');const mergeBase=r=>{const o=cp.spawnSync('git',['merge-base',r,'HEAD'],{encoding:'utf8'});return o.status===0?o.stdout.trim():''};let base=mergeBase('origin/'+target);if(!base){console.log('No origin/'+target+' ref - fetching it to resolve a merge-base.');cp.spawnSync('git',['fetch','--no-tags','origin',target],{stdio:'inherit'});base=mergeBase('FETCH_HEAD')}if(!base){console.log('Could not resolve a merge-base with '+target+' - verifying EVERY project.');all()}console.log('Pull request against '+target+' - verifying projects affected since '+base);process.exit(cp.spawnSync('npx nx affected -t '+T+' --base='+base,{stdio:'inherit',shell:true}).status ?? 1)"`;
697
+ var PACK_APPS_GUARD = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});if(fs.globSync('apps/*/project.json').length===0){console.log('No apps to pack - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('npx nx run-many -t package',{stdio:'inherit',shell:true}).status ?? 1)"`;
698
+ function releaseGuard(pythonPublishEnv) {
699
+ return `node -e "const fs=require('node:fs'),cp=require('node:child_process');const hasNpm=fs.globSync('packages/*/package.json').length>0;const hasPython=fs.globSync('python-packages/*/pyproject.toml').length>0;if(!hasNpm&&!hasPython){console.log('Nothing to release - skipping.');process.exit(0)}const env={...process.env};${pythonPublishEnv}process.exit(cp.spawnSync('npx nx release --yes',{stdio:'inherit',shell:true,env}).status ?? 1)"`;
774
700
  }
775
- var PYTHON_REQUIREMENTS_DEV = `build
776
- twine
777
- ruff
778
- pytest
779
- pip-audit
780
- `;
781
- function ensureRequirementsDev(workspaceRoot) {
782
- const requirementsDevPath = (0, import_node_path7.join)(workspaceRoot, "requirements-dev.txt");
783
- if (!fileExists(requirementsDevPath)) {
784
- writeFileEnsured(requirementsDevPath, PYTHON_REQUIREMENTS_DEV);
785
- }
701
+ function pythonPublishEnvFragment(pythonPublishUrl2) {
702
+ return pythonPublishUrl2 ? `if(hasPython){env.TWINE_REPOSITORY_URL='${pythonPublishUrl2}';env.TWINE_USERNAME='AzureArtifacts';env.TWINE_PASSWORD=Buffer.from(process.env.PAT,'base64').toString()}` : "";
786
703
  }
787
- function pythonModuleDirectory(name) {
788
- return name.replaceAll("-", "_");
704
+ function npmAuthEnvVariable(registryKind, variableReference) {
705
+ return registryKind === "npm" ? ["NODE_AUTH_TOKEN", variableReference("NPM_TOKEN")] : ["PAT", variableReference("PAT")];
789
706
  }
790
- function addProjectJsonTargets3(projectJsonPath, newTargets) {
791
- const project = readJson(projectJsonPath);
792
- const targets = project.targets ?? {};
793
- writeFileEnsured(projectJsonPath, toJson({ ...project, targets: { ...targets, ...newTargets } }));
707
+ function poolBlock(agent) {
708
+ return /^(?:ubuntu|windows|macos)-/i.test(agent) ? ` vmImage: ${agent}` : ` name: ${agent}`;
794
709
  }
795
- function pythonAppMain(name) {
796
- return `def main() -> None:
797
- print('${name} is running')
710
+ function azurePipelinesYaml(agent, variableGroup, pythonPublishUrl2, registryKind = "azure-artifacts") {
711
+ const onMain = "and(succeeded(), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI'), eq(variables['Build.SourceBranchName'], 'main'))";
712
+ const [npmAuthName, npmAuthValue] = npmAuthEnvVariable(registryKind, (name) => `$(${name})`);
713
+ return `name: monorepo-ci-$(Date:yyyyMMdd)$(Rev:.r)
798
714
 
715
+ # Generated by MoNecromanCI. Deliberately thin: Nx builds, 'nx release'
716
+ # versions from conventional commits and pushes ONLY a tag to main, and each
717
+ # app is packed into dist/drop/<type>-<name>.zip by its own 'package' target.
718
+ #
719
+ # Cross-platform by construction: no bash, no PowerShell. Every step is a
720
+ # built-in task or a single-line git/npm/npx/node command, so the pipeline
721
+ # runs unchanged on Linux, macOS and Windows agents.
799
722
 
800
- if __name__ == '__main__':
801
- main()
802
- `;
803
- }
804
- function pythonAppStartTarget(name) {
805
- return {
806
- executor: "nx:run-commands",
807
- continuous: true,
808
- options: { command: "python3 main.py", cwd: `apps/${name}` }
809
- };
810
- }
811
- function pythonFunctionAppStartTarget(name) {
812
- return {
813
- executor: "nx:run-commands",
814
- continuous: true,
815
- options: { command: "func start", cwd: `apps/${name}` }
816
- };
817
- }
818
- function pythonAppPackageTarget(name) {
819
- const zip = `dist/drop/python-app-${name}.zip`;
820
- const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFolder('apps/${name}/dist');z.writeZip('${zip}')"`;
821
- return {
822
- executor: "nx:run-commands",
823
- dependsOn: ["build"],
824
- outputs: [`{workspaceRoot}/${zip}`],
825
- options: { command }
826
- };
827
- }
828
- function pythonFunctionAppPackageTarget(name, moduleDirectory) {
829
- const zip = `dist/drop/python-function-app-${name}.zip`;
830
- const root = `apps/${name}`;
831
- const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFile('${root}/function_app.py');z.addLocalFile('${root}/host.json');z.addLocalFile('${root}/requirements.txt');z.addLocalFolder('${root}/${moduleDirectory}','${moduleDirectory}');z.writeZip('${zip}')"`;
832
- return {
833
- executor: "nx:run-commands",
834
- outputs: [`{workspaceRoot}/${zip}`],
835
- options: { command }
836
- };
837
- }
838
- function addPythonApp(workspaceRoot, name) {
839
- ensurePython(workspaceRoot);
840
- ensurePythonPipPlugin(workspaceRoot);
841
- ensureRequirementsDev(workspaceRoot);
842
- ensureAdmZip(workspaceRoot);
843
- runNx(
844
- ["g", "@mnci/nx-python-pip:application", name, `--directory=apps/${name}`, "--no-interactive"],
845
- workspaceRoot
846
- );
847
- writeFileEnsured((0, import_node_path7.join)(workspaceRoot, "apps", name, "main.py"), pythonAppMain(name));
848
- addProjectJsonTargets3((0, import_node_path7.join)(workspaceRoot, "apps", name, "project.json"), {
849
- package: pythonAppPackageTarget(name),
850
- start: pythonAppStartTarget(name)
851
- });
852
- registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
853
- }
854
- function addPythonFunctionApp(workspaceRoot, name) {
855
- ensurePython(workspaceRoot);
856
- ensurePythonPipPlugin(workspaceRoot);
857
- ensureRequirementsDev(workspaceRoot);
858
- ensureAdmZip(workspaceRoot);
859
- runNx(
860
- [
861
- "g",
862
- "@mnci/nx-python-pip:function-application",
863
- name,
864
- `--directory=apps/${name}`,
865
- "--no-interactive"
866
- ],
867
- workspaceRoot
868
- );
869
- const moduleDirectory = pythonModuleDirectory(name);
870
- addProjectJsonTargets3((0, import_node_path7.join)(workspaceRoot, "apps", name, "project.json"), {
871
- package: pythonFunctionAppPackageTarget(name, moduleDirectory),
872
- start: pythonFunctionAppStartTarget(name)
873
- });
874
- registerProjectCommands(workspaceRoot, name, { build: false, start: `nx run ${name}:start` });
875
- }
876
- function addPythonLib(workspaceRoot, name) {
877
- ensurePython(workspaceRoot);
878
- ensurePythonPipPlugin(workspaceRoot);
879
- ensureRequirementsDev(workspaceRoot);
880
- runNx(
881
- [
882
- "g",
883
- "@mnci/nx-python-pip:library",
884
- name,
885
- `--directory=python-packages/${name}`,
886
- "--no-interactive"
887
- ],
888
- workspaceRoot
889
- );
890
- registerProjectCommands(workspaceRoot, name, { build: true });
891
- }
892
- function addPythonInternalLib(workspaceRoot, name) {
893
- ensurePython(workspaceRoot);
894
- ensurePythonPipPlugin(workspaceRoot);
895
- ensureRequirementsDev(workspaceRoot);
896
- runNx(
897
- [
898
- "g",
899
- "@mnci/nx-python-pip:internal-library",
900
- name,
901
- `--directory=libs/${name}`,
902
- "--no-interactive"
903
- ],
904
- workspaceRoot
905
- );
906
- registerProjectCommands(workspaceRoot, name, { build: false });
907
- }
908
- var PYTHON_PROJECT_DIRS = ["apps", "python-packages", "libs"];
909
- function findPythonProjectDirectory(workspaceRoot, name) {
910
- return PYTHON_PROJECT_DIRS.find(
911
- (directory) => fileExists((0, import_node_path7.join)(workspaceRoot, directory, name, "pyproject.toml"))
912
- );
913
- }
914
- function parseVendorEntries(pyprojectToml) {
915
- const match = /^\s*vendor\s*=\s*\[([^\]]*)\]/m.exec(pyprojectToml);
916
- if (!match) {
917
- return [];
918
- }
919
- return match[1].split(",").map((entry) => entry.trim().replaceAll(/^["']|["']$/g, "")).filter(Boolean);
920
- }
921
- function addVendorEntry(pyprojectToml, lib) {
922
- const existing = parseVendorEntries(pyprojectToml);
923
- if (existing.length > 0) {
924
- const merged = [...existing, lib].map((name) => `"${name}"`).join(", ");
925
- return pyprojectToml.replace(
926
- /^(\s*vendor\s*=\s*)\[[^\]]*\]/m,
927
- (_match, prefix) => `${prefix}[${merged}]`
928
- );
929
- }
930
- const table = `[tool.mnci-python-pip]
931
- vendor = ["${lib}"]
723
+ trigger:
724
+ # While a run is in progress, queue further pushes and run them together rather
725
+ # than concurrently. This is the closest thing Azure Pipelines YAML has to
726
+ # GitHub's concurrency group, and it is load-bearing on main for the same
727
+ # reason: two concurrent 'nx release' runs would race to create the same tag.
728
+ batch: true
729
+ branches:
730
+ include:
731
+ - main
732
+ # EVERY other branch too, and that breadth is a correction rather than
733
+ # generosity. On Azure Repos Git a YAML 'pr:' block does nothing at all
734
+ # (see the note below), so a CI trigger is the only pre-merge
735
+ # verification this file can switch on by itself. Listing main alone \u2014
736
+ # which is what this pipeline used to do, next to a 'pr:' block that
737
+ # looked like it covered the rest \u2014 means a workspace verifies NOTHING
738
+ # until a change has already reached the branch it was supposed to
739
+ # protect.
740
+ #
741
+ # Release stays gated on main (the 'condition:' on every release step
742
+ # below), so a topic-branch run installs, audits, syncs, formats,
743
+ # verifies and packs, and publishes nothing.
744
+ - '*'
932
745
 
933
- `;
934
- return pyprojectToml.includes("[tool.pytest.ini_options]") ? pyprojectToml.replace("[tool.pytest.ini_options]", () => `${table}[tool.pytest.ini_options]`) : `${pyprojectToml}
935
- ${table}`;
936
- }
937
- async function addPythonVendor(workspaceRoot, consumer, options, kindProvided) {
938
- const lib = options.lib ?? (kindProvided ? void 0 : await promptText("Internal Python library to vendor (an existing libs/<name>)"));
939
- if (!lib) {
940
- throw new Error(
941
- "`add python-vendor` needs the library to vendor: pass --lib <name> (an existing libs/<name> internal library)."
942
- );
943
- }
944
- const consumerDirectory = findPythonProjectDirectory(workspaceRoot, consumer);
945
- if (!consumerDirectory) {
946
- throw new Error(
947
- `No Python project named '${consumer}' with a pyproject.toml found (checked apps/, python-packages/, libs/). A Python function app has no pyproject.toml \u2014 there is nothing to vendor into.`
948
- );
949
- }
950
- if (!fileExists((0, import_node_path7.join)(workspaceRoot, "libs", lib, "pyproject.toml"))) {
951
- throw new Error(
952
- `No internal Python library named '${lib}' found at libs/${lib}/pyproject.toml. Only an internal library (added via 'mnci add python-internal-lib') can be vendored.`
953
- );
954
- }
955
- if (consumerDirectory === "libs" && consumer === lib) {
956
- throw new Error(`'${consumer}' cannot vendor itself.`);
957
- }
958
- const pyprojectPath = (0, import_node_path7.join)(workspaceRoot, consumerDirectory, consumer, "pyproject.toml");
959
- const pyprojectToml = (0, import_node_fs4.readFileSync)(pyprojectPath, "utf8");
960
- if (parseVendorEntries(pyprojectToml).includes(lib)) {
961
- logger.info(`'${consumer}' already vendors '${lib}' \u2014 nothing to do.`);
962
- return;
963
- }
964
- writeFileEnsured(pyprojectPath, addVendorEntry(pyprojectToml, lib));
965
- logger.success(
966
- `'${consumer}' now vendors '${lib}' \u2014 its wheel will include '${lib}''s module on the next build.`
967
- );
968
- }
746
+ # PR validation is deliberately NOT configured here, because on Azure Repos Git
747
+ # it CANNOT be: "For an Azure Repos Git repo, you cannot configure a PR trigger
748
+ # in the YAML file. You need to use branch policies." A 'pr:' block here is not
749
+ # an error \u2014 it is silently ignored, which is worse, because the file then
750
+ # documents a gate that does not exist. (The same block IS honoured for GitHub
751
+ # and Bitbucket repos, which is why .github/workflows/ci.yml keeps its
752
+ # 'pull_request:' trigger.)
753
+ #
754
+ # To get PR validation, configure it once in the UI: Project Settings ->
755
+ # Repositories -> <repo> -> Policies -> <branch> -> Build Validation, pointing
756
+ # at this pipeline. That is also the only thing that sets
757
+ # System.PullRequest.TargetBranch, which the verify step below reads to scope
758
+ # itself to affected projects \u2014 without a policy that variable is never set and
759
+ # the step verifies every project instead. Correct either way, just slower.
760
+ #
761
+ # Cancelling a superseded PR run lives in that same policy ("automatically
762
+ # cancel"), and likewise has no YAML expression.
763
+ pr: none
764
+
765
+ pool:
766
+ ${poolBlock(agent)}
767
+
768
+ variables:
769
+ # Holds the npm auth secret the root .npmrc reads: the base64-encoded \`PAT\`
770
+ # for an Azure Artifacts feed, or a raw npm automation token as \`NPM_TOKEN\`
771
+ # for public npm. Mark it secret in Library. Add app build vars here too if needed.
772
+ - group: ${variableGroup}
773
+ # Relocates npm's cache inside the pipeline workspace so Cache@2 can restore it
774
+ # (the default ~/.npm is outside the cacheable area on hosted agents). npm reads
775
+ # this as an ordinary env var, so it needs no config file and works on every
776
+ # agent OS.
777
+ - name: npm_config_cache
778
+ value: $(Pipeline.Workspace)/.npm
779
+
780
+ steps:
781
+ - checkout: self
782
+ # Lets later steps push release tags back to the repo. The Project
783
+ # Collection Build Service account needs Contribute permission on this
784
+ # repo (Project Settings -> Repositories -> Security) \u2014 one-time grant.
785
+ persistCredentials: true
786
+ fetchDepth: 0
787
+
788
+ # checkout leaves a detached HEAD; nx release needs a real branch.
789
+ - script: git checkout -B $(Build.SourceBranchName)
790
+ displayName: Attach HEAD to the source branch
791
+
792
+ # Version resolution needs the release tags.
793
+ - script: git fetch --all --prune --tags
794
+ displayName: Fetch branches and release tags
795
+
796
+ - script: git config user.name "Azure Pipelines" && git config user.email "pipeline@dev.azure.com"
797
+ displayName: Set the git identity used for release tags
798
+
799
+ - task: UseNode@1
800
+ inputs:
801
+ version: 24.x
802
+
803
+ # The Azure counterpart of \`cache: npm\` on actions/setup-node: restores npm's
804
+ # download cache so \`npm ci\` does not re-fetch every tarball on every run.
805
+ # Keyed on the lockfile, so a dependency change misses and anything else hits;
806
+ # restoreKeys falls back to the newest cache for this OS on a miss, which still
807
+ # avoids a fully cold install. Agent.OS is in the key because a cached native
808
+ # module built for one OS is not reusable on another.
809
+ - task: Cache@2
810
+ displayName: Cache npm packages
811
+ inputs:
812
+ key: 'npm | "$(Agent.OS)" | package-lock.json'
813
+ restoreKeys: |
814
+ npm | "$(Agent.OS)"
815
+ path: $(npm_config_cache)
816
+
817
+ - script: npm ci
818
+ displayName: Install dependencies
819
+ env:
820
+ ${npmAuthName}: ${npmAuthValue}
821
+
822
+ # Fails ONLY on an advisory that has a published fix, at moderate or above;
823
+ # anything upstream has not fixed is printed and passes. See NPM_AUDIT_STEP's
824
+ # remarks for why that split replaced a warn-only step.
825
+ - script: ${NPM_AUDIT_STEP}
826
+ displayName: npm audit (fails on an actionable advisory)
827
+
828
+ # Installs the fixed Python toolchain (ruff, pytest, build, twine, pip-audit)
829
+ # \u2014 written by 'mnci add' to requirements-dev.txt on the first Python
830
+ # project. Plain pip, no uv/Poetry: portable guard skips cleanly on a
831
+ # workspace with none.
832
+ - script: ${PYTHON_INSTALL_GUARD}
833
+ displayName: Install Python dependencies (ruff, pytest, build, twine, pip-audit)
834
+
835
+ # Editable-installs every Python project into one shared environment, the
836
+ # pip-world counterpart of 'npm install' hoisting every workspace package
837
+ # into one root node_modules \u2014 so a project that imports an internal lib
838
+ # (normally vendored only at build time) can resolve that import at
839
+ # lint/test time too. Portable guard skips cleanly on a workspace with none.
840
+ - script: ${PYTHON_WORKSPACE_INSTALL_GUARD}
841
+ displayName: Install Python project dependencies (editable, workspace-wide)
842
+
843
+ # Non-blocking, same reasoning as the npm audit step above. Runs after the
844
+ # workspace-wide install so it scans the workspace's actual dependency set,
845
+ # not just the fixed toolchain.
846
+ - script: ${PIP_AUDIT_GUARD}
847
+ displayName: pip-audit (non-blocking)
848
+
849
+ # Go, if the workspace has any: hosted agents ship the toolchain, so only
850
+ # the module cache and the linter need seeding. Both guards skip cleanly on
851
+ # a workspace with no root go.mod.
852
+ - script: ${GO_MODULE_DOWNLOAD_GUARD}
853
+ displayName: Download Go module dependencies
854
+
855
+ # golangci-lint is what the generated Go lint target actually runs (the
856
+ # plugin's own default is 'go fmt', which only reformats). 'go install'
857
+ # drops it in GOPATH/bin, which is not on PATH by default on a hosted
858
+ # agent \u2014 so prepend it for every later step in the job.
859
+ - script: ${GOLANGCI_LINT_INSTALL_GUARD}
860
+ displayName: Install golangci-lint
861
+
862
+ - script: ${GO_TOOL_PATH_AZURE}
863
+ displayName: Add Go tool bin to PATH
864
+
865
+ # Flutter, if the workspace has any. Unlike Python and Go, hosted agents do
866
+ # NOT ship the Flutter SDK, so this installs it (shallow git clone at a
867
+ # pinned tag) and puts it on PATH. All three guards skip cleanly on a
868
+ # workspace with no root pubspec.yaml, and the install also skips when an
869
+ # SDK is already on PATH.
870
+ - script: ${FLUTTER_SDK_INSTALL_GUARD}
871
+ displayName: Install the Flutter SDK (${FLUTTER_SDK_VERSION})
872
+
873
+ - script: ${FLUTTER_TOOL_PATH_AZURE}
874
+ displayName: Add the Flutter SDK to PATH
875
+
876
+ # One command resolves EVERY Dart dependency, internal and external, for the
877
+ # whole workspace: the projects are pub workspace members, so this writes a
878
+ # single root pubspec.lock they all share.
879
+ - script: ${FLUTTER_PUB_GET_GUARD}
880
+ displayName: Resolve Dart dependencies (one pub get for the whole workspace)
881
+
882
+ # Fails fast, with an unambiguous message, when a stale TypeScript project
883
+ # reference (or another sync generator's drift) was never synced+committed
884
+ # locally \u2014 sync.applyChanges (nx.json) only auto-applies interactively, so
885
+ # CI still needs its own explicit, early check rather than surfacing this as
886
+ # a confusing failure buried inside the build step below.
887
+ - script: npx nx sync:check
888
+ displayName: Verify the workspace is synced (run 'npx nx sync' locally and commit if this fails)
889
+
890
+ # The one verify step, and deliberately the only one: affected projects on a
891
+ # pull request, EVERY project on anything else (a push to main included, so a
892
+ # release is always verified in full). Every fallback \u2014 no target branch, an
893
+ # unresolvable merge-base \u2014 takes the full path, because a run that verifies
894
+ # too little still reports green.
895
+ #
896
+ # 'npm run lint' is 'nx run-many -t lint', a strict subset of the targets
897
+ # below, so adding it back as its own step would only duplicate work \u2014 and on
898
+ # a pull request it would re-lint every project, discarding the point of this.
899
+ - script: ${AFFECTED_OR_ALL_GUARD}
900
+ displayName: Verify (affected on a PR, every project on main)
901
+
902
+ # Pack every app into dist/drop/<type>-<name>.zip via each app's 'package'
903
+ # target. Portable guard: skip cleanly when the workspace has no apps yet.
904
+ - script: ${PACK_APPS_GUARD}
905
+ displayName: Pack all apps (one zip per app -> dist/drop)
906
+ condition: ${onMain}
907
+
908
+ - task: PublishBuildArtifacts@1
909
+ displayName: Publish the drop (one zip per app)
910
+ condition: ${onMain}
911
+ inputs:
912
+ PathtoPublish: $(Build.SourcesDirectory)/dist/drop
913
+ ArtifactName: drop
914
+
915
+ # One build tag per packed app, EXACTLY the zip name (type-name), so the
916
+ # classic release pipeline knows which app to run for. Derived from the zip
917
+ # filenames so the tag can never drift from the artifact.
918
+ - script: node -e "const fs=require('node:fs');const path=require('node:path');for(const f of fs.globSync('dist/drop/*.zip')){console.log('##vso[build.addbuildtag]'+path.basename(f,'.zip'))}"
919
+ displayName: Tag the run per app (type-name)
920
+ condition: ${onMain}
921
+
922
+ # Version + tag + publish, in one release, for npm (packages/*) AND Python
923
+ # (python-packages/*) \u2014 conventional commits, tag-only push. Portable guard:
924
+ # nx release errors on an empty scope, so skip cleanly when there is nothing
925
+ # to release. When there are Python packages and an Azure feed, twine
926
+ # publish credentials are exported (raw PAT, decoded from the base64 variable).
927
+ - script: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2))}
928
+ displayName: Release \u2014 version, tag and publish (npm + Python)
929
+ condition: ${onMain}
930
+ env:
931
+ ${npmAuthName}: ${npmAuthValue}
969
932
 
970
- // src/commands/add/reactApp.ts
971
- var import_node_fs5 = require("fs");
972
- var import_node_path8 = require("path");
973
- var REACT_ENVIRONMENTS = ["dev", "uat", "prod"];
974
- function reactEnvironmentFile(environment) {
975
- return `# Vite compiles VITE_-prefixed vars into the '${environment}' bundle at build time.
976
- # They ship to the browser, so keep only PUBLIC config here \u2014 never secrets.
977
- VITE_ENVIRONMENT=${environment}
978
- VITE_API_URL=https://api.${environment}.example.com
933
+ # nx release's own git push (release.git.push) is deliberately left off: it
934
+ # only runs when a remote GitHub/GitLab Release is configured, which this
935
+ # pipeline never does, so it would never push the tag the step above just
936
+ # created. Pushed explicitly, unconditionally (a no-op when nothing released)
937
+ # once tagging is guaranteed to have already happened.
938
+ - script: git push origin --tags
939
+ displayName: Push release tags (nx release's own push never runs without a remote Release configured)
940
+ condition: ${onMain}
979
941
  `;
980
942
  }
981
- function reactAppTargets(name) {
982
- const targets = {};
983
- for (const environment of REACT_ENVIRONMENTS) {
984
- targets[`build-${environment}`] = {
985
- executor: "nx:run-commands",
986
- outputs: [`{workspaceRoot}/apps/${name}/dist-${environment}`],
987
- options: {
988
- command: `vite build --mode ${environment} --outDir dist-${environment}`,
989
- cwd: `apps/${name}`
990
- }
991
- };
992
- }
993
- const zipStatements = REACT_ENVIRONMENTS.map(
994
- (environment) => `z=new A();z.addLocalFolder('apps/${name}/dist-${environment}');z.writeZip('dist/drop/react-app-${name}-${environment}.zip')`
995
- ).join(";");
996
- targets.package = {
997
- executor: "nx:run-commands",
998
- dependsOn: REACT_ENVIRONMENTS.map((environment) => `build-${environment}`),
999
- outputs: REACT_ENVIRONMENTS.map(
1000
- (environment) => `{workspaceRoot}/dist/drop/react-app-${name}-${environment}.zip`
1001
- ),
1002
- options: {
1003
- command: `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');let z;${zipStatements}"`
943
+ function githubActionsYaml(agent, pythonPublishUrl2, registryKind = "azure-artifacts", ci = "github") {
944
+ const onMain = "github.event_name == 'push' && github.ref_name == 'main'";
945
+ const [npmAuthName, npmAuthValue] = npmAuthEnvVariable(
946
+ registryKind,
947
+ (name) => `\${{ secrets.${name} }}`
948
+ );
949
+ const githubReleases = ci === "github";
950
+ return `name: CI
951
+
952
+ # Generated by MoNecromanCI. Deliberately thin: Nx builds, 'nx release'
953
+ # versions from conventional commits and pushes ONLY a tag to main, and each
954
+ # app is packed into dist/drop/<type>-<name>.zip by its own 'package' target.
955
+ # The GitHub Actions equivalent of azure-pipelines.yml \u2014 see there for the
956
+ # fuller rationale; both stay hand-kept in lockstep, there is no shared template.
957
+
958
+ on:
959
+ push:
960
+ branches: [main]
961
+ pull_request:
962
+ branches: [main]
963
+
964
+ concurrency:
965
+ # One run at a time per ref, but \`cancel-in-progress\` is an **expression** on
966
+ # purpose rather than a flat \`true\`.
967
+ #
968
+ # A superseded PR run is pure waste, so cancel it. A run on \`main\` must never
969
+ # be cancelled: it publishes packages and pushes release tags, and killing it
970
+ # part-way can leave a tag pushed with the publish only half done \u2014 a state no
971
+ # rerun repairs cleanly, because the version is then already tagged. Those runs
972
+ # queue instead, which also stops two \`nx release\` invocations racing to create
973
+ # the same tag when two commits land close together.
974
+ group: \${{ github.workflow }}-\${{ github.ref }}
975
+ cancel-in-progress: \${{ github.event_name == 'pull_request' }}
976
+
977
+ permissions:
978
+ # Lets the release step push the version tag nx release creates back to main,
979
+ # and (github-only provider) create the GitHub Release itself.
980
+ contents: write
981
+
982
+ jobs:
983
+ ci:
984
+ runs-on: ${agent}
985
+ steps:
986
+ - uses: actions/checkout@v4
987
+ with:
988
+ fetch-depth: 0
989
+
990
+ # Version resolution needs the release tags.
991
+ - run: git fetch --all --prune --tags
992
+ name: Fetch branches and release tags
993
+
994
+ - run: git config user.name "github-actions[bot]" && git config user.email "github-actions[bot]@users.noreply.github.com"
995
+ name: Set the git identity used for release tags
996
+
997
+ - uses: actions/setup-node@v4
998
+ with:
999
+ node-version: ${NODE_VERSION}
1000
+ # Caches ~/.npm keyed on package-lock.json, so \`npm ci\` restores from
1001
+ # the local cache instead of re-downloading every tarball on every run.
1002
+ # Nothing to invalidate by hand: the action keys on the lockfile, so a
1003
+ # dependency change misses the cache and a no-op change hits it.
1004
+ cache: npm
1005
+
1006
+ # setup-node installs whatever npm the Node image bundles, and npm's major
1007
+ # is part of the contract: npm 11 reuses an already-installed tree instead
1008
+ # of re-resolving, so it applies \`overrides\` differently from npm 10. See
1009
+ # NPM_VERSION's remarks for the measurement.
1010
+ - run: npm install -g npm@${NPM_VERSION}
1011
+ name: Pin npm to the major mnci verifies against
1012
+
1013
+ - run: npm ci
1014
+ name: Install dependencies
1015
+ env:
1016
+ ${npmAuthName}: ${npmAuthValue}
1017
+
1018
+ # Fails ONLY on an advisory that has a published fix, at moderate or
1019
+ # above; anything upstream has not fixed is printed and passes. See
1020
+ # NPM_AUDIT_STEP's remarks for why that split replaced a warn-only step.
1021
+ - run: ${NPM_AUDIT_STEP}
1022
+ name: npm audit (fails on an actionable advisory)
1023
+
1024
+ # Installs the fixed Python toolchain (ruff, pytest, build, twine,
1025
+ # pip-audit) \u2014 written by 'mnci add' to requirements-dev.txt on the
1026
+ # first Python project. Plain pip, no uv/Poetry: portable guard skips
1027
+ # cleanly on a workspace with none.
1028
+ - run: ${PYTHON_INSTALL_GUARD}
1029
+ name: Install Python dependencies (ruff, pytest, build, twine, pip-audit)
1030
+
1031
+ # Editable-installs every Python project into one shared environment, the
1032
+ # pip-world counterpart of 'npm install' hoisting every workspace package
1033
+ # into one root node_modules \u2014 so a project that imports an internal lib
1034
+ # (normally vendored only at build time) can resolve that import at
1035
+ # lint/test time too. Portable guard skips cleanly on a workspace with none.
1036
+ - run: ${PYTHON_WORKSPACE_INSTALL_GUARD}
1037
+ name: Install Python project dependencies (editable, workspace-wide)
1038
+
1039
+ # Non-blocking, same reasoning as the npm audit step above. Runs after
1040
+ # the workspace-wide install so it scans the workspace's actual
1041
+ # dependency set, not just the fixed toolchain.
1042
+ - run: ${PIP_AUDIT_GUARD}
1043
+ name: pip-audit (non-blocking)
1044
+
1045
+ # Go, if the workspace has any: hosted runners ship the toolchain, so
1046
+ # only the module cache and the linter need seeding. Both guards skip
1047
+ # cleanly on a workspace with no root go.mod.
1048
+ - run: ${GO_MODULE_DOWNLOAD_GUARD}
1049
+ name: Download Go module dependencies
1050
+
1051
+ # golangci-lint is what the generated Go lint target actually runs (the
1052
+ # plugin's own default is 'go fmt', which only reformats). 'go install'
1053
+ # drops it in GOPATH/bin, which is not on PATH by default on a hosted
1054
+ # runner \u2014 so publish it for every later step in the job.
1055
+ - run: ${GOLANGCI_LINT_INSTALL_GUARD}
1056
+ name: Install golangci-lint
1057
+
1058
+ - run: ${GO_TOOL_PATH_GITHUB}
1059
+ name: Add Go tool bin to PATH
1060
+
1061
+ # Flutter, if the workspace has any. Unlike Python and Go, hosted runners
1062
+ # do NOT ship the Flutter SDK, so this installs it (shallow git clone at
1063
+ # a pinned tag) and puts it on PATH. All three guards skip cleanly on a
1064
+ # workspace with no root pubspec.yaml, and the install also skips when an
1065
+ # SDK is already on PATH.
1066
+ - run: ${FLUTTER_SDK_INSTALL_GUARD}
1067
+ name: Install the Flutter SDK (${FLUTTER_SDK_VERSION})
1068
+
1069
+ - run: ${FLUTTER_TOOL_PATH_GITHUB}
1070
+ name: Add the Flutter SDK to PATH
1071
+
1072
+ # One command resolves EVERY Dart dependency, internal and external, for
1073
+ # the whole workspace: the projects are pub workspace members, so this
1074
+ # writes a single root pubspec.lock they all share.
1075
+ - run: ${FLUTTER_PUB_GET_GUARD}
1076
+ name: Resolve Dart dependencies (one pub get for the whole workspace)
1077
+
1078
+ # Fails fast, with an unambiguous message, when a stale TypeScript project
1079
+ # reference (or another sync generator's drift) was never synced+committed
1080
+ # locally \u2014 sync.applyChanges (nx.json) only auto-applies interactively, so
1081
+ # CI still needs its own explicit, early check rather than surfacing this as
1082
+ # a confusing failure buried inside the build step below.
1083
+ - run: npx nx sync:check
1084
+ name: Verify the workspace is synced (run 'npx nx sync' locally and commit if this fails)
1085
+
1086
+ # The one verify step, and deliberately the only one: affected projects on
1087
+ # a pull request, EVERY project on anything else (a push to main included,
1088
+ # so a release is always verified in full). Every fallback \u2014 no target
1089
+ # branch, an unresolvable merge-base \u2014 takes the full path, because a run
1090
+ # that verifies too little still reports green.
1091
+ #
1092
+ # 'npm run lint' is 'nx run-many -t lint', a strict subset of the targets
1093
+ # below, so adding it back as its own step would only duplicate work \u2014 and
1094
+ # on a pull request it would re-lint every project, discarding the point
1095
+ # of this.
1096
+ - run: ${AFFECTED_OR_ALL_GUARD}
1097
+ name: Verify (affected on a PR, every project on main)
1098
+
1099
+ # Pack every app into dist/drop/<type>-<name>.zip via each app's 'package'
1100
+ # target. Portable guard: skip cleanly when the workspace has no apps yet.
1101
+ - run: ${PACK_APPS_GUARD}
1102
+ name: Pack all apps (one zip per app -> dist/drop)
1103
+ if: \${{ ${onMain} }}
1104
+
1105
+ - uses: actions/upload-artifact@v4
1106
+ if: \${{ ${onMain} }}
1107
+ with:
1108
+ name: drop
1109
+ path: dist/drop
1110
+ if-no-files-found: ignore
1111
+
1112
+ # Version + tag + publish, in one release, for npm (packages/*) AND Python
1113
+ # (python-packages/*) \u2014 conventional commits, tag-only push. Portable guard:
1114
+ # nx release errors on an empty scope, so skip cleanly when there is nothing
1115
+ # to release. When there are Python packages and an Azure feed, twine
1116
+ # publish credentials are exported (raw PAT, decoded from the base64 secret).${githubReleases ? `
1117
+ # This provider also creates a per-project GitHub Release (changelog
1118
+ # generated from conventional commits since that project's last tag),
1119
+ # which is why 'nx release' pushes the tag itself here \u2014 see
1120
+ # releaseConfig's remarks for why that is safe on this Nx version and
1121
+ # why every other provider combination keeps the explicit push step below.` : ""}
1122
+ - run: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2))}
1123
+ name: Release \u2014 version, tag${githubReleases ? ", publish and GitHub Release" : " and publish"} (npm + Python)
1124
+ if: \${{ ${onMain} }}
1125
+ env:
1126
+ ${npmAuthName}: ${npmAuthValue}${githubReleases ? `
1127
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}` : ""}
1128
+ ${githubReleases ? "" : `
1129
+ # nx release's own git push (release.git.push) is deliberately left off: it
1130
+ # only runs when a remote GitHub/GitLab Release is configured, which this
1131
+ # workflow never does, so it would never push the tag the step above just
1132
+ # created. Pushed explicitly, unconditionally (a no-op when nothing released)
1133
+ # once tagging is guaranteed to have already happened.
1134
+ - run: git push origin --tags
1135
+ name: Push release tags (nx release's own push never runs without a remote Release configured)
1136
+ if: \${{ ${onMain} }}
1137
+ `}`;
1138
+ }
1139
+ var PIP_PROJECT_GLOBS = ["apps", "python-packages", "libs"];
1140
+ var PUB_PROJECT_GLOBS = ["apps", "packages", "libs"];
1141
+ var ECOSYSTEM_MANIFESTS = {
1142
+ pip: ["pyproject.toml", "requirements.txt"],
1143
+ pub: ["pubspec.yaml"]
1144
+ };
1145
+ function hasEcosystemProject(workspaceRoot, roots, manifests) {
1146
+ return roots.some((root) => {
1147
+ const base = (0, import_node_path2.join)(workspaceRoot, root);
1148
+ if (!(0, import_node_fs2.existsSync)(base)) {
1149
+ return false;
1004
1150
  }
1005
- };
1006
- return targets;
1151
+ return (0, import_node_fs2.readdirSync)(base, { withFileTypes: true }).some(
1152
+ (entry) => entry.isDirectory() && manifests.some((manifest) => (0, import_node_fs2.existsSync)((0, import_node_path2.join)(base, entry.name, manifest)))
1153
+ );
1154
+ });
1007
1155
  }
1008
- function allowEnvFiles(workspaceRoot) {
1009
- const gitignorePath = (0, import_node_path8.join)(workspaceRoot, ".gitignore");
1010
- if (!fileExists(gitignorePath)) {
1011
- return;
1156
+ function dependabotBlock(ecosystem, roots) {
1157
+ return `
1158
+ - package-ecosystem: ${ecosystem}
1159
+ directories:
1160
+ ` + roots.map((root) => ` - "/${root}/*"
1161
+ `).join("") + " schedule:\n interval: weekly\n";
1162
+ }
1163
+ function dependabotConfig(workspaceRoot) {
1164
+ let config = `# Generated by MoNecromanCI. Weekly dependency-update PRs so vulnerable or
1165
+ # stale dependencies surface as a reviewable PR instead of only a push-time
1166
+ # warning.
1167
+ #
1168
+ # pip and pub appear here only once the workspace HAS a Python or Dart project.
1169
+ # An ecosystem entry whose directories match no manifest is a hard Dependabot
1170
+ # failure, not a no-op, so emitting them unconditionally cost every workspace
1171
+ # two red runs a week over projects it does not have.
1172
+ version: 2
1173
+ updates:
1174
+ - package-ecosystem: npm
1175
+ directory: "/"
1176
+ schedule:
1177
+ interval: weekly
1178
+
1179
+ - package-ecosystem: github-actions
1180
+ directory: "/"
1181
+ schedule:
1182
+ interval: weekly
1183
+ `;
1184
+ if (hasEcosystemProject(workspaceRoot, PIP_PROJECT_GLOBS, ECOSYSTEM_MANIFESTS.pip)) {
1185
+ config += dependabotBlock("pip", PIP_PROJECT_GLOBS);
1012
1186
  }
1013
- const marker = "# MoNecromanCI: keep the committed per-environment Vite config (public VITE_ values)";
1014
- const current = (0, import_node_fs5.readFileSync)(gitignorePath, "utf8");
1015
- if (current.includes(marker)) {
1016
- return;
1187
+ if (hasEcosystemProject(workspaceRoot, PUB_PROJECT_GLOBS, ECOSYSTEM_MANIFESTS.pub)) {
1188
+ config += dependabotBlock("pub", PUB_PROJECT_GLOBS);
1017
1189
  }
1018
- const allow = REACT_ENVIRONMENTS.map((environment) => `!apps/*/.env.${environment}`).join("\n");
1019
- writeFileEnsured(gitignorePath, `${current.replace(/\n*$/, "\n")}
1020
- ${marker}
1021
- ${allow}
1022
- `);
1190
+ return config;
1023
1191
  }
1024
- function addReactApp(workspaceRoot, name, stack) {
1025
- ensurePlugin(workspaceRoot, "@nx/react");
1026
- runNx(
1027
- [
1028
- "g",
1029
- "@nx/react:app",
1030
- `apps/${name}`,
1031
- "--bundler=vite",
1032
- `--unitTestRunner=${stack.testRunner}`,
1033
- "--linter=none",
1034
- "--style=css",
1035
- "--e2eTestRunner=none",
1036
- "--no-interactive"
1037
- ],
1038
- workspaceRoot
1039
- );
1040
- ensureAdmZip(workspaceRoot);
1041
- const reactAppRoot = (0, import_node_path8.join)(workspaceRoot, "apps", name);
1042
- for (const environment of REACT_ENVIRONMENTS) {
1043
- writeFileEnsured((0, import_node_path8.join)(reactAppRoot, `.env.${environment}`), reactEnvironmentFile(environment));
1192
+ var NX_SCAFFOLDING_TO_REMOVE = [".prettierrc", ".prettierrc.json", ".vscode"];
1193
+ function removeNxScaffolding(workspaceRoot) {
1194
+ for (const entry of NX_SCAFFOLDING_TO_REMOVE) {
1195
+ (0, import_node_fs2.rmSync)((0, import_node_path2.join)(workspaceRoot, entry), { recursive: true, force: true });
1044
1196
  }
1045
- allowEnvFiles(workspaceRoot);
1046
- addNxTargets((0, import_node_path8.join)(reactAppRoot, "package.json"), reactAppTargets(name));
1047
- removeGeneratedEslintConfig(workspaceRoot, `apps/${name}`);
1048
- registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:serve` });
1049
- }
1050
-
1051
- // src/commands/add/reactLib.ts
1052
- var import_node_path9 = require("path");
1053
- var REACT_LIB_BUNDLER = "rollup";
1054
- async function addReactLib(workspaceRoot, name, options, kindProvided, stack) {
1055
- ensurePlugin(workspaceRoot, "@nx/react");
1056
- const scope = options.scope ?? (kindProvided ? defaultScope(workspaceRoot) : await promptText("npm scope for the published package", defaultScope(workspaceRoot)));
1057
- runNx(
1058
- [
1059
- "g",
1060
- "@nx/react:library",
1061
- `packages/${name}`,
1062
- `--name=${name}`,
1063
- "--publishable",
1064
- `--importPath=${scope}/${name}`,
1065
- `--bundler=${REACT_LIB_BUNDLER}`,
1066
- `--unitTestRunner=${stack.testRunner}`,
1067
- "--linter=none",
1068
- "--no-interactive"
1069
- ],
1070
- workspaceRoot
1071
- );
1072
- const publishableManifest = (0, import_node_path9.join)(workspaceRoot, "packages", name, "package.json");
1073
- markPublic(publishableManifest);
1074
- repairPublishableManifest(publishableManifest);
1075
- removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
1076
- registerProjectCommands(workspaceRoot, name, { build: true });
1197
+ removeProjectEslintConfigs(workspaceRoot);
1077
1198
  }
1078
- function addReactInternalLib(workspaceRoot, name, stack) {
1079
- ensurePlugin(workspaceRoot, "@nx/react");
1080
- runNx(
1081
- [
1082
- "g",
1083
- "@nx/react:library",
1084
- `libs/${name}`,
1085
- `--name=${name}`,
1086
- `--bundler=${REACT_LIB_BUNDLER}`,
1087
- `--unitTestRunner=${stack.testRunner}`,
1088
- "--linter=none",
1089
- "--no-interactive"
1090
- ],
1091
- workspaceRoot
1092
- );
1093
- const privateManifest = (0, import_node_path9.join)(workspaceRoot, "libs", name, "package.json");
1094
- markPrivate(privateManifest);
1095
- repairPublishableManifest(privateManifest);
1096
- removeGeneratedEslintConfig(workspaceRoot, `libs/${name}`);
1097
- registerProjectCommands(workspaceRoot, name, { build: true });
1199
+ function removeIfPresent(path) {
1200
+ (0, import_node_fs2.rmSync)(path, { recursive: true, force: true });
1098
1201
  }
1099
-
1100
- // src/commands/add.ts
1101
- var PROJECT_KINDS = [
1102
- "react-app",
1103
- "react-lib",
1104
- "react-internal-lib",
1105
- "node-app",
1106
- "node-function-app",
1107
- "npm-lib",
1108
- "internal-lib",
1109
- "python-app",
1110
- "python-function-app",
1111
- "python-lib",
1112
- "python-internal-lib",
1113
- "python-vendor",
1114
- "go-app",
1115
- "go-function-app",
1116
- "go-lib",
1117
- "go-internal-lib",
1118
- "flutter-app",
1119
- "flutter-lib",
1120
- "flutter-internal-lib"
1121
- ];
1122
- async function runAdd(kind, name, options) {
1123
- const workspaceRoot = process.cwd();
1124
- if (!fileExists((0, import_node_path10.join)(workspaceRoot, "nx.json"))) {
1125
- throw new Error("No nx.json found here. Run `add` from the workspace root.");
1202
+ function removeProjectEslintConfigs(workspaceRoot) {
1203
+ const matches = (0, import_node_fs2.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
1204
+ cwd: workspaceRoot
1205
+ });
1206
+ for (const match of matches) {
1207
+ (0, import_node_fs2.rmSync)((0, import_node_path2.join)(workspaceRoot, match), { force: true });
1126
1208
  }
1127
- const stack = readWorkspaceStack(workspaceRoot);
1128
- const kindProvided = kind !== void 0;
1129
- const resolvedKind = kind ?? await (0, import_prompts5.select)({
1130
- message: "What kind of project?",
1131
- choices: PROJECT_KINDS.map((value) => ({ name: value, value }))
1209
+ }
1210
+ function applyOverlay(workspaceRoot, options, onProgress = () => {
1211
+ }) {
1212
+ const nxJsonPath = (0, import_node_path2.join)(workspaceRoot, "nx.json");
1213
+ const nxJson = readJson(nxJsonPath);
1214
+ const generators = {
1215
+ ...nxJson.generators,
1216
+ ...generatorDefaults(options.stack)
1217
+ };
1218
+ const sync = { ...nxJson.sync, ...SYNC_CONFIG };
1219
+ const mnci = { ...nxJson.mnci, ...mnciConfig(options) };
1220
+ const patched = withSharedGlobals(withEslintPlugin(withReleaseConfig(nxJson, options.ci)));
1221
+ onProgress("nx.json \u2014 release, sync, generators, shared inputs, mnci block");
1222
+ writeFileEnsured(nxJsonPath, toJson({ ...patched, generators, sync, mnci }));
1223
+ onProgress("package.json \u2014 root scripts, TS compilers, ESLint toolchain, overrides");
1224
+ const manifestPath = (0, import_node_path2.join)(workspaceRoot, "package.json");
1225
+ const manifest = readJson(manifestPath);
1226
+ const scripts = {
1227
+ ...manifest.scripts,
1228
+ ...rootScripts()
1229
+ };
1230
+ const existingDevDeps = manifest.devDependencies;
1231
+ const devDeps = withoutRetiredFormatterDependencies({
1232
+ ...existingDevDeps,
1233
+ ...TS_COMPILER_DEPENDENCIES,
1234
+ // The preset pins `nx` itself; the ESLint plugins must match it exactly.
1235
+ ...eslintToolchainDependencies(existingDevDeps?.nx ?? "latest")
1132
1236
  });
1133
- const resolvedName = name ?? await promptText("Project name");
1134
- assertValidProjectName(resolvedName, "Project name");
1135
- switch (resolvedKind) {
1136
- case "react-app": {
1137
- addReactApp(workspaceRoot, resolvedName, stack);
1138
- break;
1139
- }
1140
- case "react-lib": {
1141
- await addReactLib(workspaceRoot, resolvedName, options, kindProvided, stack);
1142
- break;
1143
- }
1144
- case "react-internal-lib": {
1145
- addReactInternalLib(workspaceRoot, resolvedName, stack);
1146
- break;
1147
- }
1148
- case "node-app": {
1149
- addNodeApp(workspaceRoot, resolvedName, stack, options.framework);
1150
- break;
1151
- }
1152
- case "node-function-app": {
1153
- addNodeFunctionApp(workspaceRoot, resolvedName, stack);
1154
- break;
1155
- }
1156
- case "npm-lib": {
1157
- await addNpmLib(workspaceRoot, resolvedName, options, kindProvided, stack);
1158
- break;
1159
- }
1160
- case "internal-lib": {
1161
- runNx(
1162
- [
1163
- "g",
1164
- "@nx/js:lib",
1165
- `libs/${resolvedName}`,
1166
- "--bundler=tsc",
1167
- `--unitTestRunner=${stack.testRunner}`,
1168
- "--linter=none",
1169
- "--no-interactive"
1170
- ],
1171
- workspaceRoot
1172
- );
1173
- markPrivate((0, import_node_path10.join)(workspaceRoot, "libs", resolvedName, "package.json"));
1174
- removeGeneratedEslintConfig(workspaceRoot, `libs/${resolvedName}`);
1175
- registerProjectCommands(workspaceRoot, resolvedName, { build: true });
1176
- break;
1177
- }
1178
- case "python-app": {
1179
- addPythonApp(workspaceRoot, resolvedName);
1180
- break;
1181
- }
1182
- case "python-function-app": {
1183
- addPythonFunctionApp(workspaceRoot, resolvedName);
1184
- break;
1185
- }
1186
- case "python-lib": {
1187
- addPythonLib(workspaceRoot, resolvedName);
1188
- break;
1189
- }
1190
- case "python-internal-lib": {
1191
- addPythonInternalLib(workspaceRoot, resolvedName);
1192
- break;
1193
- }
1194
- case "go-app": {
1195
- addGoApp(workspaceRoot, resolvedName);
1196
- break;
1197
- }
1198
- case "go-function-app": {
1199
- addGoFunctionApp(workspaceRoot, resolvedName);
1200
- break;
1201
- }
1202
- case "go-lib": {
1203
- addGoLib(workspaceRoot, resolvedName);
1204
- break;
1205
- }
1206
- case "go-internal-lib": {
1207
- addGoInternalLib(workspaceRoot, resolvedName);
1208
- break;
1209
- }
1210
- case "flutter-app": {
1211
- addFlutterApp(workspaceRoot, resolvedName);
1212
- break;
1213
- }
1214
- case "flutter-lib": {
1215
- addFlutterLib(workspaceRoot, resolvedName);
1216
- break;
1217
- }
1218
- case "flutter-internal-lib": {
1219
- addFlutterInternalLib(workspaceRoot, resolvedName);
1220
- break;
1221
- }
1222
- case "python-vendor": {
1223
- await addPythonVendor(workspaceRoot, resolvedName, options, kindProvided);
1224
- syncProjectReferences(workspaceRoot);
1225
- runFormatter(workspaceRoot);
1226
- return;
1227
- }
1228
- default: {
1229
- const exhaustive = resolvedKind;
1230
- throw new Error(
1231
- `Unknown project kind '${exhaustive}'. Expected one of: ${PROJECT_KINDS.join(", ")}.`
1232
- );
1237
+ const overrides = {
1238
+ ...manifest.overrides,
1239
+ ...ESLINT_PEER_OVERRIDES,
1240
+ ...SECURITY_OVERRIDES,
1241
+ // Last, and conditional — see reactExpressPeerOverride. At `mnci new` there
1242
+ // is no express yet, so this is a no-op; `mnci add node-app --framework
1243
+ // express` is what puts express in the root manifest, and syncs the override
1244
+ // itself. This line is what carries it across an `mnci upgrade`.
1245
+ ...reactExpressPeerOverride(manifest)
1246
+ };
1247
+ const existingNx = manifest.nx;
1248
+ const nx = {
1249
+ ...existingNx,
1250
+ includedScripts: existingNx?.includedScripts ?? [],
1251
+ targets: {
1252
+ ...existingNx?.targets,
1253
+ lint: ROOT_LINT_TARGET
1233
1254
  }
1255
+ };
1256
+ writeFileEnsured(
1257
+ manifestPath,
1258
+ toJson({
1259
+ ...manifest,
1260
+ name: `${options.scope}/source`,
1261
+ scripts,
1262
+ devDependencies: devDeps,
1263
+ overrides,
1264
+ nx
1265
+ })
1266
+ );
1267
+ onProgress(
1268
+ `.npmrc \u2014 ${options.registry.kind === "azure-artifacts" ? "Azure Artifacts feed routing and credentials" : "public npm registry auth"}`
1269
+ );
1270
+ writeFileEnsured((0, import_node_path2.join)(workspaceRoot, ".npmrc"), npmrcContent(options.registry, options.scope));
1271
+ onProgress("commitlint.config.mjs and .husky/commit-msg \u2014 conventional commit enforcement");
1272
+ writeFileEnsured((0, import_node_path2.join)(workspaceRoot, "commitlint.config.mjs"), COMMITLINT_CONFIG);
1273
+ const hookPath = (0, import_node_path2.join)(workspaceRoot, ".husky/commit-msg");
1274
+ writeFileEnsured(hookPath, COMMIT_MSG_HOOK);
1275
+ markExecutable(hookPath);
1276
+ onProgress("eslint.config.mjs \u2014 the shared lint AND formatting opinion");
1277
+ writeFileEnsured((0, import_node_path2.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG);
1278
+ for (const retired of RETIRED_FORMATTER_FILES) {
1279
+ removeIfPresent((0, import_node_path2.join)(workspaceRoot, retired));
1234
1280
  }
1235
- syncProjectReferences(workspaceRoot);
1236
- runFormatter(workspaceRoot);
1237
- logger.success(`Added ${resolvedKind} '${resolvedName}'.`);
1238
- }
1239
- function syncProjectReferences(workspaceRoot) {
1240
- logger.step("Syncing TypeScript project references (nx sync)");
1241
- if (runShell("npx", ["nx", "sync"], workspaceRoot) !== 0) {
1242
- logger.warn(
1243
- "nx sync did not complete \u2014 run `npx nx sync` yourself so cross-project imports resolve in your editor."
1281
+ onProgress(".devcontainer/devcontainer.json \u2014 a local toolchain matching CI");
1282
+ writeFileEnsured(
1283
+ (0, import_node_path2.join)(workspaceRoot, ".devcontainer/devcontainer.json"),
1284
+ devcontainerJson(options.workspaceName)
1285
+ );
1286
+ removeNxScaffolding(workspaceRoot);
1287
+ onProgress(
1288
+ `${options.workspaceName}.code-workspace \u2014 settings, extensions, launch configs`
1289
+ );
1290
+ const codeWorkspacePath = (0, import_node_path2.join)(workspaceRoot, `${options.workspaceName}.code-workspace`);
1291
+ const existing = readCodeWorkspace(codeWorkspacePath);
1292
+ writeFileEnsured(
1293
+ codeWorkspacePath,
1294
+ vscodeWorkspace(
1295
+ options.workspaceName,
1296
+ existing?.tasks,
1297
+ existing?.launch,
1298
+ existing?.settings
1299
+ )
1300
+ );
1301
+ if (options.workspaceName !== "undefined") {
1302
+ (0, import_node_fs2.rmSync)((0, import_node_path2.join)(workspaceRoot, "undefined.code-workspace"), { force: true });
1303
+ }
1304
+ const publishUrl = pythonPublishUrl(options.registry);
1305
+ if (options.ci === "azure" || options.ci === "both") {
1306
+ onProgress("azure-pipelines.yml \u2014 build, verify, pack and release");
1307
+ writeFileEnsured(
1308
+ (0, import_node_path2.join)(workspaceRoot, "azure-pipelines.yml"),
1309
+ azurePipelinesYaml(options.agent, options.variableGroup, publishUrl, options.registry.kind)
1244
1310
  );
1245
1311
  }
1246
- }
1247
- function readWorkspaceStack(workspaceRoot) {
1248
- const nxJson = readJson(
1249
- (0, import_node_path10.join)(workspaceRoot, "nx.json")
1250
- );
1251
- const stack = nxJson.mnci?.stack;
1252
- return {
1253
- testRunner: stack?.testRunner === "vitest" ? "vitest" : "jest"
1254
- };
1255
- }
1256
-
1257
- // src/commands/doctor.ts
1258
- var import_node_fs7 = require("fs");
1259
- var import_node_path12 = require("path");
1260
-
1261
- // src/overlay.ts
1262
- var import_node_fs6 = require("fs");
1263
- var import_node_path11 = require("path");
1264
- var DEFAULT_STACK = { testRunner: "jest" };
1265
- var TS_COMPILER_DEPENDENCIES = {
1266
- "@typescript/native": "npm:typescript@^7.0.2",
1267
- typescript: "npm:@typescript/typescript6@^6.0.2"
1268
- };
1269
- function registryUrl(registry) {
1270
- if (registry.kind === "azure-artifacts") {
1271
- return `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}/npm/registry/`;
1312
+ if (options.ci === "github" || options.ci === "both") {
1313
+ onProgress(".github/workflows/ci.yml and dependabot.yml");
1314
+ writeFileEnsured(
1315
+ (0, import_node_path2.join)(workspaceRoot, ".github/workflows/ci.yml"),
1316
+ githubActionsYaml(options.agent, publishUrl, options.registry.kind, options.ci)
1317
+ );
1318
+ writeFileEnsured((0, import_node_path2.join)(workspaceRoot, ".github/dependabot.yml"), dependabotConfig(workspaceRoot));
1272
1319
  }
1273
- return void 0;
1274
1320
  }
1275
- function npmrcContent(registry, scope) {
1276
- if (registry.kind === "npm") {
1277
- return `; Publish authentication for the public npm registry.
1278
- ;
1279
- ; NODE_AUTH_TOKEN is exported by the generated CI's release step; an unset value
1280
- ; is harmless locally, so no token is needed for day-to-day work.
1281
- //registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN}
1282
-
1283
- ; There is deliberately NO '${scope}:registry' line here. npmjs.org is already
1284
- ; the default registry, so routing the scope to it would change nothing \u2014 and
1285
- ; presenting that as protection against an accidental public publish would be
1286
- ; false, because the public registry is the intended target. (An earlier version
1287
- ; of this file made exactly that claim while emitting no routing line at all.)
1288
- ; To keep a scoped package off npmjs.org, use a private feed: generate with
1289
- ; --registry azure-artifacts, which does route the scope.
1290
- `;
1291
- }
1292
- const feedUrl = registryUrl(registry);
1293
- const feedKey = feedUrl.replace(/^https:/, "");
1294
- const feedShortKey = feedKey.replace("npm/registry/", "npm/");
1295
- return `; Publish + resolution routing for this workspace's own scope.
1296
- ;
1297
- ; '${scope}:registry' sends BOTH resolution and 'npm publish' of ${scope}/* to the
1298
- ; feed: npm prefers a scope's registry over the global one when publishing a
1299
- ; scoped package, so a ${scope}/* package cannot reach npmjs.org by accident.
1300
- ;
1301
- ; Only the scope is routed, on purpose. A global 'registry=' would send every
1302
- ; install through the feed as well, so 'npm ci' would need feed auth just to fetch
1303
- ; public packages.
1304
- ${scope}:registry=${feedUrl}
1305
1321
 
1306
- ; Feed credentials. PAT is the BASE64 value Azure Artifacts' "Connect to feed"
1307
- ; instructions give you, which is exactly what _password expects, so it is used
1308
- ; as-is. (twine wants the RAW token; the CI release step decodes it there.)
1309
- ; Azure ignores the username, and npm requires an email it never uses.
1310
- ;
1311
- ; NOT _authToken, and that is measured rather than assumed. The feed answers an
1312
- ; unauthenticated PUT to the publish endpoint with:
1313
- ; www-authenticate: Bearer authorization_uri=https://login.windows.net/<tenant>,
1314
- ; Basic realm="...", TFS-Federated
1315
- ; so its Bearer scheme wants an Entra ID access token, NOT a PAT. npm sends
1316
- ; _authToken verbatim as a Bearer header, so a PAT there is rejected with
1317
- ; "Unable to authenticate, your authentication token seems to be invalid". A PAT
1318
- ; authenticates through the Basic scheme, which is username/_password. Note the
1319
- ; Packaging REST API DOES accept a PAT as Bearer - do not generalise from it.
1320
- ;
1321
- ; Both path forms are keyed because npm walks only UP a URL when matching.
1322
- ${feedKey}:username=AzureArtifacts
1323
- ${feedKey}:_password=\${PAT}
1324
- ${feedKey}:email=npm-requires-this-and-never-uses-it
1325
- ${feedShortKey}:username=AzureArtifacts
1326
- ${feedShortKey}:_password=\${PAT}
1327
- ${feedShortKey}:email=npm-requires-this-and-never-uses-it
1328
- `;
1329
- }
1330
- function releaseConfig(ci) {
1331
- const githubReleases = ci === "github";
1332
- return {
1333
- projectsRelationship: "independent",
1334
- projects: ["packages/*", "python-packages/*", "!tag:type:go-lib"],
1335
- releaseTag: { pattern: "{projectName}@{version}" },
1336
- git: { commit: false, tag: true, push: githubReleases },
1337
- version: {
1338
- conventionalCommits: true,
1339
- fallbackCurrentVersionResolver: "disk",
1340
- // Build only what is being released. Without this, @nx/js:lib's generator
1341
- // defaults the pre-version command to building EVERY project, so a broken
1342
- // (or merely slow) app build would block releasing unrelated packages.
1343
- // Set here at `new` time it wins: the generator only fills this in when
1344
- // absent (it spreads the existing release.version over its default). Both
1345
- // globs are listed; `nx run-many` no-ops cleanly when one matches nothing.
1346
- preVersionCommand: "npx nx run-many -t build --projects=packages/*,python-packages/*"
1347
- },
1348
- changelog: githubReleases ? {
1349
- workspaceChangelog: false,
1350
- projectChangelogs: { createRelease: "github", file: false }
1351
- } : { workspaceChangelog: false }
1352
- };
1353
- }
1354
- var SYNC_CONFIG = { applyChanges: true };
1355
- var ESLINT_PLUGIN_CONFIG = {
1356
- plugin: "@nx/eslint/plugin",
1357
- options: { targetName: "lint" }
1358
- };
1359
- function pluginName(entry) {
1360
- return typeof entry === "string" ? entry : entry.plugin;
1361
- }
1362
- function withEslintPlugin(nxJson) {
1363
- const plugins = nxJson.plugins ?? [];
1364
- const registered = plugins.some((entry) => pluginName(entry) === ESLINT_PLUGIN_CONFIG.plugin);
1365
- return registered ? { ...nxJson, plugins } : { ...nxJson, plugins: [...plugins, ESLINT_PLUGIN_CONFIG] };
1322
+ // src/commands/add/shared.ts
1323
+ function hasPlugin(workspaceRoot, packageName) {
1324
+ const manifest = readJson((0, import_node_path3.join)(workspaceRoot, "package.json"));
1325
+ const installed = { ...manifest.dependencies, ...manifest.devDependencies };
1326
+ return Object.hasOwn(installed, packageName);
1366
1327
  }
1367
- var SHARED_GLOBAL_INPUTS = [
1368
- "{workspaceRoot}/eslint.config.mjs",
1369
- "{workspaceRoot}/tsconfig.base.json",
1370
- "{workspaceRoot}/package.json"
1371
- ];
1372
- function withSharedGlobals(nxJson) {
1373
- const namedInputs = nxJson.namedInputs ?? {};
1374
- const existing = namedInputs.sharedGlobals ?? [];
1375
- const missing = SHARED_GLOBAL_INPUTS.filter((entry) => !existing.includes(entry));
1376
- return {
1377
- ...nxJson,
1378
- namedInputs: { ...namedInputs, sharedGlobals: [...existing, ...missing] }
1379
- };
1328
+ function ensurePlugin(workspaceRoot, packageName) {
1329
+ if (hasPlugin(workspaceRoot, packageName)) {
1330
+ return;
1331
+ }
1332
+ logger.step(`Installing Nx plugin ${packageName}`);
1333
+ runNx(["add", packageName], workspaceRoot);
1380
1334
  }
1381
- function withReleaseConfig(nxJson, ci) {
1382
- return { ...nxJson, defaultBase: "main", release: releaseConfig(ci) };
1335
+ function markPrivate(manifestPath) {
1336
+ const manifest = readJson(manifestPath);
1337
+ writeFileEnsured(manifestPath, toJson({ ...manifest, private: true }));
1383
1338
  }
1384
- var COMMITLINT_CONFIG = `export default { extends: ['@commitlint/config-conventional'] }
1385
- `;
1386
- var COMMIT_MSG_HOOK = `npx --no -- commitlint --edit "$1"
1387
- `;
1388
- var ESLINT_CONFIG_VERSION = "^0.1.0";
1389
- function eslintConfigSpec() {
1390
- return process.env.MNCI_ESLINT_CONFIG_SPEC ?? ESLINT_CONFIG_VERSION;
1339
+ function markPublic(manifestPath) {
1340
+ const manifest = readJson(manifestPath);
1341
+ writeFileEnsured(manifestPath, toJson({ ...manifest, publishConfig: { access: "public" } }));
1391
1342
  }
1392
- var ESLINT_VERSION = "^10.8.0";
1393
- function devcontainerJson(workspaceName) {
1394
- return `${toJson({
1395
- name: workspaceName,
1396
- image: `mcr.microsoft.com/devcontainers/typescript-node:${NODE_VERSION}-bookworm`,
1397
- features: {
1398
- "ghcr.io/devcontainers/features/python:1": { version: "3.12" },
1399
- "ghcr.io/devcontainers/features/go:1": { version: "latest" }
1400
- },
1401
- // `npm ci` first: every guard after it runs through the workspace's own
1402
- // scripts and Nx, which do not exist until the install completes.
1403
- postCreateCommand: [
1404
- // Same pin the workflow applies, for the same reason — a container whose
1405
- // npm major differs from CI's resolves dependencies differently, which is
1406
- // precisely the drift this file exists to remove.
1407
- `npm install -g npm@${NPM_VERSION}`,
1408
- "npm ci",
1409
- "npm run python:install",
1410
- GOLANGCI_LINT_INSTALL_GUARD,
1411
- FLUTTER_SDK_INSTALL_GUARD
1412
- ].join(" && "),
1413
- // The same recommendations the `.code-workspace` file carries, so opening
1414
- // the folder in a container suggests the identical toolset.
1415
- customizations: {
1416
- vscode: { extensions: VSCODE_RECOMMENDED_EXTENSIONS }
1417
- }
1418
- })}
1419
- `;
1343
+ var WRONG_TYPES_PATH = "./dist/index.esm.d.ts";
1344
+ var ACTUAL_TYPES_PATH = "./dist/src/index.d.ts";
1345
+ var DECLARATION_MAP_EXCLUSION = "!**/*.d.ts.map";
1346
+ function repairPublishableManifest(manifestPath) {
1347
+ const manifest = readJson(manifestPath);
1348
+ if (manifest.types === WRONG_TYPES_PATH) {
1349
+ manifest.types = ACTUAL_TYPES_PATH;
1350
+ }
1351
+ const dot = manifest.exports?.["."];
1352
+ if (typeof dot === "object" && dot.types === WRONG_TYPES_PATH) {
1353
+ dot.types = ACTUAL_TYPES_PATH;
1354
+ }
1355
+ if (manifest.files && !manifest.files.includes(DECLARATION_MAP_EXCLUSION)) {
1356
+ manifest.files.push(DECLARATION_MAP_EXCLUSION);
1357
+ }
1358
+ writeFileEnsured(manifestPath, toJson(manifest));
1420
1359
  }
1421
- var ROOT_LINT_TARGET = {
1422
- executor: "nx:run-commands",
1423
- cache: true,
1424
- options: {
1425
- command: [
1426
- // `--cache` is the one ESLint speed option that actually pays here:
1427
- // measured best-of-3 on this repo, an unchanged re-run drops from 9,546ms
1428
- // to 2,835ms (3.4x). `--concurrency=auto` was measured too and REJECTED —
1429
- // it came out 8% SLOWER, because per-worker TypeScript type-service
1430
- // startup costs more than the parallelism saves. The cache file is
1431
- // per-machine and gitignored.
1432
- "eslint . --cache",
1433
- '--ignore-pattern "apps/**"',
1434
- '--ignore-pattern "libs/**"',
1435
- '--ignore-pattern "packages/**"',
1436
- '--ignore-pattern "python-packages/**"',
1437
- "--ignore-pattern package-lock.json"
1438
- ].join(" "),
1439
- cwd: "."
1360
+ function ensureAdmZip(workspaceRoot) {
1361
+ if (hasPlugin(workspaceRoot, "adm-zip")) {
1362
+ return;
1363
+ }
1364
+ logger.step("Installing the app packager (adm-zip)");
1365
+ if (runShell(
1366
+ "npm",
1367
+ ["install", "--save-dev", "adm-zip", "--no-audit", "--no-fund"],
1368
+ workspaceRoot
1369
+ ) !== 0) {
1370
+ throw new Error("npm install of adm-zip failed");
1440
1371
  }
1441
- };
1442
- var ESLINT_PEER_OVERRIDES = {
1443
- "eslint-plugin-jsx-a11y": { eslint: "$eslint" },
1444
- // Scoped to EACH named parent, not top-level and not `nx` alone.
1445
- //
1446
- // `nx` alone was the first attempt and it did not work: the e2e's audit step
1447
- // still reported all six advisories in a freshly generated workspace, because
1448
- // `@nx/js`, `@nx/eslint`, `@nx/eslint-plugin` and `@nx/workspace` are their own
1449
- // dependents there, and an override scoped to `nx` never reaches a sibling's
1450
- // dependencies. Those four are exactly the packages npm audit named.
1451
- //
1452
- // Top-level would be worse than wrong. A tree with `minimatch@3` legitimately
1453
- // carries `brace-expansion@1.1.18` (and `@2.1.4` elsewhere) — this repo has
1454
- // both — and forcing those to v5 breaks them. So the blast radius is one
1455
- // dependency edge per named parent, and a test asserts there is no top-level
1456
- // entry.
1457
- nx: { "brace-expansion": "^5.0.9" },
1458
- "@nx/js": { "brace-expansion": "^5.0.9" },
1459
- "@nx/eslint": { "brace-expansion": "^5.0.9" },
1460
- "@nx/eslint-plugin": { "brace-expansion": "^5.0.9" },
1461
- "@nx/workspace": { "brace-expansion": "^5.0.9" },
1462
- // `postcss` → `nanoid` reaches a generated workspace through `@nx/rollup`,
1463
- // which every publishable `npm-lib` gets. GHSA-2v37-7h3g-55p8 is high, fixed in
1464
- // 3.3.18, and the fix is a patch — so unlike the entry above this one is not
1465
- // even a trade, just a version nobody had bumped yet.
1466
- postcss: { nanoid: "^3.3.18" }
1467
- };
1468
- var SECURITY_OVERRIDES = {
1469
- "brace-expansion@4.0.0 - 5.0.8": "^5.0.9"
1470
- };
1471
- function eslintToolchainDependencies(nxVersion) {
1472
- return {
1473
- eslint: ESLINT_VERSION,
1474
- "@nx/eslint": nxVersion,
1475
- "@nx/eslint-plugin": nxVersion,
1476
- "@mnci/eslint-config": eslintConfigSpec()
1477
- };
1478
1372
  }
1479
- var RETIRED_FORMATTER_FILES = [
1480
- ".prettierrc",
1481
- ".prettierrc.json",
1482
- ".prettierrc.mjs",
1483
- ".prettierignore",
1484
- "oxlint.config.ts",
1485
- ".oxfmtrc.json"
1486
- ];
1487
- function withoutRetiredFormatterDependencies(devDeps) {
1488
- const retired = /* @__PURE__ */ new Set([
1489
- "prettier",
1490
- "eslint-config-prettier",
1491
- "oxlint",
1492
- "oxfmt",
1493
- "@mnci/oxlint-config"
1494
- ]);
1495
- return Object.fromEntries(Object.entries(devDeps).filter(([name]) => !retired.has(name)));
1373
+ function defaultScope(workspaceRoot) {
1374
+ const { name } = readJson((0, import_node_path3.join)(workspaceRoot, "package.json"));
1375
+ const base = (name.startsWith("@") ? name.slice(1) : name).split("/", 1)[0];
1376
+ return `@${base}`;
1496
1377
  }
1497
- var ESLINT_BLOCK_INVENTORY = `// WHAT IS IN HERE. Each line is one config block, by the \`name\` it carries.
1498
- //
1499
- // mnci/ignores paths never linted (dist, coverage, .venv, \u2026)
1500
- // mnci/base JS/TS correctness \u2014 @eslint/js, eslint-plugin-unicorn,
1501
- // -promise, -n, -unused-imports
1502
- // typescript-eslint/* typescript-eslint's own recommended blocks
1503
- // mnci/typescript* TS rules on top of them, no type information needed
1504
- // mnci/type-aware* the rules that DO read types (no-floating-promises and
1505
- // friends), scoped to {apps,libs,packages}/*/src
1506
- // mnci/import-graph import cycles \u2014 eslint-plugin-import-x
1507
- // mnci/react JSX/TSX \u2014 @eslint-react/eslint-plugin,
1508
- // eslint-plugin-react-hooks, -react-refresh, -jsx-a11y
1509
- // mnci/regexp* regex correctness \u2014 eslint-plugin-regexp
1510
- // mnci/json mnci/jsonc mnci/json5
1511
- // eslint-plugin-jsonc \u2014 comments are allowed in .jsonc
1512
- // and tsconfig.json, forbidden in plain .json
1513
- // mnci/yaml* eslint-plugin-yml \u2014 your CI pipeline files
1514
- // mnci/toml/base* eslint-plugin-toml, PARSER ONLY: a malformed
1515
- // pyproject.toml is a syntax error, nothing is styled
1516
- // mnci/markdown @eslint/markdown
1517
- // mnci/css @eslint/css
1518
- // mnci/html @html-eslint/eslint-plugin
1519
- // mnci/tests *.spec/*.test relaxations \u2014 eslint-plugin-jest
1520
- // (Vitest's globals too; the two stacks share them)
1521
- // mnci/nx-dependency-checks @nx/eslint-plugin, on publishable packages' manifests
1522
- // mnci/standard JavaScript Standard Style as ESLint rules \u2014 the
1523
- // whole formatting opinion. Composed LAST, on
1524
- // purpose: nothing may follow that disables it.
1525
- //
1526
- // To list them as ESLint actually resolves them: npx eslint --inspect-config
1527
- `;
1528
- var ESLINT_CONFIG = `import mnci from '@mnci/eslint-config'
1529
-
1530
- ${ESLINT_BLOCK_INVENTORY}//
1531
- // TO OVERRIDE a rule, append a block AFTER the spread \u2014 later blocks win, so one
1532
- // of your own beats anything above it. Give it a name, so the inspector shows
1533
- // where the change came from:
1534
- //
1535
- // export default [
1536
- // ...mnci({ workspaceRoot: import.meta.dirname }),
1537
- // {
1538
- // name: 'local/legacy-app-allows-any',
1539
- // files: ['apps/legacy/**/*.ts'],
1540
- // rules: { '@typescript-eslint/no-explicit-any': 'off' }
1541
- // }
1542
- // ]
1543
- //
1544
- // Do NOT edit @mnci/eslint-config inside node_modules, and do not fork it: it is
1545
- // a dependency, so \`npm update\` brings rule fixes in the way it brings any
1546
- // other. An override here survives that; an edit to the package does not.
1547
- //
1548
- // FORMATTING IS LINTING HERE. There is no Prettier, no oxfmt and no
1549
- // \`format:check\` \u2014 \`npm run lint\` reports indentation, quotes and spacing as
1550
- // ordinary errors, and \`npm run format\` is \`eslint . --fix\`. So do not add a
1551
- // formatter: whichever one you pick will disagree with the \`mnci/standard\`
1552
- // block above, and because a formatter runs on save it wins silently, leaving
1553
- // \`lint\` to fail on files you never edited by hand.
1554
- //
1555
- // That also applies to the editor. Installing a Prettier or oxfmt extension is
1556
- // enough on its own \u2014 neither needs a config file, and with none present they
1557
- // format against their own defaults (semicolons, double quotes), which is the
1558
- // inverse of Standard.
1559
- export default mnci({ workspaceRoot: import.meta.dirname })
1560
- `;
1561
- var VSCODE_RECOMMENDED_EXTENSIONS = [
1562
- "dbaeumer.vscode-eslint",
1563
- "nrwl.angular-console",
1564
- "firsttris.vscode-jest-runner"
1565
- ];
1566
- var FORMATTED_LANGUAGES = [
1567
- "javascript",
1568
- "javascriptreact",
1569
- "typescript",
1570
- "typescriptreact",
1571
- "json",
1572
- "jsonc",
1573
- "yaml",
1574
- "markdown",
1575
- "css",
1576
- "html",
1577
- // TOML is here for PARSING, not formatting, and the distinction is measured
1578
- // rather than assumed: `eslint --fix` leaves a badly-laid-out `.toml`
1579
- // byte-identical (the TOML block is `flat/base`, parser only — `flat/standard`
1580
- // reported six errors on the `pyproject.toml` mnci itself generates), but it
1581
- // DOES report `Parsing error: ...` on a malformed one.
1582
- //
1583
- // So the entry buys two things. `eslint.validate` gains toml, so a broken
1584
- // `pyproject.toml` is flagged in the editor rather than at the next CI run.
1585
- // And pinning the formatter to ESLint means format-on-save does nothing to the
1586
- // file instead of handing it to whichever TOML formatter the user happens to
1587
- // have installed — the same reasoning as every other entry here: nothing
1588
- // reformats against an opinion no gate checks.
1589
- //
1590
- // Impossible under the Prettier stack, which is why it is only here now:
1591
- // `npx prettier` on a `.toml` exits with "No parser could be inferred".
1592
- "toml"
1593
- ];
1594
- function vscodeSettings() {
1595
- const formatter = "dbaeumer.vscode-eslint";
1596
- const eslintLanguages = [...FORMATTED_LANGUAGES];
1597
- return {
1598
- "eslint.validate": eslintLanguages,
1599
- "editor.codeActionsOnSave": {
1600
- "source.fixAll.eslint": "explicit"
1601
- },
1602
- "editor.defaultFormatter": formatter,
1603
- "editor.formatOnSave": true,
1604
- // Every language spelled out, and the global default above is NOT enough on
1605
- // its own — which is the bug this fixes rather than a belt-and-braces habit.
1606
- //
1607
- // VS Code resolves a language-specific setting ahead of a general one, and it
1608
- // does that comparison BEFORE scope. So a `[typescript]` block in someone's
1609
- // USER settings — left over from any other project — outranks this file's
1610
- // workspace-level `editor.defaultFormatter`, and format-on-save quietly uses
1611
- // that other formatter instead — and nothing reports it, because the gate
1612
- // and the editor are looking at different tools. That is how it was found:
1613
- // reported from a real workspace, not deduced from the docs.
1614
- //
1615
- // Reported from a real workspace where `.ts` files were not being formatted
1616
- // while `.json`/`.jsonc`/`.yaml` were — exactly the three that had explicit
1617
- // entries here and nothing else did.
1618
- ...Object.fromEntries(
1619
- FORMATTED_LANGUAGES.map((language) => [
1620
- `[${language}]`,
1621
- { "editor.defaultFormatter": formatter }
1622
- ])
1623
- )
1624
- };
1378
+ function addNxTargets(manifestPath, newTargets) {
1379
+ const manifest = fileExists(manifestPath) ? readJson(manifestPath) : {};
1380
+ const nx = manifest.nx ?? {};
1381
+ const targets = nx.targets ?? {};
1382
+ writeFileEnsured(
1383
+ manifestPath,
1384
+ toJson({ ...manifest, nx: { ...nx, targets: { ...targets, ...newTargets } } })
1385
+ );
1625
1386
  }
1626
- var LAUNCH_CONFIG_PREFIX = "mnci: ";
1627
- var LAUNCH_CONFIGURATIONS = ["build", "test", "lint", "typecheck"];
1628
- function launchConfigurations(workspaceName) {
1629
- return LAUNCH_CONFIGURATIONS.map((script, index) => ({
1630
- type: "node-terminal",
1631
- request: "launch",
1632
- name: `${LAUNCH_CONFIG_PREFIX}${script}`,
1633
- command: `npm run ${script}`,
1634
- // Scoped by folder name rather than a bare ${workspaceFolder}: that variable is
1635
- // ambiguous once a second folder is added to the workspace, and VS Code then
1636
- // refuses to resolve it.
1637
- cwd: `\${workspaceFolder:${workspaceName}}`,
1638
- // One shared group keeps the four together in the dropdown; a per-entry group
1639
- // would make four groups of one. `order` holds them in verify order.
1640
- presentation: { group: "mnci", order: index + 1 }
1641
- }));
1387
+ var ESLINT_CONFIG_FILENAMES = [
1388
+ "eslint.config.js",
1389
+ "eslint.config.mjs",
1390
+ "eslint.config.cjs",
1391
+ "eslint.config.ts",
1392
+ "eslint.config.mts",
1393
+ "eslint.config.cts"
1394
+ ];
1395
+ function removeGeneratedEslintConfig(workspaceRoot, projectRoot) {
1396
+ for (const filename of ESLINT_CONFIG_FILENAMES) {
1397
+ (0, import_node_fs3.rmSync)((0, import_node_path3.join)(workspaceRoot, projectRoot, filename), { force: true });
1398
+ }
1399
+ (0, import_node_fs3.rmSync)((0, import_node_path3.join)(workspaceRoot, ".vscode"), { recursive: true, force: true });
1642
1400
  }
1643
- function vscodeWorkspace(workspaceName, existingTasks, existingLaunch, existingSettings) {
1644
- const userConfigurations = (existingLaunch?.configurations ?? []).filter(
1645
- (configuration) => !String(configuration.name ?? "").startsWith(LAUNCH_CONFIG_PREFIX)
1401
+ function findCodeWorkspaceFile(workspaceRoot) {
1402
+ try {
1403
+ const entry = (0, import_node_fs3.readdirSync)(workspaceRoot).find((file) => file.endsWith(".code-workspace"));
1404
+ return entry ? (0, import_node_path3.join)(workspaceRoot, entry) : void 0;
1405
+ } catch {
1406
+ return void 0;
1407
+ }
1408
+ }
1409
+ function projectTask(name, kind) {
1410
+ const script = `${name}:${kind}`;
1411
+ const base = { label: `${name}: ${kind}`, type: "npm", script, problemMatcher: [] };
1412
+ return kind === "start" ? { ...base, isBackground: true } : { ...base, group: kind };
1413
+ }
1414
+ function registerProjectCommands(workspaceRoot, name, commands) {
1415
+ const scripts = {
1416
+ [`${name}:qa`]: `nx run ${name}:lint && nx run ${name}:test`
1417
+ };
1418
+ if (commands.build) {
1419
+ scripts[`${name}:build`] = `nx run ${name}:build`;
1420
+ }
1421
+ if (commands.start) {
1422
+ scripts[`${name}:start`] = commands.start;
1423
+ }
1424
+ const dependabotPath = (0, import_node_path3.join)(workspaceRoot, ".github/dependabot.yml");
1425
+ if ((0, import_node_fs3.existsSync)(dependabotPath)) {
1426
+ writeFileEnsured(dependabotPath, dependabotConfig(workspaceRoot));
1427
+ }
1428
+ const manifestPath = (0, import_node_path3.join)(workspaceRoot, "package.json");
1429
+ const manifest = readJson(manifestPath);
1430
+ const existingScripts = manifest.scripts ?? {};
1431
+ const overrides = {
1432
+ ...manifest.overrides,
1433
+ ...reactExpressPeerOverride(manifest)
1434
+ };
1435
+ writeFileEnsured(
1436
+ manifestPath,
1437
+ toJson({
1438
+ ...manifest,
1439
+ scripts: { ...existingScripts, ...scripts },
1440
+ ...Object.keys(overrides).length > 0 && { overrides }
1441
+ })
1646
1442
  );
1647
- const settings = { ...existingSettings, ...vscodeSettings() };
1648
- return JSON.stringify(
1649
- {
1650
- folders: [{ path: ".", name: workspaceName }],
1651
- settings,
1652
- extensions: { recommendations: VSCODE_RECOMMENDED_EXTENSIONS },
1443
+ const codeWorkspacePath = findCodeWorkspaceFile(workspaceRoot);
1444
+ if (!codeWorkspacePath) {
1445
+ return;
1446
+ }
1447
+ const workspaceFile = readCodeWorkspace(codeWorkspacePath) ?? {};
1448
+ const label = (task) => task.label ?? "";
1449
+ const existingTasks = (workspaceFile.tasks?.tasks ?? []).filter(
1450
+ (task) => !label(task).startsWith(`${name}: `)
1451
+ );
1452
+ const newTasks = [
1453
+ projectTask(name, "qa"),
1454
+ ...commands.build ? [projectTask(name, "build")] : [],
1455
+ ...commands.start ? [projectTask(name, "start")] : []
1456
+ ];
1457
+ writeFileEnsured(
1458
+ codeWorkspacePath,
1459
+ toJson({
1460
+ ...workspaceFile,
1653
1461
  tasks: {
1654
- version: existingTasks?.version ?? "2.0.0",
1655
- tasks: existingTasks?.tasks ?? []
1656
- },
1657
- launch: {
1658
- version: existingLaunch?.version ?? "0.2.0",
1659
- configurations: [...launchConfigurations(workspaceName), ...userConfigurations]
1462
+ version: workspaceFile.tasks?.version ?? "2.0.0",
1463
+ tasks: [...existingTasks, ...newTasks]
1660
1464
  }
1661
- },
1662
- null,
1663
- 2
1465
+ })
1664
1466
  );
1665
1467
  }
1666
- var ROOT_SCRIPTS = {
1667
- build: "nx run-many -t build",
1668
- lint: "nx run-many -t lint",
1669
- test: "nx run-many -t test",
1670
- typecheck: "nx run-many -t typecheck",
1671
- affected: "nx affected -t lint,typecheck,test,build",
1672
- graph: "nx graph",
1673
- "release:preview": "nx release --dry-run",
1674
- prepare: "husky"
1675
- };
1676
- function rootScripts() {
1677
- return {
1678
- ...ROOT_SCRIPTS,
1679
- // ESLint is the formatter, so `format` is `--fix` and there is no separate
1680
- // `format:check`: `lint` already reports formatting as ordinary errors.
1681
- // Carrying a second script that ran the same tool twice would just make a
1682
- // CI run slower for no extra coverage.
1683
- format: "eslint . --fix --cache",
1684
- "python:install": `${PYTHON_INSTALL_GUARD} && ${PYTHON_WORKSPACE_INSTALL_GUARD}`
1685
- };
1468
+
1469
+ // src/commands/add/flutter.ts
1470
+ function addProjectJsonTargets(projectJsonPath, newTargets) {
1471
+ const project = readJson(projectJsonPath);
1472
+ const targets = project.targets ?? {};
1473
+ writeFileEnsured(projectJsonPath, toJson({ ...project, targets: { ...targets, ...newTargets } }));
1686
1474
  }
1687
- function generatorDefaults(stack) {
1688
- const shared = {
1689
- // `none`, not `eslint`, and the workspace is still fully linted. The root
1690
- // config plus `@nx/eslint/plugin` ({@link ESLINT_PLUGIN_CONFIG}) give every
1691
- // project its `lint` target; `eslint` here would only make the generator
1692
- // scaffold a per-project config mnci deletes anyway — and drag in
1693
- // `eslint-plugin-import@2.31.0`, which peer-caps at ESLint 9 and breaks the
1694
- // install outright on this workspace's ESLint 10.
1695
- linter: "none",
1696
- unitTestRunner: stack.testRunner
1697
- };
1475
+ function flutterAppStartTarget(name) {
1698
1476
  return {
1699
- "@nx/react:application": shared,
1700
- "@nx/react:library": shared,
1701
- "@nx/js:library": shared
1477
+ executor: "nx:run-commands",
1478
+ continuous: true,
1479
+ options: { command: "flutter run -d chrome", cwd: `apps/${name}` }
1702
1480
  };
1703
1481
  }
1704
- function mnciConfig(options) {
1705
- return {
1706
- // Persisted so `mnci upgrade` can name the `<name>.code-workspace` file it
1707
- // rewrites. Its absence is why upgrade used to write a file literally called
1708
- // `undefined.code-workspace` and therefore never refreshed the real one —
1709
- // see `resolveWorkspaceName` in `commands/upgrade.ts`, which still needs a
1710
- // fallback chain for workspaces generated before this field existed.
1711
- workspaceName: options.workspaceName,
1712
- scope: options.scope,
1713
- registry: options.registry,
1714
- agent: options.agent,
1715
- variableGroup: options.variableGroup,
1716
- ci: options.ci,
1717
- stack: { testRunner: options.stack.testRunner }
1718
- };
1482
+ function ensureFlutter(workspaceRoot) {
1483
+ if (runShell("flutter", ["--version"], workspaceRoot) !== 0) {
1484
+ throw new Error(
1485
+ "Flutter not found. Install the Flutter SDK first (3.27+, for Dart 3.6+ pub workspaces): https://docs.flutter.dev/get-started/install"
1486
+ );
1487
+ }
1719
1488
  }
1720
- function readMnciConfig(workspaceRoot) {
1721
- const nxJson = readJson((0, import_node_path11.join)(workspaceRoot, "nx.json"));
1722
- return nxJson.mnci ?? {};
1489
+ function nxFlutterPluginSpec() {
1490
+ return process.env.MNCI_NX_FLUTTER_SPEC ?? "@mnci/nx-flutter";
1723
1491
  }
1724
- function pythonPublishUrl(registry) {
1725
- if (registry.kind === "azure-artifacts") {
1726
- return `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}/pypi/upload/`;
1492
+ function ensureNxFlutterPlugin(workspaceRoot) {
1493
+ if (hasPlugin(workspaceRoot, "@mnci/nx-flutter")) {
1494
+ return;
1495
+ }
1496
+ const spec = nxFlutterPluginSpec();
1497
+ logger.step(`Installing the Flutter toolchain plugin (${spec})`);
1498
+ if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
1499
+ throw new Error("npm install of @mnci/nx-flutter failed");
1727
1500
  }
1728
- return void 0;
1729
1501
  }
1730
- var PYTHON_INSTALL_GUARD = `node -e "if(!require('node:fs').existsSync('requirements-dev.txt')){console.log('No Python projects - skipping.');process.exit(0)}const py=process.platform==='win32'?'python':'python3';process.exit(require('node:child_process').spawnSync(py+' -m pip install -r requirements-dev.txt',{stdio:'inherit',shell:true}).status ?? 1)"`;
1731
- var PYTHON_WORKSPACE_INSTALL_GUARD = `node -e "const fs=require('node:fs'),path=require('node:path');const editableDirs=[...fs.globSync('apps/*/pyproject.toml'),...fs.globSync('python-packages/*/pyproject.toml'),...fs.globSync('libs/*/pyproject.toml')].map((p)=>path.dirname(p));const requirementsFiles=fs.globSync('apps/*/requirements.txt');if(editableDirs.length===0&&requirementsFiles.length===0){console.log('No Python projects - skipping.');process.exit(0)}const args=['-m','pip','install','--quiet',...editableDirs.flatMap((d)=>['-e',d]),...requirementsFiles.flatMap((f)=>['-r',f])];const py=process.platform==='win32'?'python':'python3';process.exit(require('node:child_process').spawnSync(py,args,{stdio:'inherit'}).status ?? 1)"`;
1732
- var PIP_AUDIT_GUARD = `node -e "if(!require('node:fs').existsSync('requirements-dev.txt')){console.log('No Python projects - skipping.');process.exit(0)}const py=process.platform==='win32'?'python':'python3';require('node:child_process').spawnSync(py,['-m','pip_audit'],{stdio:'inherit'});process.exit(0)"`;
1733
- var GO_MODULE_DOWNLOAD_GUARD = `node -e "if(!require('node:fs').existsSync('go.mod')){console.log('No Go projects - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('go',['mod','download'],{stdio:'inherit'}).status ?? 1)"`;
1734
- var GOLANGCI_LINT_INSTALL_GUARD = `node -e "const fs=require('node:fs'),cp=require('node:child_process');if(!fs.existsSync('go.mod')){console.log('No Go projects - skipping.');process.exit(0)}if(cp.spawnSync('golangci-lint',['--version'],{stdio:'ignore'}).status===0){console.log('golangci-lint already installed - skipping.');process.exit(0)}process.exit(cp.spawnSync('go',['install','github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest'],{stdio:'inherit'}).status ?? 1)"`;
1735
- var GO_TOOL_PATH_PRELUDE = "const fs=require('node:fs'),cp=require('node:child_process');if(!fs.existsSync('go.mod')){console.log('No Go projects - skipping.');process.exit(0)}const r=cp.spawnSync('go',['env','GOPATH'],{encoding:'utf8'});if(r.status!==0){console.log('Could not resolve GOPATH - skipping.');process.exit(0)}const bin=require('node:path').join(r.stdout.trim(),'bin');";
1736
- var GO_TOOL_PATH_AZURE = `node -e "${GO_TOOL_PATH_PRELUDE}console.log('##vso[task.prependpath]'+bin)"`;
1737
- var GO_TOOL_PATH_GITHUB = `node -e "${GO_TOOL_PATH_PRELUDE}if(!process.env.GITHUB_PATH){console.log('Not running in GitHub Actions - skipping.');process.exit(0)}fs.appendFileSync(process.env.GITHUB_PATH,bin+${String.raw`'\n'`})"`;
1738
- var FLUTTER_SDK_VERSION = "3.44.8";
1739
- var NODE_VERSION = "24";
1740
- var NPM_VERSION = "11";
1741
- var FLUTTER_SDK_DIRECTORY_EXPRESSION = `require('node:path').join(require('node:os').homedir(),'.mnci-flutter-${FLUTTER_SDK_VERSION}')`;
1742
- var FLUTTER_SDK_INSTALL_GUARD = `node -e "const fs=require('node:fs'),cp=require('node:child_process');if(!fs.existsSync('pubspec.yaml')){console.log('No Flutter projects - skipping.');process.exit(0)}if(cp.spawnSync('flutter',['--version'],{stdio:'ignore',shell:process.platform==='win32'}).status===0){console.log('Flutter SDK already on PATH - skipping.');process.exit(0)}const sdk=${FLUTTER_SDK_DIRECTORY_EXPRESSION};if(fs.existsSync(sdk)){console.log('Flutter SDK already installed at '+sdk+' - skipping.');process.exit(0)}process.exit(cp.spawnSync('git',['clone','--depth','1','--branch','${FLUTTER_SDK_VERSION}','https://github.com/flutter/flutter.git',sdk],{stdio:'inherit'}).status ?? 1)"`;
1743
- var FLUTTER_TOOL_PATH_PRELUDE = `const fs=require('node:fs'),path=require('node:path');if(!fs.existsSync('pubspec.yaml')){console.log('No Flutter projects - skipping.');process.exit(0)}const sdk=${FLUTTER_SDK_DIRECTORY_EXPRESSION};if(!fs.existsSync(sdk)){console.log('Flutter SDK was not installed by mnci (already on PATH) - skipping.');process.exit(0)}const bin=path.join(sdk,'bin');`;
1744
- var FLUTTER_TOOL_PATH_AZURE = `node -e "${FLUTTER_TOOL_PATH_PRELUDE}console.log('##vso[task.prependpath]'+bin)"`;
1745
- var FLUTTER_TOOL_PATH_GITHUB = `node -e "${FLUTTER_TOOL_PATH_PRELUDE}if(!process.env.GITHUB_PATH){console.log('Not running in GitHub Actions - skipping.');process.exit(0)}fs.appendFileSync(process.env.GITHUB_PATH,bin+${String.raw`'\n'`})"`;
1746
- var FLUTTER_PUB_GET_GUARD = `node -e "const fs=require('node:fs');if(!fs.existsSync('pubspec.yaml')){console.log('No Flutter projects - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('flutter',['pub','get'],{stdio:'inherit',shell:process.platform==='win32'}).status ?? 1)"`;
1747
- var NPM_AUDIT_STEP = `node -e "const cp=require('node:child_process');const r=cp.spawnSync('npm',['audit','--json'],{encoding:'utf8',shell:process.platform==='win32',maxBuffer:33554432});let d;try{d=JSON.parse(r.stdout)}catch{console.log('npm audit produced no JSON (exit '+r.status+') - not blocking on a broken audit.');process.exit(0)}const all=Object.values(d.vulnerabilities||{});const BLOCK=['critical','high','moderate'];const act=all.filter(v=>v.fixAvailable&&BLOCK.includes(v.severity));const rest=all.filter(v=>!act.includes(v));for(const v of rest)console.log(' note ['+v.severity+'] '+v.name+(v.fixAvailable?' - fix available, below the blocking threshold':' - NO fix available upstream, nothing to do here'));if(act.length===0){console.log('npm audit - '+all.length+' advisory(ies), none actionable at moderate or above.');process.exit(0)}for(const v of act)console.log(' BLOCKING ['+v.severity+'] '+v.name+' - fix available'+(v.fixAvailable&&v.fixAvailable.isSemVerMajor?' (semver-major)':''));console.log('Each has a published fix. Add a targeted overrides entry in package.json rather than npm audit fix --force.');process.exit(1)"`;
1748
- var VERIFY_TARGETS = "lint,typecheck,test,build";
1749
- var AFFECTED_OR_ALL_GUARD = `node -e "const cp=require('node:child_process');const T='${VERIFY_TARGETS}';const all=()=>process.exit(cp.spawnSync('npx nx run-many -t '+T,{stdio:'inherit',shell:true}).status ?? 1);const ref=process.env.GITHUB_BASE_REF||process.env.SYSTEM_PULLREQUEST_TARGETBRANCH||'';if(!ref){console.log('Not a pull request - verifying EVERY project.');all()}const target=ref.replace('refs/heads/','');const mergeBase=r=>{const o=cp.spawnSync('git',['merge-base',r,'HEAD'],{encoding:'utf8'});return o.status===0?o.stdout.trim():''};let base=mergeBase('origin/'+target);if(!base){console.log('No origin/'+target+' ref - fetching it to resolve a merge-base.');cp.spawnSync('git',['fetch','--no-tags','origin',target],{stdio:'inherit'});base=mergeBase('FETCH_HEAD')}if(!base){console.log('Could not resolve a merge-base with '+target+' - verifying EVERY project.');all()}console.log('Pull request against '+target+' - verifying projects affected since '+base);process.exit(cp.spawnSync('npx nx affected -t '+T+' --base='+base,{stdio:'inherit',shell:true}).status ?? 1)"`;
1750
- var PACK_APPS_GUARD = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});if(fs.globSync('apps/*/project.json').length===0){console.log('No apps to pack - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('npx nx run-many -t package',{stdio:'inherit',shell:true}).status ?? 1)"`;
1751
- function releaseGuard(pythonPublishEnv) {
1752
- return `node -e "const fs=require('node:fs'),cp=require('node:child_process');const hasNpm=fs.globSync('packages/*/package.json').length>0;const hasPython=fs.globSync('python-packages/*/pyproject.toml').length>0;if(!hasNpm&&!hasPython){console.log('Nothing to release - skipping.');process.exit(0)}const env={...process.env};${pythonPublishEnv}process.exit(cp.spawnSync('npx nx release --yes',{stdio:'inherit',shell:true,env}).status ?? 1)"`;
1502
+ function prepareFlutter(workspaceRoot) {
1503
+ ensureFlutter(workspaceRoot);
1504
+ ensureNxFlutterPlugin(workspaceRoot);
1753
1505
  }
1754
- function pythonPublishEnvFragment(pythonPublishUrl2) {
1755
- return pythonPublishUrl2 ? `if(hasPython){env.TWINE_REPOSITORY_URL='${pythonPublishUrl2}';env.TWINE_USERNAME='AzureArtifacts';env.TWINE_PASSWORD=Buffer.from(process.env.PAT,'base64').toString()}` : "";
1506
+ function addFlutterApp(workspaceRoot, name) {
1507
+ prepareFlutter(workspaceRoot);
1508
+ ensureAdmZip(workspaceRoot);
1509
+ runNx(["g", "@mnci/nx-flutter:application", name, "--no-interactive"], workspaceRoot);
1510
+ addProjectJsonTargets((0, import_node_path4.join)(workspaceRoot, "apps", name, "project.json"), {
1511
+ start: flutterAppStartTarget(name)
1512
+ });
1513
+ registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
1514
+ }
1515
+ function addFlutterLib(workspaceRoot, name) {
1516
+ prepareFlutter(workspaceRoot);
1517
+ runNx(["g", "@mnci/nx-flutter:library", name, "--no-interactive"], workspaceRoot);
1518
+ registerProjectCommands(workspaceRoot, name, { build: false });
1519
+ }
1520
+ function addFlutterInternalLib(workspaceRoot, name) {
1521
+ prepareFlutter(workspaceRoot);
1522
+ runNx(["g", "@mnci/nx-flutter:internal-library", name, "--no-interactive"], workspaceRoot);
1523
+ registerProjectCommands(workspaceRoot, name, { build: false });
1524
+ }
1525
+
1526
+ // src/commands/add/go.ts
1527
+ var import_node_fs4 = require("fs");
1528
+ var import_node_path5 = require("path");
1529
+ function ensureGo(workspaceRoot) {
1530
+ if (runShell("go", ["version"], workspaceRoot) !== 0) {
1531
+ throw new Error("Go not found. Install Go 1.21+ first: https://go.dev/dl/");
1532
+ }
1533
+ }
1534
+ function warnIfNoGolangciLint(workspaceRoot) {
1535
+ if (runShell("golangci-lint", ["--version"], workspaceRoot) !== 0) {
1536
+ logger.warn(
1537
+ "golangci-lint not found \u2014 the generated lint target needs it. Install: https://golangci-lint.run/welcome/install/"
1538
+ );
1539
+ }
1540
+ }
1541
+ function nxGoPluginSpec() {
1542
+ return process.env.MNCI_NX_GO_SPEC ?? "@nx-go/nx-go";
1543
+ }
1544
+ function ensureNxGoPlugin(workspaceRoot) {
1545
+ if (hasPlugin(workspaceRoot, "@nx-go/nx-go")) {
1546
+ return;
1547
+ }
1548
+ const spec = nxGoPluginSpec();
1549
+ logger.step(`Installing the Go toolchain plugin (${spec})`);
1550
+ if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
1551
+ throw new Error("npm install of @nx-go/nx-go failed");
1552
+ }
1553
+ }
1554
+ function ensureGoModule(workspaceRoot) {
1555
+ if (fileExists((0, import_node_path5.join)(workspaceRoot, "go.mod"))) {
1556
+ return;
1557
+ }
1558
+ logger.step("Bootstrapping the workspace Go module (single root go.mod)");
1559
+ runNx(["g", "@nx-go/nx-go:init", "--no-interactive"], workspaceRoot);
1560
+ runNx(["g", "@nx-go/nx-go:convert-to-one-mod", "--no-interactive"], workspaceRoot);
1561
+ }
1562
+ function goModulePath(workspaceRoot) {
1563
+ try {
1564
+ const contents = (0, import_node_fs4.readFileSync)((0, import_node_path5.join)(workspaceRoot, "go.mod"), "utf8");
1565
+ return /^module\s+(\S+)/m.exec(contents)?.[1];
1566
+ } catch {
1567
+ return void 0;
1568
+ }
1569
+ }
1570
+ function addProjectJsonTargets2(projectJsonPath, newTargets) {
1571
+ const project = readJson(projectJsonPath);
1572
+ const targets = project.targets ?? {};
1573
+ writeFileEnsured(projectJsonPath, toJson({ ...project, targets: { ...targets, ...newTargets } }));
1574
+ }
1575
+ function goTestTarget() {
1576
+ return { executor: "@nx-go/nx-go:test" };
1577
+ }
1578
+ function goLintTarget() {
1579
+ return {
1580
+ executor: "@nx-go/nx-go:lint",
1581
+ parallelism: false,
1582
+ options: { linter: "golangci-lint", args: ["run"] }
1583
+ };
1756
1584
  }
1757
- function npmAuthEnvVariable(registryKind, variableReference) {
1758
- return registryKind === "npm" ? ["NODE_AUTH_TOKEN", variableReference("NPM_TOKEN")] : ["PAT", variableReference("PAT")];
1585
+ function goBuildTarget(name) {
1586
+ return {
1587
+ executor: "@nx-go/nx-go:build",
1588
+ outputs: [`{workspaceRoot}/dist/apps/${name}`],
1589
+ options: { outputPath: `../../dist/apps/${name}/${name}` }
1590
+ };
1759
1591
  }
1760
- function poolBlock(agent) {
1761
- return /^(?:ubuntu|windows|macos)-/i.test(agent) ? ` vmImage: ${agent}` : ` name: ${agent}`;
1592
+ function goPackageTarget(tag, name) {
1593
+ const zip = `dist/drop/${tag}-${name}.zip`;
1594
+ const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFolder('dist/apps/${name}');z.writeZip('${zip}')"`;
1595
+ return {
1596
+ executor: "nx:run-commands",
1597
+ dependsOn: ["build"],
1598
+ outputs: [`{workspaceRoot}/${zip}`],
1599
+ options: { command }
1600
+ };
1762
1601
  }
1763
- function azurePipelinesYaml(agent, variableGroup, pythonPublishUrl2, registryKind = "azure-artifacts") {
1764
- const onMain = "and(succeeded(), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI'), eq(variables['Build.SourceBranchName'], 'main'))";
1765
- const [npmAuthName, npmAuthValue] = npmAuthEnvVariable(registryKind, (name) => `$(${name})`);
1766
- return `name: monorepo-ci-$(Date:yyyyMMdd)$(Rev:.r)
1767
-
1768
- # Generated by MoNecromanCI. Deliberately thin: Nx builds, 'nx release'
1769
- # versions from conventional commits and pushes ONLY a tag to main, and each
1770
- # app is packed into dist/drop/<type>-<name>.zip by its own 'package' target.
1771
- #
1772
- # Cross-platform by construction: no bash, no PowerShell. Every step is a
1773
- # built-in task or a single-line git/npm/npx/node command, so the pipeline
1774
- # runs unchanged on Linux, macOS and Windows agents.
1775
-
1776
- trigger:
1777
- # While a run is in progress, queue further pushes and run them together rather
1778
- # than concurrently. This is the closest thing Azure Pipelines YAML has to
1779
- # GitHub's concurrency group, and it is load-bearing on main for the same
1780
- # reason: two concurrent 'nx release' runs would race to create the same tag.
1781
- batch: true
1782
- branches:
1783
- include:
1784
- - main
1785
- # EVERY other branch too, and that breadth is a correction rather than
1786
- # generosity. On Azure Repos Git a YAML 'pr:' block does nothing at all
1787
- # (see the note below), so a CI trigger is the only pre-merge
1788
- # verification this file can switch on by itself. Listing main alone \u2014
1789
- # which is what this pipeline used to do, next to a 'pr:' block that
1790
- # looked like it covered the rest \u2014 means a workspace verifies NOTHING
1791
- # until a change has already reached the branch it was supposed to
1792
- # protect.
1793
- #
1794
- # Release stays gated on main (the 'condition:' on every release step
1795
- # below), so a topic-branch run installs, audits, syncs, formats,
1796
- # verifies and packs, and publishes nothing.
1797
- - '*'
1798
-
1799
- # PR validation is deliberately NOT configured here, because on Azure Repos Git
1800
- # it CANNOT be: "For an Azure Repos Git repo, you cannot configure a PR trigger
1801
- # in the YAML file. You need to use branch policies." A 'pr:' block here is not
1802
- # an error \u2014 it is silently ignored, which is worse, because the file then
1803
- # documents a gate that does not exist. (The same block IS honoured for GitHub
1804
- # and Bitbucket repos, which is why .github/workflows/ci.yml keeps its
1805
- # 'pull_request:' trigger.)
1806
- #
1807
- # To get PR validation, configure it once in the UI: Project Settings ->
1808
- # Repositories -> <repo> -> Policies -> <branch> -> Build Validation, pointing
1809
- # at this pipeline. That is also the only thing that sets
1810
- # System.PullRequest.TargetBranch, which the verify step below reads to scope
1811
- # itself to affected projects \u2014 without a policy that variable is never set and
1812
- # the step verifies every project instead. Correct either way, just slower.
1813
- #
1814
- # Cancelling a superseded PR run lives in that same policy ("automatically
1815
- # cancel"), and likewise has no YAML expression.
1816
- pr: none
1817
-
1818
- pool:
1819
- ${poolBlock(agent)}
1820
-
1821
- variables:
1822
- # Holds the npm auth secret the root .npmrc reads: the base64-encoded \`PAT\`
1823
- # for an Azure Artifacts feed, or a raw npm automation token as \`NPM_TOKEN\`
1824
- # for public npm. Mark it secret in Library. Add app build vars here too if needed.
1825
- - group: ${variableGroup}
1826
- # Relocates npm's cache inside the pipeline workspace so Cache@2 can restore it
1827
- # (the default ~/.npm is outside the cacheable area on hosted agents). npm reads
1828
- # this as an ordinary env var, so it needs no config file and works on every
1829
- # agent OS.
1830
- - name: npm_config_cache
1831
- value: $(Pipeline.Workspace)/.npm
1832
-
1833
- steps:
1834
- - checkout: self
1835
- # Lets later steps push release tags back to the repo. The Project
1836
- # Collection Build Service account needs Contribute permission on this
1837
- # repo (Project Settings -> Repositories -> Security) \u2014 one-time grant.
1838
- persistCredentials: true
1839
- fetchDepth: 0
1840
-
1841
- # checkout leaves a detached HEAD; nx release needs a real branch.
1842
- - script: git checkout -B $(Build.SourceBranchName)
1843
- displayName: Attach HEAD to the source branch
1844
-
1845
- # Version resolution needs the release tags.
1846
- - script: git fetch --all --prune --tags
1847
- displayName: Fetch branches and release tags
1848
-
1849
- - script: git config user.name "Azure Pipelines" && git config user.email "pipeline@dev.azure.com"
1850
- displayName: Set the git identity used for release tags
1851
-
1852
- - task: UseNode@1
1853
- inputs:
1854
- version: 24.x
1855
-
1856
- # The Azure counterpart of \`cache: npm\` on actions/setup-node: restores npm's
1857
- # download cache so \`npm ci\` does not re-fetch every tarball on every run.
1858
- # Keyed on the lockfile, so a dependency change misses and anything else hits;
1859
- # restoreKeys falls back to the newest cache for this OS on a miss, which still
1860
- # avoids a fully cold install. Agent.OS is in the key because a cached native
1861
- # module built for one OS is not reusable on another.
1862
- - task: Cache@2
1863
- displayName: Cache npm packages
1864
- inputs:
1865
- key: 'npm | "$(Agent.OS)" | package-lock.json'
1866
- restoreKeys: |
1867
- npm | "$(Agent.OS)"
1868
- path: $(npm_config_cache)
1869
-
1870
- - script: npm ci
1871
- displayName: Install dependencies
1872
- env:
1873
- ${npmAuthName}: ${npmAuthValue}
1874
-
1875
- # Fails ONLY on an advisory that has a published fix, at moderate or above;
1876
- # anything upstream has not fixed is printed and passes. See NPM_AUDIT_STEP's
1877
- # remarks for why that split replaced a warn-only step.
1878
- - script: ${NPM_AUDIT_STEP}
1879
- displayName: npm audit (fails on an actionable advisory)
1880
-
1881
- # Installs the fixed Python toolchain (ruff, pytest, build, twine, pip-audit)
1882
- # \u2014 written by 'mnci add' to requirements-dev.txt on the first Python
1883
- # project. Plain pip, no uv/Poetry: portable guard skips cleanly on a
1884
- # workspace with none.
1885
- - script: ${PYTHON_INSTALL_GUARD}
1886
- displayName: Install Python dependencies (ruff, pytest, build, twine, pip-audit)
1887
-
1888
- # Editable-installs every Python project into one shared environment, the
1889
- # pip-world counterpart of 'npm install' hoisting every workspace package
1890
- # into one root node_modules \u2014 so a project that imports an internal lib
1891
- # (normally vendored only at build time) can resolve that import at
1892
- # lint/test time too. Portable guard skips cleanly on a workspace with none.
1893
- - script: ${PYTHON_WORKSPACE_INSTALL_GUARD}
1894
- displayName: Install Python project dependencies (editable, workspace-wide)
1895
-
1896
- # Non-blocking, same reasoning as the npm audit step above. Runs after the
1897
- # workspace-wide install so it scans the workspace's actual dependency set,
1898
- # not just the fixed toolchain.
1899
- - script: ${PIP_AUDIT_GUARD}
1900
- displayName: pip-audit (non-blocking)
1901
-
1902
- # Go, if the workspace has any: hosted agents ship the toolchain, so only
1903
- # the module cache and the linter need seeding. Both guards skip cleanly on
1904
- # a workspace with no root go.mod.
1905
- - script: ${GO_MODULE_DOWNLOAD_GUARD}
1906
- displayName: Download Go module dependencies
1907
-
1908
- # golangci-lint is what the generated Go lint target actually runs (the
1909
- # plugin's own default is 'go fmt', which only reformats). 'go install'
1910
- # drops it in GOPATH/bin, which is not on PATH by default on a hosted
1911
- # agent \u2014 so prepend it for every later step in the job.
1912
- - script: ${GOLANGCI_LINT_INSTALL_GUARD}
1913
- displayName: Install golangci-lint
1914
-
1915
- - script: ${GO_TOOL_PATH_AZURE}
1916
- displayName: Add Go tool bin to PATH
1917
-
1918
- # Flutter, if the workspace has any. Unlike Python and Go, hosted agents do
1919
- # NOT ship the Flutter SDK, so this installs it (shallow git clone at a
1920
- # pinned tag) and puts it on PATH. All three guards skip cleanly on a
1921
- # workspace with no root pubspec.yaml, and the install also skips when an
1922
- # SDK is already on PATH.
1923
- - script: ${FLUTTER_SDK_INSTALL_GUARD}
1924
- displayName: Install the Flutter SDK (${FLUTTER_SDK_VERSION})
1925
-
1926
- - script: ${FLUTTER_TOOL_PATH_AZURE}
1927
- displayName: Add the Flutter SDK to PATH
1928
-
1929
- # One command resolves EVERY Dart dependency, internal and external, for the
1930
- # whole workspace: the projects are pub workspace members, so this writes a
1931
- # single root pubspec.lock they all share.
1932
- - script: ${FLUTTER_PUB_GET_GUARD}
1933
- displayName: Resolve Dart dependencies (one pub get for the whole workspace)
1934
-
1935
- # Fails fast, with an unambiguous message, when a stale TypeScript project
1936
- # reference (or another sync generator's drift) was never synced+committed
1937
- # locally \u2014 sync.applyChanges (nx.json) only auto-applies interactively, so
1938
- # CI still needs its own explicit, early check rather than surfacing this as
1939
- # a confusing failure buried inside the build step below.
1940
- - script: npx nx sync:check
1941
- displayName: Verify the workspace is synced (run 'npx nx sync' locally and commit if this fails)
1942
-
1943
- # The one verify step, and deliberately the only one: affected projects on a
1944
- # pull request, EVERY project on anything else (a push to main included, so a
1945
- # release is always verified in full). Every fallback \u2014 no target branch, an
1946
- # unresolvable merge-base \u2014 takes the full path, because a run that verifies
1947
- # too little still reports green.
1948
- #
1949
- # 'npm run lint' is 'nx run-many -t lint', a strict subset of the targets
1950
- # below, so adding it back as its own step would only duplicate work \u2014 and on
1951
- # a pull request it would re-lint every project, discarding the point of this.
1952
- - script: ${AFFECTED_OR_ALL_GUARD}
1953
- displayName: Verify (affected on a PR, every project on main)
1954
-
1955
- # Pack every app into dist/drop/<type>-<name>.zip via each app's 'package'
1956
- # target. Portable guard: skip cleanly when the workspace has no apps yet.
1957
- - script: ${PACK_APPS_GUARD}
1958
- displayName: Pack all apps (one zip per app -> dist/drop)
1959
- condition: ${onMain}
1960
-
1961
- - task: PublishBuildArtifacts@1
1962
- displayName: Publish the drop (one zip per app)
1963
- condition: ${onMain}
1964
- inputs:
1965
- PathtoPublish: $(Build.SourcesDirectory)/dist/drop
1966
- ArtifactName: drop
1967
-
1968
- # One build tag per packed app, EXACTLY the zip name (type-name), so the
1969
- # classic release pipeline knows which app to run for. Derived from the zip
1970
- # filenames so the tag can never drift from the artifact.
1971
- - script: node -e "const fs=require('node:fs');const path=require('node:path');for(const f of fs.globSync('dist/drop/*.zip')){console.log('##vso[build.addbuildtag]'+path.basename(f,'.zip'))}"
1972
- displayName: Tag the run per app (type-name)
1973
- condition: ${onMain}
1974
-
1975
- # Version + tag + publish, in one release, for npm (packages/*) AND Python
1976
- # (python-packages/*) \u2014 conventional commits, tag-only push. Portable guard:
1977
- # nx release errors on an empty scope, so skip cleanly when there is nothing
1978
- # to release. When there are Python packages and an Azure feed, twine
1979
- # publish credentials are exported (raw PAT, decoded from the base64 variable).
1980
- - script: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2))}
1981
- displayName: Release \u2014 version, tag and publish (npm + Python)
1982
- condition: ${onMain}
1983
- env:
1984
- ${npmAuthName}: ${npmAuthValue}
1985
-
1986
- # nx release's own git push (release.git.push) is deliberately left off: it
1987
- # only runs when a remote GitHub/GitLab Release is configured, which this
1988
- # pipeline never does, so it would never push the tag the step above just
1989
- # created. Pushed explicitly, unconditionally (a no-op when nothing released)
1990
- # once tagging is guaranteed to have already happened.
1991
- - script: git push origin --tags
1992
- displayName: Push release tags (nx release's own push never runs without a remote Release configured)
1993
- condition: ${onMain}
1994
- `;
1602
+ function goStartTarget(name) {
1603
+ return {
1604
+ executor: "nx:run-commands",
1605
+ continuous: true,
1606
+ options: { command: "go run .", cwd: `apps/${name}` }
1607
+ };
1995
1608
  }
1996
- function githubActionsYaml(agent, pythonPublishUrl2, registryKind = "azure-artifacts", ci = "github") {
1997
- const onMain = "github.event_name == 'push' && github.ref_name == 'main'";
1998
- const [npmAuthName, npmAuthValue] = npmAuthEnvVariable(
1999
- registryKind,
2000
- (name) => `\${{ secrets.${name} }}`
1609
+ function prepareGo(workspaceRoot) {
1610
+ ensureGo(workspaceRoot);
1611
+ warnIfNoGolangciLint(workspaceRoot);
1612
+ ensureNxGoPlugin(workspaceRoot);
1613
+ ensureGoModule(workspaceRoot);
1614
+ }
1615
+ function addGoApp(workspaceRoot, name) {
1616
+ prepareGo(workspaceRoot);
1617
+ ensureAdmZip(workspaceRoot);
1618
+ runNx(
1619
+ [
1620
+ "g",
1621
+ "@nx-go/nx-go:application",
1622
+ `apps/${name}`,
1623
+ `--name=${name}`,
1624
+ "--tags=type:go-app",
1625
+ "--no-interactive"
1626
+ ],
1627
+ workspaceRoot
2001
1628
  );
2002
- const githubReleases = ci === "github";
2003
- return `name: CI
2004
-
2005
- # Generated by MoNecromanCI. Deliberately thin: Nx builds, 'nx release'
2006
- # versions from conventional commits and pushes ONLY a tag to main, and each
2007
- # app is packed into dist/drop/<type>-<name>.zip by its own 'package' target.
2008
- # The GitHub Actions equivalent of azure-pipelines.yml \u2014 see there for the
2009
- # fuller rationale; both stay hand-kept in lockstep, there is no shared template.
2010
-
2011
- on:
2012
- push:
2013
- branches: [main]
2014
- pull_request:
2015
- branches: [main]
2016
-
2017
- concurrency:
2018
- # One run at a time per ref, but \`cancel-in-progress\` is an **expression** on
2019
- # purpose rather than a flat \`true\`.
2020
- #
2021
- # A superseded PR run is pure waste, so cancel it. A run on \`main\` must never
2022
- # be cancelled: it publishes packages and pushes release tags, and killing it
2023
- # part-way can leave a tag pushed with the publish only half done \u2014 a state no
2024
- # rerun repairs cleanly, because the version is then already tagged. Those runs
2025
- # queue instead, which also stops two \`nx release\` invocations racing to create
2026
- # the same tag when two commits land close together.
2027
- group: \${{ github.workflow }}-\${{ github.ref }}
2028
- cancel-in-progress: \${{ github.event_name == 'pull_request' }}
2029
-
2030
- permissions:
2031
- # Lets the release step push the version tag nx release creates back to main,
2032
- # and (github-only provider) create the GitHub Release itself.
2033
- contents: write
2034
-
2035
- jobs:
2036
- ci:
2037
- runs-on: ${agent}
2038
- steps:
2039
- - uses: actions/checkout@v4
2040
- with:
2041
- fetch-depth: 0
2042
-
2043
- # Version resolution needs the release tags.
2044
- - run: git fetch --all --prune --tags
2045
- name: Fetch branches and release tags
2046
-
2047
- - run: git config user.name "github-actions[bot]" && git config user.email "github-actions[bot]@users.noreply.github.com"
2048
- name: Set the git identity used for release tags
2049
-
2050
- - uses: actions/setup-node@v4
2051
- with:
2052
- node-version: ${NODE_VERSION}
2053
- # Caches ~/.npm keyed on package-lock.json, so \`npm ci\` restores from
2054
- # the local cache instead of re-downloading every tarball on every run.
2055
- # Nothing to invalidate by hand: the action keys on the lockfile, so a
2056
- # dependency change misses the cache and a no-op change hits it.
2057
- cache: npm
2058
-
2059
- # setup-node installs whatever npm the Node image bundles, and npm's major
2060
- # is part of the contract: npm 11 reuses an already-installed tree instead
2061
- # of re-resolving, so it applies \`overrides\` differently from npm 10. See
2062
- # NPM_VERSION's remarks for the measurement.
2063
- - run: npm install -g npm@${NPM_VERSION}
2064
- name: Pin npm to the major mnci verifies against
2065
-
2066
- - run: npm ci
2067
- name: Install dependencies
2068
- env:
2069
- ${npmAuthName}: ${npmAuthValue}
2070
-
2071
- # Fails ONLY on an advisory that has a published fix, at moderate or
2072
- # above; anything upstream has not fixed is printed and passes. See
2073
- # NPM_AUDIT_STEP's remarks for why that split replaced a warn-only step.
2074
- - run: ${NPM_AUDIT_STEP}
2075
- name: npm audit (fails on an actionable advisory)
2076
-
2077
- # Installs the fixed Python toolchain (ruff, pytest, build, twine,
2078
- # pip-audit) \u2014 written by 'mnci add' to requirements-dev.txt on the
2079
- # first Python project. Plain pip, no uv/Poetry: portable guard skips
2080
- # cleanly on a workspace with none.
2081
- - run: ${PYTHON_INSTALL_GUARD}
2082
- name: Install Python dependencies (ruff, pytest, build, twine, pip-audit)
2083
-
2084
- # Editable-installs every Python project into one shared environment, the
2085
- # pip-world counterpart of 'npm install' hoisting every workspace package
2086
- # into one root node_modules \u2014 so a project that imports an internal lib
2087
- # (normally vendored only at build time) can resolve that import at
2088
- # lint/test time too. Portable guard skips cleanly on a workspace with none.
2089
- - run: ${PYTHON_WORKSPACE_INSTALL_GUARD}
2090
- name: Install Python project dependencies (editable, workspace-wide)
2091
-
2092
- # Non-blocking, same reasoning as the npm audit step above. Runs after
2093
- # the workspace-wide install so it scans the workspace's actual
2094
- # dependency set, not just the fixed toolchain.
2095
- - run: ${PIP_AUDIT_GUARD}
2096
- name: pip-audit (non-blocking)
2097
-
2098
- # Go, if the workspace has any: hosted runners ship the toolchain, so
2099
- # only the module cache and the linter need seeding. Both guards skip
2100
- # cleanly on a workspace with no root go.mod.
2101
- - run: ${GO_MODULE_DOWNLOAD_GUARD}
2102
- name: Download Go module dependencies
2103
-
2104
- # golangci-lint is what the generated Go lint target actually runs (the
2105
- # plugin's own default is 'go fmt', which only reformats). 'go install'
2106
- # drops it in GOPATH/bin, which is not on PATH by default on a hosted
2107
- # runner \u2014 so publish it for every later step in the job.
2108
- - run: ${GOLANGCI_LINT_INSTALL_GUARD}
2109
- name: Install golangci-lint
2110
-
2111
- - run: ${GO_TOOL_PATH_GITHUB}
2112
- name: Add Go tool bin to PATH
2113
-
2114
- # Flutter, if the workspace has any. Unlike Python and Go, hosted runners
2115
- # do NOT ship the Flutter SDK, so this installs it (shallow git clone at
2116
- # a pinned tag) and puts it on PATH. All three guards skip cleanly on a
2117
- # workspace with no root pubspec.yaml, and the install also skips when an
2118
- # SDK is already on PATH.
2119
- - run: ${FLUTTER_SDK_INSTALL_GUARD}
2120
- name: Install the Flutter SDK (${FLUTTER_SDK_VERSION})
2121
-
2122
- - run: ${FLUTTER_TOOL_PATH_GITHUB}
2123
- name: Add the Flutter SDK to PATH
2124
-
2125
- # One command resolves EVERY Dart dependency, internal and external, for
2126
- # the whole workspace: the projects are pub workspace members, so this
2127
- # writes a single root pubspec.lock they all share.
2128
- - run: ${FLUTTER_PUB_GET_GUARD}
2129
- name: Resolve Dart dependencies (one pub get for the whole workspace)
1629
+ addProjectJsonTargets2((0, import_node_path5.join)(workspaceRoot, "apps", name, "project.json"), {
1630
+ build: goBuildTarget(name),
1631
+ test: goTestTarget(),
1632
+ lint: goLintTarget(),
1633
+ package: goPackageTarget("go-app", name),
1634
+ start: goStartTarget(name)
1635
+ });
1636
+ registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
1637
+ }
1638
+ function addGoFunctionApp(workspaceRoot, name) {
1639
+ prepareGo(workspaceRoot);
1640
+ ensureAdmZip(workspaceRoot);
1641
+ runNx(
1642
+ [
1643
+ "g",
1644
+ "@nx-go/nx-go:application",
1645
+ `apps/${name}`,
1646
+ `--name=${name}`,
1647
+ "--tags=type:go-function-app",
1648
+ "--no-interactive"
1649
+ ],
1650
+ workspaceRoot
1651
+ );
1652
+ addProjectJsonTargets2((0, import_node_path5.join)(workspaceRoot, "apps", name, "project.json"), {
1653
+ build: goBuildTarget(name),
1654
+ test: goTestTarget(),
1655
+ lint: goLintTarget(),
1656
+ package: goPackageTarget("go-function-app", name)
1657
+ });
1658
+ registerProjectCommands(workspaceRoot, name, { build: true });
1659
+ }
1660
+ function addGoLib(workspaceRoot, name) {
1661
+ prepareGo(workspaceRoot);
1662
+ runNx(
1663
+ [
1664
+ "g",
1665
+ "@nx-go/nx-go:library",
1666
+ `packages/${name}`,
1667
+ `--name=${name}`,
1668
+ "--tags=type:go-lib",
1669
+ "--no-interactive"
1670
+ ],
1671
+ workspaceRoot
1672
+ );
1673
+ addProjectJsonTargets2((0, import_node_path5.join)(workspaceRoot, "packages", name, "project.json"), {
1674
+ test: goTestTarget(),
1675
+ lint: goLintTarget()
1676
+ });
1677
+ registerProjectCommands(workspaceRoot, name, { build: false });
1678
+ const module2 = goModulePath(workspaceRoot);
1679
+ if (module2) {
1680
+ logger.step(`Import it as ${module2}/packages/${name}`);
1681
+ }
1682
+ }
1683
+ function addGoInternalLib(workspaceRoot, name) {
1684
+ prepareGo(workspaceRoot);
1685
+ runNx(
1686
+ [
1687
+ "g",
1688
+ "@nx-go/nx-go:library",
1689
+ `libs/${name}`,
1690
+ `--name=${name}`,
1691
+ "--tags=type:go-internal-lib",
1692
+ "--no-interactive"
1693
+ ],
1694
+ workspaceRoot
1695
+ );
1696
+ addProjectJsonTargets2((0, import_node_path5.join)(workspaceRoot, "libs", name, "project.json"), {
1697
+ test: goTestTarget(),
1698
+ lint: goLintTarget()
1699
+ });
1700
+ registerProjectCommands(workspaceRoot, name, { build: false });
1701
+ const module2 = goModulePath(workspaceRoot);
1702
+ if (module2) {
1703
+ logger.step(`Import it as ${module2}/libs/${name}`);
1704
+ }
1705
+ }
2130
1706
 
2131
- # Fails fast, with an unambiguous message, when a stale TypeScript project
2132
- # reference (or another sync generator's drift) was never synced+committed
2133
- # locally \u2014 sync.applyChanges (nx.json) only auto-applies interactively, so
2134
- # CI still needs its own explicit, early check rather than surfacing this as
2135
- # a confusing failure buried inside the build step below.
2136
- - run: npx nx sync:check
2137
- name: Verify the workspace is synced (run 'npx nx sync' locally and commit if this fails)
1707
+ // src/commands/add/node.ts
1708
+ var import_node_path6 = require("path");
1709
+ function ensurePlugin2(workspaceRoot, packageName) {
1710
+ if (hasPlugin(workspaceRoot, packageName)) {
1711
+ return;
1712
+ }
1713
+ logger.step(`Installing Nx plugin ${packageName}`);
1714
+ runNx(["add", packageName], workspaceRoot);
1715
+ }
1716
+ function runNodeApp(workspaceRoot, name, stack, framework) {
1717
+ ensurePlugin2(workspaceRoot, "@nx/node");
1718
+ runNx(
1719
+ [
1720
+ "g",
1721
+ "@nx/node:application",
1722
+ `apps/${name}`,
1723
+ "--bundler=esbuild",
1724
+ `--unitTestRunner=${stack.testRunner}`,
1725
+ "--linter=none",
1726
+ "--e2eTestRunner=none",
1727
+ `--framework=${framework}`,
1728
+ "--no-interactive"
1729
+ ],
1730
+ workspaceRoot
1731
+ );
1732
+ }
1733
+ function nodeAppPackageTarget(name) {
1734
+ const zip = `dist/drop/node-app-${name}.zip`;
1735
+ const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFolder('apps/${name}/dist');z.writeZip('${zip}')"`;
1736
+ return {
1737
+ executor: "nx:run-commands",
1738
+ dependsOn: ["build"],
1739
+ outputs: [`{workspaceRoot}/${zip}`],
1740
+ options: { command }
1741
+ };
1742
+ }
1743
+ function addNodeApp(workspaceRoot, name, stack, framework = "none") {
1744
+ runNodeApp(workspaceRoot, name, stack, framework);
1745
+ ensureAdmZip(workspaceRoot);
1746
+ addNxTargets((0, import_node_path6.join)(workspaceRoot, "apps", name, "package.json"), {
1747
+ package: nodeAppPackageTarget(name)
1748
+ });
1749
+ removeGeneratedEslintConfig(workspaceRoot, `apps/${name}`);
1750
+ registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:serve` });
1751
+ }
1752
+ function ensureAzureFunctionsPackage(workspaceRoot) {
1753
+ if (hasPlugin(workspaceRoot, "@azure/functions")) {
1754
+ return;
1755
+ }
1756
+ logger.step("Installing @azure/functions");
1757
+ if (runShell("npm", ["install", "@azure/functions", "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
1758
+ throw new Error("npm install of @azure/functions failed");
1759
+ }
1760
+ }
1761
+ var NODE_FUNCTION_APP_MAIN = `// esbuild only includes what is reachable from here, so add one import per
1762
+ // function file you create under src/functions/.
1763
+ import './functions/hello'
1764
+ `;
1765
+ var NODE_FUNCTION_APP_HOST_JSON = `{
1766
+ "version": "2.0",
1767
+ "extensionBundle": {
1768
+ "id": "Microsoft.Azure.Functions.ExtensionBundle",
1769
+ "version": "[4.*, 5.0.0)"
1770
+ }
1771
+ }
1772
+ `;
1773
+ var NODE_FUNCTION_APP_HELLO = `import { app } from '@azure/functions'
1774
+ import type { HttpRequest, HttpResponseInit } from '@azure/functions'
1775
+ import { buildGreeting } from './greeting'
2138
1776
 
2139
- # The one verify step, and deliberately the only one: affected projects on
2140
- # a pull request, EVERY project on anything else (a push to main included,
2141
- # so a release is always verified in full). Every fallback \u2014 no target
2142
- # branch, an unresolvable merge-base \u2014 takes the full path, because a run
2143
- # that verifies too little still reports green.
2144
- #
2145
- # 'npm run lint' is 'nx run-many -t lint', a strict subset of the targets
2146
- # below, so adding it back as its own step would only duplicate work \u2014 and
2147
- # on a pull request it would re-lint every project, discarding the point
2148
- # of this.
2149
- - run: ${AFFECTED_OR_ALL_GUARD}
2150
- name: Verify (affected on a PR, every project on main)
1777
+ async function hello(request: HttpRequest): Promise<HttpResponseInit> {
1778
+ const name = request.query.get('name') ?? 'world'
1779
+ return { body: buildGreeting(name) }
1780
+ }
2151
1781
 
2152
- # Pack every app into dist/drop/<type>-<name>.zip via each app's 'package'
2153
- # target. Portable guard: skip cleanly when the workspace has no apps yet.
2154
- - run: ${PACK_APPS_GUARD}
2155
- name: Pack all apps (one zip per app -> dist/drop)
2156
- if: \${{ ${onMain} }}
1782
+ app.http('hello', {
1783
+ methods: ['GET'],
1784
+ authLevel: 'anonymous',
1785
+ route: 'hello',
1786
+ handler: hello,
1787
+ })
1788
+ `;
1789
+ var NODE_FUNCTION_APP_GREETING = `export function buildGreeting (name: string): string {
1790
+ return 'Hello, ' + name + '!'
1791
+ }
1792
+ `;
1793
+ var NODE_FUNCTION_APP_GREETING_SPEC = `import { buildGreeting } from './greeting'
2157
1794
 
2158
- - uses: actions/upload-artifact@v4
2159
- if: \${{ ${onMain} }}
2160
- with:
2161
- name: drop
2162
- path: dist/drop
2163
- if-no-files-found: ignore
1795
+ describe('buildGreeting', () => {
1796
+ it('greets a name', () => {
1797
+ expect(buildGreeting('world')).toBe('Hello, world!')
1798
+ })
1799
+ })
1800
+ `;
1801
+ function repairNodeFunctionAppManifest(nodeFunctionAppRoot, workspaceRoot) {
1802
+ const manifestPath = (0, import_node_path6.join)(nodeFunctionAppRoot, "package.json");
1803
+ const manifest = readJson(manifestPath);
1804
+ const azureFunctionsVersion = readJson(
1805
+ (0, import_node_path6.join)(workspaceRoot, "node_modules/@azure/functions/package.json")
1806
+ ).version;
1807
+ const dependencies = manifest.dependencies ?? {};
1808
+ writeFileEnsured(
1809
+ manifestPath,
1810
+ toJson({
1811
+ ...manifest,
1812
+ main: "dist/main.js",
1813
+ dependencies: { ...dependencies, "@azure/functions": `^${azureFunctionsVersion}` }
1814
+ })
1815
+ );
1816
+ }
1817
+ function nodeFunctionAppPackageTarget(name) {
1818
+ const zip = `dist/drop/node-function-app-${name}.zip`;
1819
+ const root = `apps/${name}`;
1820
+ const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFolder('${root}/dist','dist');z.addLocalFile('${root}/host.json');z.addLocalFile('${root}/package.json');z.writeZip('${zip}')"`;
1821
+ return {
1822
+ executor: "nx:run-commands",
1823
+ dependsOn: ["build"],
1824
+ outputs: [`{workspaceRoot}/${zip}`],
1825
+ options: { command }
1826
+ };
1827
+ }
1828
+ function nodeFunctionAppStartTarget(name) {
1829
+ return {
1830
+ executor: "nx:run-commands",
1831
+ dependsOn: ["build"],
1832
+ continuous: true,
1833
+ options: { command: "func start", cwd: `apps/${name}` }
1834
+ };
1835
+ }
1836
+ function addNodeFunctionApp(workspaceRoot, name, stack) {
1837
+ runNodeApp(workspaceRoot, name, stack, "none");
1838
+ ensureAzureFunctionsPackage(workspaceRoot);
1839
+ const nodeFunctionAppRoot = (0, import_node_path6.join)(workspaceRoot, "apps", name);
1840
+ writeFileEnsured((0, import_node_path6.join)(nodeFunctionAppRoot, "src/main.ts"), NODE_FUNCTION_APP_MAIN);
1841
+ writeFileEnsured((0, import_node_path6.join)(nodeFunctionAppRoot, "src/functions/hello.ts"), NODE_FUNCTION_APP_HELLO);
1842
+ writeFileEnsured(
1843
+ (0, import_node_path6.join)(nodeFunctionAppRoot, "src/functions/greeting.ts"),
1844
+ NODE_FUNCTION_APP_GREETING
1845
+ );
1846
+ writeFileEnsured(
1847
+ (0, import_node_path6.join)(nodeFunctionAppRoot, "src/functions/greeting.spec.ts"),
1848
+ NODE_FUNCTION_APP_GREETING_SPEC
1849
+ );
1850
+ writeFileEnsured((0, import_node_path6.join)(nodeFunctionAppRoot, "host.json"), NODE_FUNCTION_APP_HOST_JSON);
1851
+ repairNodeFunctionAppManifest(nodeFunctionAppRoot, workspaceRoot);
1852
+ ensureAdmZip(workspaceRoot);
1853
+ addNxTargets((0, import_node_path6.join)(nodeFunctionAppRoot, "package.json"), {
1854
+ package: nodeFunctionAppPackageTarget(name),
1855
+ start: nodeFunctionAppStartTarget(name)
1856
+ });
1857
+ removeGeneratedEslintConfig(workspaceRoot, `apps/${name}`);
1858
+ registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
1859
+ }
2164
1860
 
2165
- # Version + tag + publish, in one release, for npm (packages/*) AND Python
2166
- # (python-packages/*) \u2014 conventional commits, tag-only push. Portable guard:
2167
- # nx release errors on an empty scope, so skip cleanly when there is nothing
2168
- # to release. When there are Python packages and an Azure feed, twine
2169
- # publish credentials are exported (raw PAT, decoded from the base64 secret).${githubReleases ? `
2170
- # This provider also creates a per-project GitHub Release (changelog
2171
- # generated from conventional commits since that project's last tag),
2172
- # which is why 'nx release' pushes the tag itself here \u2014 see
2173
- # releaseConfig's remarks for why that is safe on this Nx version and
2174
- # why every other provider combination keeps the explicit push step below.` : ""}
2175
- - run: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2))}
2176
- name: Release \u2014 version, tag${githubReleases ? ", publish and GitHub Release" : " and publish"} (npm + Python)
2177
- if: \${{ ${onMain} }}
2178
- env:
2179
- ${npmAuthName}: ${npmAuthValue}${githubReleases ? `
2180
- GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}` : ""}
2181
- ${githubReleases ? "" : `
2182
- # nx release's own git push (release.git.push) is deliberately left off: it
2183
- # only runs when a remote GitHub/GitLab Release is configured, which this
2184
- # workflow never does, so it would never push the tag the step above just
2185
- # created. Pushed explicitly, unconditionally (a no-op when nothing released)
2186
- # once tagging is guaranteed to have already happened.
2187
- - run: git push origin --tags
2188
- name: Push release tags (nx release's own push never runs without a remote Release configured)
2189
- if: \${{ ${onMain} }}
2190
- `}`;
1861
+ // src/commands/add/npmLib.ts
1862
+ var import_node_path7 = require("path");
1863
+ async function addNpmLib(workspaceRoot, name, options, kindProvided, stack) {
1864
+ const scope = options.scope ?? (kindProvided ? defaultScope(workspaceRoot) : await promptText("npm scope for the published package", defaultScope(workspaceRoot)));
1865
+ runNx(
1866
+ [
1867
+ "g",
1868
+ "@nx/js:lib",
1869
+ `packages/${name}`,
1870
+ "--publishable",
1871
+ `--importPath=${scope}/${name}`,
1872
+ "--bundler=rollup",
1873
+ `--unitTestRunner=${stack.testRunner}`,
1874
+ "--linter=none",
1875
+ "--no-interactive"
1876
+ ],
1877
+ workspaceRoot
1878
+ );
1879
+ const manifestPath = (0, import_node_path7.join)(workspaceRoot, "packages", name, "package.json");
1880
+ markPublic(manifestPath);
1881
+ repairPublishableManifest(manifestPath);
1882
+ removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
1883
+ registerProjectCommands(workspaceRoot, name, { build: true });
2191
1884
  }
2192
- var DEPENDABOT_CONFIG = `# Generated by MoNecromanCI. Weekly dependency-update PRs so vulnerable or
2193
- # stale dependencies surface as a reviewable PR instead of only a push-time
2194
- # warning.
2195
- version: 2
2196
- updates:
2197
- - package-ecosystem: npm
2198
- directory: "/"
2199
- schedule:
2200
- interval: weekly
2201
1885
 
2202
- - package-ecosystem: github-actions
2203
- directory: "/"
2204
- schedule:
2205
- interval: weekly
1886
+ // src/commands/add/python.ts
1887
+ var import_node_fs5 = require("fs");
1888
+ var import_node_path8 = require("path");
1889
+ function ensurePython(workspaceRoot) {
1890
+ const hasPython = runShell("python3", ["--version"], workspaceRoot) === 0 || runShell("python", ["--version"], workspaceRoot) === 0;
1891
+ if (!hasPython) {
1892
+ throw new Error(
1893
+ "Python not found. Install Python 3.9+ first: https://www.python.org/downloads/"
1894
+ );
1895
+ }
1896
+ }
1897
+ function pythonPipPluginSpec() {
1898
+ return process.env.MNCI2_PYTHON_PIP_SPEC ?? "@mnci/nx-python-pip";
1899
+ }
1900
+ function ensurePythonPipPlugin(workspaceRoot) {
1901
+ if (hasPlugin(workspaceRoot, "@mnci/nx-python-pip")) {
1902
+ return;
1903
+ }
1904
+ const spec = pythonPipPluginSpec();
1905
+ logger.step(`Installing the Python toolchain plugin (${spec})`);
1906
+ if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
1907
+ throw new Error("npm install of @mnci/nx-python-pip failed");
1908
+ }
1909
+ }
1910
+ var PYTHON_REQUIREMENTS_DEV = `build
1911
+ twine
1912
+ ruff
1913
+ pytest
1914
+ pip-audit
1915
+ `;
1916
+ function ensureRequirementsDev(workspaceRoot) {
1917
+ const requirementsDevPath = (0, import_node_path8.join)(workspaceRoot, "requirements-dev.txt");
1918
+ if (!fileExists(requirementsDevPath)) {
1919
+ writeFileEnsured(requirementsDevPath, PYTHON_REQUIREMENTS_DEV);
1920
+ }
1921
+ }
1922
+ function pythonModuleDirectory(name) {
1923
+ return name.replaceAll("-", "_");
1924
+ }
1925
+ function addProjectJsonTargets3(projectJsonPath, newTargets) {
1926
+ const project = readJson(projectJsonPath);
1927
+ const targets = project.targets ?? {};
1928
+ writeFileEnsured(projectJsonPath, toJson({ ...project, targets: { ...targets, ...newTargets } }));
1929
+ }
1930
+ function pythonAppMain(name) {
1931
+ return `def main() -> None:
1932
+ print('${name} is running')
2206
1933
 
2207
- # Globs, not one entry per project: Python projects are added later via
2208
- # 'mnci add python-*', so this starts covering them automatically the
2209
- # moment the first one exists \u2014 a glob matching nothing yet is not an error.
2210
- - package-ecosystem: pip
2211
- directories:
2212
- - "/apps/*"
2213
- - "/python-packages/*"
2214
- - "/libs/*"
2215
- schedule:
2216
- interval: weekly
2217
1934
 
2218
- # Same glob reasoning as pip above. Dart projects are pub workspace members,
2219
- # so each one declares its own dependencies even though they all resolve
2220
- # through the single root pubspec.lock \u2014 hence the per-project directories
2221
- # rather than just "/".
2222
- - package-ecosystem: pub
2223
- directories:
2224
- - "/apps/*"
2225
- - "/packages/*"
2226
- - "/libs/*"
2227
- schedule:
2228
- interval: weekly
1935
+ if __name__ == '__main__':
1936
+ main()
2229
1937
  `;
2230
- var NX_SCAFFOLDING_TO_REMOVE = [".prettierrc", ".prettierrc.json", ".vscode"];
2231
- function removeNxScaffolding(workspaceRoot) {
2232
- for (const entry of NX_SCAFFOLDING_TO_REMOVE) {
2233
- (0, import_node_fs6.rmSync)((0, import_node_path11.join)(workspaceRoot, entry), { recursive: true, force: true });
2234
- }
2235
- removeProjectEslintConfigs(workspaceRoot);
2236
1938
  }
2237
- function removeIfPresent(path) {
2238
- (0, import_node_fs6.rmSync)(path, { recursive: true, force: true });
1939
+ function pythonAppStartTarget(name) {
1940
+ return {
1941
+ executor: "nx:run-commands",
1942
+ continuous: true,
1943
+ options: { command: "python3 main.py", cwd: `apps/${name}` }
1944
+ };
2239
1945
  }
2240
- function removeProjectEslintConfigs(workspaceRoot) {
2241
- const matches = (0, import_node_fs6.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
2242
- cwd: workspaceRoot
2243
- });
2244
- for (const match of matches) {
2245
- (0, import_node_fs6.rmSync)((0, import_node_path11.join)(workspaceRoot, match), { force: true });
2246
- }
1946
+ function pythonFunctionAppStartTarget(name) {
1947
+ return {
1948
+ executor: "nx:run-commands",
1949
+ continuous: true,
1950
+ options: { command: "func start", cwd: `apps/${name}` }
1951
+ };
2247
1952
  }
2248
- function applyOverlay(workspaceRoot, options, onProgress = () => {
2249
- }) {
2250
- const nxJsonPath = (0, import_node_path11.join)(workspaceRoot, "nx.json");
2251
- const nxJson = readJson(nxJsonPath);
2252
- const generators = {
2253
- ...nxJson.generators,
2254
- ...generatorDefaults(options.stack)
1953
+ function pythonAppPackageTarget(name) {
1954
+ const zip = `dist/drop/python-app-${name}.zip`;
1955
+ const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFolder('apps/${name}/dist');z.writeZip('${zip}')"`;
1956
+ return {
1957
+ executor: "nx:run-commands",
1958
+ dependsOn: ["build"],
1959
+ outputs: [`{workspaceRoot}/${zip}`],
1960
+ options: { command }
2255
1961
  };
2256
- const sync = { ...nxJson.sync, ...SYNC_CONFIG };
2257
- const mnci = { ...nxJson.mnci, ...mnciConfig(options) };
2258
- const patched = withSharedGlobals(withEslintPlugin(withReleaseConfig(nxJson, options.ci)));
2259
- onProgress("nx.json \u2014 release, sync, generators, shared inputs, mnci block");
2260
- writeFileEnsured(nxJsonPath, toJson({ ...patched, generators, sync, mnci }));
2261
- onProgress("package.json \u2014 root scripts, TS compilers, ESLint toolchain, overrides");
2262
- const manifestPath = (0, import_node_path11.join)(workspaceRoot, "package.json");
2263
- const manifest = readJson(manifestPath);
2264
- const scripts = {
2265
- ...manifest.scripts,
2266
- ...rootScripts()
1962
+ }
1963
+ function pythonFunctionAppPackageTarget(name, moduleDirectory) {
1964
+ const zip = `dist/drop/python-function-app-${name}.zip`;
1965
+ const root = `apps/${name}`;
1966
+ const command = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');const z=new A();z.addLocalFile('${root}/function_app.py');z.addLocalFile('${root}/host.json');z.addLocalFile('${root}/requirements.txt');z.addLocalFolder('${root}/${moduleDirectory}','${moduleDirectory}');z.writeZip('${zip}')"`;
1967
+ return {
1968
+ executor: "nx:run-commands",
1969
+ outputs: [`{workspaceRoot}/${zip}`],
1970
+ options: { command }
2267
1971
  };
2268
- const existingDevDeps = manifest.devDependencies;
2269
- const devDeps = withoutRetiredFormatterDependencies({
2270
- ...existingDevDeps,
2271
- ...TS_COMPILER_DEPENDENCIES,
2272
- // The preset pins `nx` itself; the ESLint plugins must match it exactly.
2273
- ...eslintToolchainDependencies(existingDevDeps?.nx ?? "latest")
1972
+ }
1973
+ function addPythonApp(workspaceRoot, name) {
1974
+ ensurePython(workspaceRoot);
1975
+ ensurePythonPipPlugin(workspaceRoot);
1976
+ ensureRequirementsDev(workspaceRoot);
1977
+ ensureAdmZip(workspaceRoot);
1978
+ runNx(
1979
+ ["g", "@mnci/nx-python-pip:application", name, `--directory=apps/${name}`, "--no-interactive"],
1980
+ workspaceRoot
1981
+ );
1982
+ writeFileEnsured((0, import_node_path8.join)(workspaceRoot, "apps", name, "main.py"), pythonAppMain(name));
1983
+ addProjectJsonTargets3((0, import_node_path8.join)(workspaceRoot, "apps", name, "project.json"), {
1984
+ package: pythonAppPackageTarget(name),
1985
+ start: pythonAppStartTarget(name)
2274
1986
  });
2275
- const overrides = {
2276
- ...manifest.overrides,
2277
- ...ESLINT_PEER_OVERRIDES,
2278
- ...SECURITY_OVERRIDES
2279
- };
2280
- const existingNx = manifest.nx;
2281
- const nx = {
2282
- ...existingNx,
2283
- includedScripts: existingNx?.includedScripts ?? [],
2284
- targets: {
2285
- ...existingNx?.targets,
2286
- lint: ROOT_LINT_TARGET
2287
- }
2288
- };
2289
- writeFileEnsured(
2290
- manifestPath,
2291
- toJson({
2292
- ...manifest,
2293
- name: `${options.scope}/source`,
2294
- scripts,
2295
- devDependencies: devDeps,
2296
- overrides,
2297
- nx
2298
- })
1987
+ registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
1988
+ }
1989
+ function addPythonFunctionApp(workspaceRoot, name) {
1990
+ ensurePython(workspaceRoot);
1991
+ ensurePythonPipPlugin(workspaceRoot);
1992
+ ensureRequirementsDev(workspaceRoot);
1993
+ ensureAdmZip(workspaceRoot);
1994
+ runNx(
1995
+ [
1996
+ "g",
1997
+ "@mnci/nx-python-pip:function-application",
1998
+ name,
1999
+ `--directory=apps/${name}`,
2000
+ "--no-interactive"
2001
+ ],
2002
+ workspaceRoot
2299
2003
  );
2300
- onProgress(
2301
- `.npmrc \u2014 ${options.registry.kind === "azure-artifacts" ? "Azure Artifacts feed routing and credentials" : "public npm registry auth"}`
2004
+ const moduleDirectory = pythonModuleDirectory(name);
2005
+ addProjectJsonTargets3((0, import_node_path8.join)(workspaceRoot, "apps", name, "project.json"), {
2006
+ package: pythonFunctionAppPackageTarget(name, moduleDirectory),
2007
+ start: pythonFunctionAppStartTarget(name)
2008
+ });
2009
+ registerProjectCommands(workspaceRoot, name, { build: false, start: `nx run ${name}:start` });
2010
+ }
2011
+ function addPythonLib(workspaceRoot, name) {
2012
+ ensurePython(workspaceRoot);
2013
+ ensurePythonPipPlugin(workspaceRoot);
2014
+ ensureRequirementsDev(workspaceRoot);
2015
+ runNx(
2016
+ [
2017
+ "g",
2018
+ "@mnci/nx-python-pip:library",
2019
+ name,
2020
+ `--directory=python-packages/${name}`,
2021
+ "--no-interactive"
2022
+ ],
2023
+ workspaceRoot
2302
2024
  );
2303
- writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".npmrc"), npmrcContent(options.registry, options.scope));
2304
- onProgress("commitlint.config.mjs and .husky/commit-msg \u2014 conventional commit enforcement");
2305
- writeFileEnsured((0, import_node_path11.join)(workspaceRoot, "commitlint.config.mjs"), COMMITLINT_CONFIG);
2306
- const hookPath = (0, import_node_path11.join)(workspaceRoot, ".husky/commit-msg");
2307
- writeFileEnsured(hookPath, COMMIT_MSG_HOOK);
2308
- markExecutable(hookPath);
2309
- onProgress("eslint.config.mjs \u2014 the shared lint AND formatting opinion");
2310
- writeFileEnsured((0, import_node_path11.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG);
2311
- for (const retired of RETIRED_FORMATTER_FILES) {
2312
- removeIfPresent((0, import_node_path11.join)(workspaceRoot, retired));
2025
+ registerProjectCommands(workspaceRoot, name, { build: true });
2026
+ }
2027
+ function addPythonInternalLib(workspaceRoot, name) {
2028
+ ensurePython(workspaceRoot);
2029
+ ensurePythonPipPlugin(workspaceRoot);
2030
+ ensureRequirementsDev(workspaceRoot);
2031
+ runNx(
2032
+ [
2033
+ "g",
2034
+ "@mnci/nx-python-pip:internal-library",
2035
+ name,
2036
+ `--directory=libs/${name}`,
2037
+ "--no-interactive"
2038
+ ],
2039
+ workspaceRoot
2040
+ );
2041
+ registerProjectCommands(workspaceRoot, name, { build: false });
2042
+ }
2043
+ var PYTHON_PROJECT_DIRS = ["apps", "python-packages", "libs"];
2044
+ function findPythonProjectDirectory(workspaceRoot, name) {
2045
+ return PYTHON_PROJECT_DIRS.find(
2046
+ (directory) => fileExists((0, import_node_path8.join)(workspaceRoot, directory, name, "pyproject.toml"))
2047
+ );
2048
+ }
2049
+ function parseVendorEntries(pyprojectToml) {
2050
+ const match = /^\s*vendor\s*=\s*\[([^\]]*)\]/m.exec(pyprojectToml);
2051
+ if (!match) {
2052
+ return [];
2053
+ }
2054
+ return match[1].split(",").map((entry) => entry.trim().replaceAll(/^["']|["']$/g, "")).filter(Boolean);
2055
+ }
2056
+ function addVendorEntry(pyprojectToml, lib) {
2057
+ const existing = parseVendorEntries(pyprojectToml);
2058
+ if (existing.length > 0) {
2059
+ const merged = [...existing, lib].map((name) => `"${name}"`).join(", ");
2060
+ return pyprojectToml.replace(
2061
+ /^(\s*vendor\s*=\s*)\[[^\]]*\]/m,
2062
+ (_match, prefix) => `${prefix}[${merged}]`
2063
+ );
2064
+ }
2065
+ const table = `[tool.mnci-python-pip]
2066
+ vendor = ["${lib}"]
2067
+
2068
+ `;
2069
+ return pyprojectToml.includes("[tool.pytest.ini_options]") ? pyprojectToml.replace("[tool.pytest.ini_options]", () => `${table}[tool.pytest.ini_options]`) : `${pyprojectToml}
2070
+ ${table}`;
2071
+ }
2072
+ async function addPythonVendor(workspaceRoot, consumer, options, kindProvided) {
2073
+ const lib = options.lib ?? (kindProvided ? void 0 : await promptText("Internal Python library to vendor (an existing libs/<name>)"));
2074
+ if (!lib) {
2075
+ throw new Error(
2076
+ "`add python-vendor` needs the library to vendor: pass --lib <name> (an existing libs/<name> internal library)."
2077
+ );
2078
+ }
2079
+ const consumerDirectory = findPythonProjectDirectory(workspaceRoot, consumer);
2080
+ if (!consumerDirectory) {
2081
+ throw new Error(
2082
+ `No Python project named '${consumer}' with a pyproject.toml found (checked apps/, python-packages/, libs/). A Python function app has no pyproject.toml \u2014 there is nothing to vendor into.`
2083
+ );
2084
+ }
2085
+ if (!fileExists((0, import_node_path8.join)(workspaceRoot, "libs", lib, "pyproject.toml"))) {
2086
+ throw new Error(
2087
+ `No internal Python library named '${lib}' found at libs/${lib}/pyproject.toml. Only an internal library (added via 'mnci add python-internal-lib') can be vendored.`
2088
+ );
2089
+ }
2090
+ if (consumerDirectory === "libs" && consumer === lib) {
2091
+ throw new Error(`'${consumer}' cannot vendor itself.`);
2092
+ }
2093
+ const pyprojectPath = (0, import_node_path8.join)(workspaceRoot, consumerDirectory, consumer, "pyproject.toml");
2094
+ const pyprojectToml = (0, import_node_fs5.readFileSync)(pyprojectPath, "utf8");
2095
+ if (parseVendorEntries(pyprojectToml).includes(lib)) {
2096
+ logger.info(`'${consumer}' already vendors '${lib}' \u2014 nothing to do.`);
2097
+ return;
2098
+ }
2099
+ writeFileEnsured(pyprojectPath, addVendorEntry(pyprojectToml, lib));
2100
+ logger.success(
2101
+ `'${consumer}' now vendors '${lib}' \u2014 its wheel will include '${lib}''s module on the next build.`
2102
+ );
2103
+ }
2104
+
2105
+ // src/commands/add/reactApp.ts
2106
+ var import_node_fs6 = require("fs");
2107
+ var import_node_path9 = require("path");
2108
+ var REACT_ENVIRONMENTS = ["dev", "uat", "prod"];
2109
+ function reactEnvironmentFile(environment) {
2110
+ return `# Vite compiles VITE_-prefixed vars into the '${environment}' bundle at build time.
2111
+ # They ship to the browser, so keep only PUBLIC config here \u2014 never secrets.
2112
+ VITE_ENVIRONMENT=${environment}
2113
+ VITE_API_URL=https://api.${environment}.example.com
2114
+ `;
2115
+ }
2116
+ function reactAppTargets(name) {
2117
+ const targets = {};
2118
+ for (const environment of REACT_ENVIRONMENTS) {
2119
+ targets[`build-${environment}`] = {
2120
+ executor: "nx:run-commands",
2121
+ outputs: [`{workspaceRoot}/apps/${name}/dist-${environment}`],
2122
+ options: {
2123
+ command: `vite build --mode ${environment} --outDir dist-${environment}`,
2124
+ cwd: `apps/${name}`
2125
+ }
2126
+ };
2127
+ }
2128
+ const zipStatements = REACT_ENVIRONMENTS.map(
2129
+ (environment) => `z=new A();z.addLocalFolder('apps/${name}/dist-${environment}');z.writeZip('dist/drop/react-app-${name}-${environment}.zip')`
2130
+ ).join(";");
2131
+ targets.package = {
2132
+ executor: "nx:run-commands",
2133
+ dependsOn: REACT_ENVIRONMENTS.map((environment) => `build-${environment}`),
2134
+ outputs: REACT_ENVIRONMENTS.map(
2135
+ (environment) => `{workspaceRoot}/dist/drop/react-app-${name}-${environment}.zip`
2136
+ ),
2137
+ options: {
2138
+ command: `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');let z;${zipStatements}"`
2139
+ }
2140
+ };
2141
+ return targets;
2142
+ }
2143
+ function allowEnvFiles(workspaceRoot) {
2144
+ const gitignorePath = (0, import_node_path9.join)(workspaceRoot, ".gitignore");
2145
+ if (!fileExists(gitignorePath)) {
2146
+ return;
2313
2147
  }
2314
- onProgress(".devcontainer/devcontainer.json \u2014 a local toolchain matching CI");
2315
- writeFileEnsured(
2316
- (0, import_node_path11.join)(workspaceRoot, ".devcontainer/devcontainer.json"),
2317
- devcontainerJson(options.workspaceName)
2148
+ const marker = "# MoNecromanCI: keep the committed per-environment Vite config (public VITE_ values)";
2149
+ const current = (0, import_node_fs6.readFileSync)(gitignorePath, "utf8");
2150
+ if (current.includes(marker)) {
2151
+ return;
2152
+ }
2153
+ const allow = REACT_ENVIRONMENTS.map((environment) => `!apps/*/.env.${environment}`).join("\n");
2154
+ writeFileEnsured(gitignorePath, `${current.replace(/\n*$/, "\n")}
2155
+ ${marker}
2156
+ ${allow}
2157
+ `);
2158
+ }
2159
+ function addReactApp(workspaceRoot, name, stack) {
2160
+ ensurePlugin(workspaceRoot, "@nx/react");
2161
+ runNx(
2162
+ [
2163
+ "g",
2164
+ "@nx/react:app",
2165
+ `apps/${name}`,
2166
+ "--bundler=vite",
2167
+ `--unitTestRunner=${stack.testRunner}`,
2168
+ "--linter=none",
2169
+ "--style=css",
2170
+ "--e2eTestRunner=none",
2171
+ "--no-interactive"
2172
+ ],
2173
+ workspaceRoot
2318
2174
  );
2319
- removeNxScaffolding(workspaceRoot);
2320
- onProgress(
2321
- `${options.workspaceName}.code-workspace \u2014 settings, extensions, launch configs`
2175
+ ensureAdmZip(workspaceRoot);
2176
+ const reactAppRoot = (0, import_node_path9.join)(workspaceRoot, "apps", name);
2177
+ for (const environment of REACT_ENVIRONMENTS) {
2178
+ writeFileEnsured((0, import_node_path9.join)(reactAppRoot, `.env.${environment}`), reactEnvironmentFile(environment));
2179
+ }
2180
+ allowEnvFiles(workspaceRoot);
2181
+ addNxTargets((0, import_node_path9.join)(reactAppRoot, "package.json"), reactAppTargets(name));
2182
+ removeGeneratedEslintConfig(workspaceRoot, `apps/${name}`);
2183
+ registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:serve` });
2184
+ }
2185
+
2186
+ // src/commands/add/reactLib.ts
2187
+ var import_node_path10 = require("path");
2188
+ var REACT_LIB_BUNDLER = "rollup";
2189
+ async function addReactLib(workspaceRoot, name, options, kindProvided, stack) {
2190
+ ensurePlugin(workspaceRoot, "@nx/react");
2191
+ const scope = options.scope ?? (kindProvided ? defaultScope(workspaceRoot) : await promptText("npm scope for the published package", defaultScope(workspaceRoot)));
2192
+ runNx(
2193
+ [
2194
+ "g",
2195
+ "@nx/react:library",
2196
+ `packages/${name}`,
2197
+ `--name=${name}`,
2198
+ "--publishable",
2199
+ `--importPath=${scope}/${name}`,
2200
+ `--bundler=${REACT_LIB_BUNDLER}`,
2201
+ `--unitTestRunner=${stack.testRunner}`,
2202
+ "--linter=none",
2203
+ "--no-interactive"
2204
+ ],
2205
+ workspaceRoot
2322
2206
  );
2323
- const codeWorkspacePath = (0, import_node_path11.join)(workspaceRoot, `${options.workspaceName}.code-workspace`);
2324
- const existing = readCodeWorkspace(codeWorkspacePath);
2325
- writeFileEnsured(
2326
- codeWorkspacePath,
2327
- vscodeWorkspace(
2328
- options.workspaceName,
2329
- existing?.tasks,
2330
- existing?.launch,
2331
- existing?.settings
2332
- )
2207
+ const publishableManifest = (0, import_node_path10.join)(workspaceRoot, "packages", name, "package.json");
2208
+ markPublic(publishableManifest);
2209
+ repairPublishableManifest(publishableManifest);
2210
+ removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
2211
+ registerProjectCommands(workspaceRoot, name, { build: true });
2212
+ }
2213
+ function addReactInternalLib(workspaceRoot, name, stack) {
2214
+ ensurePlugin(workspaceRoot, "@nx/react");
2215
+ runNx(
2216
+ [
2217
+ "g",
2218
+ "@nx/react:library",
2219
+ `libs/${name}`,
2220
+ `--name=${name}`,
2221
+ `--bundler=${REACT_LIB_BUNDLER}`,
2222
+ `--unitTestRunner=${stack.testRunner}`,
2223
+ "--linter=none",
2224
+ "--no-interactive"
2225
+ ],
2226
+ workspaceRoot
2333
2227
  );
2334
- if (options.workspaceName !== "undefined") {
2335
- (0, import_node_fs6.rmSync)((0, import_node_path11.join)(workspaceRoot, "undefined.code-workspace"), { force: true });
2228
+ const privateManifest = (0, import_node_path10.join)(workspaceRoot, "libs", name, "package.json");
2229
+ markPrivate(privateManifest);
2230
+ repairPublishableManifest(privateManifest);
2231
+ removeGeneratedEslintConfig(workspaceRoot, `libs/${name}`);
2232
+ registerProjectCommands(workspaceRoot, name, { build: true });
2233
+ }
2234
+
2235
+ // src/commands/add.ts
2236
+ var PROJECT_KINDS = [
2237
+ "react-app",
2238
+ "react-lib",
2239
+ "react-internal-lib",
2240
+ "node-app",
2241
+ "node-function-app",
2242
+ "npm-lib",
2243
+ "internal-lib",
2244
+ "python-app",
2245
+ "python-function-app",
2246
+ "python-lib",
2247
+ "python-internal-lib",
2248
+ "python-vendor",
2249
+ "go-app",
2250
+ "go-function-app",
2251
+ "go-lib",
2252
+ "go-internal-lib",
2253
+ "flutter-app",
2254
+ "flutter-lib",
2255
+ "flutter-internal-lib"
2256
+ ];
2257
+ async function runAdd(kind, name, options) {
2258
+ const workspaceRoot = process.cwd();
2259
+ if (!fileExists((0, import_node_path11.join)(workspaceRoot, "nx.json"))) {
2260
+ throw new Error("No nx.json found here. Run `add` from the workspace root.");
2336
2261
  }
2337
- const publishUrl = pythonPublishUrl(options.registry);
2338
- if (options.ci === "azure" || options.ci === "both") {
2339
- onProgress("azure-pipelines.yml \u2014 build, verify, pack and release");
2340
- writeFileEnsured(
2341
- (0, import_node_path11.join)(workspaceRoot, "azure-pipelines.yml"),
2342
- azurePipelinesYaml(options.agent, options.variableGroup, publishUrl, options.registry.kind)
2343
- );
2262
+ const stack = readWorkspaceStack(workspaceRoot);
2263
+ const kindProvided = kind !== void 0;
2264
+ const resolvedKind = kind ?? await (0, import_prompts5.select)({
2265
+ message: "What kind of project?",
2266
+ choices: PROJECT_KINDS.map((value) => ({ name: value, value }))
2267
+ });
2268
+ const resolvedName = name ?? await promptText("Project name");
2269
+ assertValidProjectName(resolvedName, "Project name");
2270
+ switch (resolvedKind) {
2271
+ case "react-app": {
2272
+ addReactApp(workspaceRoot, resolvedName, stack);
2273
+ break;
2274
+ }
2275
+ case "react-lib": {
2276
+ await addReactLib(workspaceRoot, resolvedName, options, kindProvided, stack);
2277
+ break;
2278
+ }
2279
+ case "react-internal-lib": {
2280
+ addReactInternalLib(workspaceRoot, resolvedName, stack);
2281
+ break;
2282
+ }
2283
+ case "node-app": {
2284
+ addNodeApp(workspaceRoot, resolvedName, stack, options.framework);
2285
+ break;
2286
+ }
2287
+ case "node-function-app": {
2288
+ addNodeFunctionApp(workspaceRoot, resolvedName, stack);
2289
+ break;
2290
+ }
2291
+ case "npm-lib": {
2292
+ await addNpmLib(workspaceRoot, resolvedName, options, kindProvided, stack);
2293
+ break;
2294
+ }
2295
+ case "internal-lib": {
2296
+ runNx(
2297
+ [
2298
+ "g",
2299
+ "@nx/js:lib",
2300
+ `libs/${resolvedName}`,
2301
+ "--bundler=tsc",
2302
+ `--unitTestRunner=${stack.testRunner}`,
2303
+ "--linter=none",
2304
+ "--no-interactive"
2305
+ ],
2306
+ workspaceRoot
2307
+ );
2308
+ markPrivate((0, import_node_path11.join)(workspaceRoot, "libs", resolvedName, "package.json"));
2309
+ removeGeneratedEslintConfig(workspaceRoot, `libs/${resolvedName}`);
2310
+ registerProjectCommands(workspaceRoot, resolvedName, { build: true });
2311
+ break;
2312
+ }
2313
+ case "python-app": {
2314
+ addPythonApp(workspaceRoot, resolvedName);
2315
+ break;
2316
+ }
2317
+ case "python-function-app": {
2318
+ addPythonFunctionApp(workspaceRoot, resolvedName);
2319
+ break;
2320
+ }
2321
+ case "python-lib": {
2322
+ addPythonLib(workspaceRoot, resolvedName);
2323
+ break;
2324
+ }
2325
+ case "python-internal-lib": {
2326
+ addPythonInternalLib(workspaceRoot, resolvedName);
2327
+ break;
2328
+ }
2329
+ case "go-app": {
2330
+ addGoApp(workspaceRoot, resolvedName);
2331
+ break;
2332
+ }
2333
+ case "go-function-app": {
2334
+ addGoFunctionApp(workspaceRoot, resolvedName);
2335
+ break;
2336
+ }
2337
+ case "go-lib": {
2338
+ addGoLib(workspaceRoot, resolvedName);
2339
+ break;
2340
+ }
2341
+ case "go-internal-lib": {
2342
+ addGoInternalLib(workspaceRoot, resolvedName);
2343
+ break;
2344
+ }
2345
+ case "flutter-app": {
2346
+ addFlutterApp(workspaceRoot, resolvedName);
2347
+ break;
2348
+ }
2349
+ case "flutter-lib": {
2350
+ addFlutterLib(workspaceRoot, resolvedName);
2351
+ break;
2352
+ }
2353
+ case "flutter-internal-lib": {
2354
+ addFlutterInternalLib(workspaceRoot, resolvedName);
2355
+ break;
2356
+ }
2357
+ case "python-vendor": {
2358
+ await addPythonVendor(workspaceRoot, resolvedName, options, kindProvided);
2359
+ syncProjectReferences(workspaceRoot);
2360
+ runFormatter(workspaceRoot);
2361
+ return;
2362
+ }
2363
+ default: {
2364
+ const exhaustive = resolvedKind;
2365
+ throw new Error(
2366
+ `Unknown project kind '${exhaustive}'. Expected one of: ${PROJECT_KINDS.join(", ")}.`
2367
+ );
2368
+ }
2344
2369
  }
2345
- if (options.ci === "github" || options.ci === "both") {
2346
- onProgress(".github/workflows/ci.yml and dependabot.yml");
2347
- writeFileEnsured(
2348
- (0, import_node_path11.join)(workspaceRoot, ".github/workflows/ci.yml"),
2349
- githubActionsYaml(options.agent, publishUrl, options.registry.kind, options.ci)
2370
+ syncProjectReferences(workspaceRoot);
2371
+ runFormatter(workspaceRoot);
2372
+ logger.success(`Added ${resolvedKind} '${resolvedName}'.`);
2373
+ }
2374
+ function syncProjectReferences(workspaceRoot) {
2375
+ logger.step("Syncing TypeScript project references (nx sync)");
2376
+ if (runShell("npx", ["nx", "sync"], workspaceRoot) !== 0) {
2377
+ logger.warn(
2378
+ "nx sync did not complete \u2014 run `npx nx sync` yourself so cross-project imports resolve in your editor."
2350
2379
  );
2351
- writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".github/dependabot.yml"), DEPENDABOT_CONFIG);
2352
2380
  }
2353
2381
  }
2382
+ function readWorkspaceStack(workspaceRoot) {
2383
+ const nxJson = readJson(
2384
+ (0, import_node_path11.join)(workspaceRoot, "nx.json")
2385
+ );
2386
+ const stack = nxJson.mnci?.stack;
2387
+ return {
2388
+ testRunner: stack?.testRunner === "vitest" ? "vitest" : "jest"
2389
+ };
2390
+ }
2354
2391
 
2355
2392
  // src/commands/doctor.ts
2393
+ var import_node_fs7 = require("fs");
2394
+ var import_node_path12 = require("path");
2356
2395
  var SUPPORTED_ESLINT_MAJOR = ESLINT_VERSION.replace(/^\D*/, "").split(".", 1)[0];
2357
2396
  function toPosix(path) {
2358
2397
  return path.replaceAll("\\", "/");