@mnci/cli 4.0.5 → 4.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2061 -2041
- package/dist/cli.js.map +1 -1
- 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
|
|
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,2187 @@ function assertValidProjectName(name, label) {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
// src/commands/add/flutter.ts
|
|
195
|
-
var
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
function
|
|
206
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
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
|
|
265
|
-
"eslint.config.
|
|
266
|
-
"
|
|
267
|
-
"
|
|
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
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
options: { command: "flutter run -d chrome", cwd: `apps/${name}` }
|
|
317
|
+
...nxJson,
|
|
318
|
+
namedInputs: { ...namedInputs, sharedGlobals: [...existing, ...missing] }
|
|
345
319
|
};
|
|
346
320
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
420
|
+
eslint: ESLINT_VERSION,
|
|
421
|
+
"@nx/eslint": nxVersion,
|
|
422
|
+
"@nx/eslint-plugin": nxVersion,
|
|
423
|
+
"@mnci/eslint-config": eslintConfigSpec()
|
|
472
424
|
};
|
|
473
425
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
options: { command }
|
|
646
|
+
"@nx/react:application": shared,
|
|
647
|
+
"@nx/react:library": shared,
|
|
648
|
+
"@nx/js:library": shared
|
|
691
649
|
};
|
|
692
650
|
}
|
|
693
|
-
function
|
|
651
|
+
function mnciConfig(options) {
|
|
694
652
|
return {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
752
|
-
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
776
|
-
|
|
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
|
|
788
|
-
return
|
|
704
|
+
function npmAuthEnvVariable(registryKind, variableReference) {
|
|
705
|
+
return registryKind === "npm" ? ["NODE_AUTH_TOKEN", variableReference("NPM_TOKEN")] : ["PAT", variableReference("PAT")];
|
|
789
706
|
}
|
|
790
|
-
function
|
|
791
|
-
|
|
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
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
|
969
764
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
#
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
$
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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)
|
|
1050
889
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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 });
|
|
1077
|
-
}
|
|
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 });
|
|
1098
|
-
}
|
|
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)
|
|
1099
901
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
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."
|
|
1244
|
-
);
|
|
1245
|
-
}
|
|
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}
|
|
932
|
+
|
|
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}
|
|
941
|
+
`;
|
|
1246
942
|
}
|
|
1247
|
-
function
|
|
1248
|
-
const
|
|
1249
|
-
|
|
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} }}`
|
|
1250
948
|
);
|
|
1251
|
-
const
|
|
1252
|
-
return
|
|
1253
|
-
testRunner: stack?.testRunner === "vitest" ? "vitest" : "jest"
|
|
1254
|
-
};
|
|
1255
|
-
}
|
|
949
|
+
const githubReleases = ci === "github";
|
|
950
|
+
return `name: CI
|
|
1256
951
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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.
|
|
1260
957
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
+
`}`;
|
|
1274
1138
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1139
|
+
var DEPENDABOT_CONFIG = `# Generated by MoNecromanCI. Weekly dependency-update PRs so vulnerable or
|
|
1140
|
+
# stale dependencies surface as a reviewable PR instead of only a push-time
|
|
1141
|
+
# warning.
|
|
1142
|
+
version: 2
|
|
1143
|
+
updates:
|
|
1144
|
+
- package-ecosystem: npm
|
|
1145
|
+
directory: "/"
|
|
1146
|
+
schedule:
|
|
1147
|
+
interval: weekly
|
|
1282
1148
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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}
|
|
1149
|
+
- package-ecosystem: github-actions
|
|
1150
|
+
directory: "/"
|
|
1151
|
+
schedule:
|
|
1152
|
+
interval: weekly
|
|
1153
|
+
|
|
1154
|
+
# Globs, not one entry per project: Python projects are added later via
|
|
1155
|
+
# 'mnci add python-*', so this starts covering them automatically the
|
|
1156
|
+
# moment the first one exists \u2014 a glob matching nothing yet is not an error.
|
|
1157
|
+
- package-ecosystem: pip
|
|
1158
|
+
directories:
|
|
1159
|
+
- "/apps/*"
|
|
1160
|
+
- "/python-packages/*"
|
|
1161
|
+
- "/libs/*"
|
|
1162
|
+
schedule:
|
|
1163
|
+
interval: weekly
|
|
1305
1164
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
|
1165
|
+
# Same glob reasoning as pip above. Dart projects are pub workspace members,
|
|
1166
|
+
# so each one declares its own dependencies even though they all resolve
|
|
1167
|
+
# through the single root pubspec.lock \u2014 hence the per-project directories
|
|
1168
|
+
# rather than just "/".
|
|
1169
|
+
- package-ecosystem: pub
|
|
1170
|
+
directories:
|
|
1171
|
+
- "/apps/*"
|
|
1172
|
+
- "/packages/*"
|
|
1173
|
+
- "/libs/*"
|
|
1174
|
+
schedule:
|
|
1175
|
+
interval: weekly
|
|
1328
1176
|
`;
|
|
1177
|
+
var NX_SCAFFOLDING_TO_REMOVE = [".prettierrc", ".prettierrc.json", ".vscode"];
|
|
1178
|
+
function removeNxScaffolding(workspaceRoot) {
|
|
1179
|
+
for (const entry of NX_SCAFFOLDING_TO_REMOVE) {
|
|
1180
|
+
(0, import_node_fs2.rmSync)((0, import_node_path2.join)(workspaceRoot, entry), { recursive: true, force: true });
|
|
1181
|
+
}
|
|
1182
|
+
removeProjectEslintConfigs(workspaceRoot);
|
|
1329
1183
|
}
|
|
1330
|
-
function
|
|
1331
|
-
|
|
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;
|
|
1184
|
+
function removeIfPresent(path) {
|
|
1185
|
+
(0, import_node_fs2.rmSync)(path, { recursive: true, force: true });
|
|
1361
1186
|
}
|
|
1362
|
-
function
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
1187
|
+
function removeProjectEslintConfigs(workspaceRoot) {
|
|
1188
|
+
const matches = (0, import_node_fs2.globSync)("{apps,libs,packages}/*/eslint.config.{js,mjs,cjs,ts,mts,cts}", {
|
|
1189
|
+
cwd: workspaceRoot
|
|
1190
|
+
});
|
|
1191
|
+
for (const match of matches) {
|
|
1192
|
+
(0, import_node_fs2.rmSync)((0, import_node_path2.join)(workspaceRoot, match), { force: true });
|
|
1193
|
+
}
|
|
1366
1194
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
"
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
const
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1195
|
+
function applyOverlay(workspaceRoot, options, onProgress = () => {
|
|
1196
|
+
}) {
|
|
1197
|
+
const nxJsonPath = (0, import_node_path2.join)(workspaceRoot, "nx.json");
|
|
1198
|
+
const nxJson = readJson(nxJsonPath);
|
|
1199
|
+
const generators = {
|
|
1200
|
+
...nxJson.generators,
|
|
1201
|
+
...generatorDefaults(options.stack)
|
|
1202
|
+
};
|
|
1203
|
+
const sync = { ...nxJson.sync, ...SYNC_CONFIG };
|
|
1204
|
+
const mnci = { ...nxJson.mnci, ...mnciConfig(options) };
|
|
1205
|
+
const patched = withSharedGlobals(withEslintPlugin(withReleaseConfig(nxJson, options.ci)));
|
|
1206
|
+
onProgress("nx.json \u2014 release, sync, generators, shared inputs, mnci block");
|
|
1207
|
+
writeFileEnsured(nxJsonPath, toJson({ ...patched, generators, sync, mnci }));
|
|
1208
|
+
onProgress("package.json \u2014 root scripts, TS compilers, ESLint toolchain, overrides");
|
|
1209
|
+
const manifestPath = (0, import_node_path2.join)(workspaceRoot, "package.json");
|
|
1210
|
+
const manifest = readJson(manifestPath);
|
|
1211
|
+
const scripts = {
|
|
1212
|
+
...manifest.scripts,
|
|
1213
|
+
...rootScripts()
|
|
1214
|
+
};
|
|
1215
|
+
const existingDevDeps = manifest.devDependencies;
|
|
1216
|
+
const devDeps = withoutRetiredFormatterDependencies({
|
|
1217
|
+
...existingDevDeps,
|
|
1218
|
+
...TS_COMPILER_DEPENDENCIES,
|
|
1219
|
+
// The preset pins `nx` itself; the ESLint plugins must match it exactly.
|
|
1220
|
+
...eslintToolchainDependencies(existingDevDeps?.nx ?? "latest")
|
|
1221
|
+
});
|
|
1222
|
+
const overrides = {
|
|
1223
|
+
...manifest.overrides,
|
|
1224
|
+
...ESLINT_PEER_OVERRIDES,
|
|
1225
|
+
...SECURITY_OVERRIDES,
|
|
1226
|
+
// Last, and conditional — see reactExpressPeerOverride. At `mnci new` there
|
|
1227
|
+
// is no express yet, so this is a no-op; `mnci add node-app --framework
|
|
1228
|
+
// express` is what puts express in the root manifest, and syncs the override
|
|
1229
|
+
// itself. This line is what carries it across an `mnci upgrade`.
|
|
1230
|
+
...reactExpressPeerOverride(manifest)
|
|
1231
|
+
};
|
|
1232
|
+
const existingNx = manifest.nx;
|
|
1233
|
+
const nx = {
|
|
1234
|
+
...existingNx,
|
|
1235
|
+
includedScripts: existingNx?.includedScripts ?? [],
|
|
1236
|
+
targets: {
|
|
1237
|
+
...existingNx?.targets,
|
|
1238
|
+
lint: ROOT_LINT_TARGET
|
|
1239
|
+
}
|
|
1379
1240
|
};
|
|
1241
|
+
writeFileEnsured(
|
|
1242
|
+
manifestPath,
|
|
1243
|
+
toJson({
|
|
1244
|
+
...manifest,
|
|
1245
|
+
name: `${options.scope}/source`,
|
|
1246
|
+
scripts,
|
|
1247
|
+
devDependencies: devDeps,
|
|
1248
|
+
overrides,
|
|
1249
|
+
nx
|
|
1250
|
+
})
|
|
1251
|
+
);
|
|
1252
|
+
onProgress(
|
|
1253
|
+
`.npmrc \u2014 ${options.registry.kind === "azure-artifacts" ? "Azure Artifacts feed routing and credentials" : "public npm registry auth"}`
|
|
1254
|
+
);
|
|
1255
|
+
writeFileEnsured((0, import_node_path2.join)(workspaceRoot, ".npmrc"), npmrcContent(options.registry, options.scope));
|
|
1256
|
+
onProgress("commitlint.config.mjs and .husky/commit-msg \u2014 conventional commit enforcement");
|
|
1257
|
+
writeFileEnsured((0, import_node_path2.join)(workspaceRoot, "commitlint.config.mjs"), COMMITLINT_CONFIG);
|
|
1258
|
+
const hookPath = (0, import_node_path2.join)(workspaceRoot, ".husky/commit-msg");
|
|
1259
|
+
writeFileEnsured(hookPath, COMMIT_MSG_HOOK);
|
|
1260
|
+
markExecutable(hookPath);
|
|
1261
|
+
onProgress("eslint.config.mjs \u2014 the shared lint AND formatting opinion");
|
|
1262
|
+
writeFileEnsured((0, import_node_path2.join)(workspaceRoot, "eslint.config.mjs"), ESLINT_CONFIG);
|
|
1263
|
+
for (const retired of RETIRED_FORMATTER_FILES) {
|
|
1264
|
+
removeIfPresent((0, import_node_path2.join)(workspaceRoot, retired));
|
|
1265
|
+
}
|
|
1266
|
+
onProgress(".devcontainer/devcontainer.json \u2014 a local toolchain matching CI");
|
|
1267
|
+
writeFileEnsured(
|
|
1268
|
+
(0, import_node_path2.join)(workspaceRoot, ".devcontainer/devcontainer.json"),
|
|
1269
|
+
devcontainerJson(options.workspaceName)
|
|
1270
|
+
);
|
|
1271
|
+
removeNxScaffolding(workspaceRoot);
|
|
1272
|
+
onProgress(
|
|
1273
|
+
`${options.workspaceName}.code-workspace \u2014 settings, extensions, launch configs`
|
|
1274
|
+
);
|
|
1275
|
+
const codeWorkspacePath = (0, import_node_path2.join)(workspaceRoot, `${options.workspaceName}.code-workspace`);
|
|
1276
|
+
const existing = readCodeWorkspace(codeWorkspacePath);
|
|
1277
|
+
writeFileEnsured(
|
|
1278
|
+
codeWorkspacePath,
|
|
1279
|
+
vscodeWorkspace(
|
|
1280
|
+
options.workspaceName,
|
|
1281
|
+
existing?.tasks,
|
|
1282
|
+
existing?.launch,
|
|
1283
|
+
existing?.settings
|
|
1284
|
+
)
|
|
1285
|
+
);
|
|
1286
|
+
if (options.workspaceName !== "undefined") {
|
|
1287
|
+
(0, import_node_fs2.rmSync)((0, import_node_path2.join)(workspaceRoot, "undefined.code-workspace"), { force: true });
|
|
1288
|
+
}
|
|
1289
|
+
const publishUrl = pythonPublishUrl(options.registry);
|
|
1290
|
+
if (options.ci === "azure" || options.ci === "both") {
|
|
1291
|
+
onProgress("azure-pipelines.yml \u2014 build, verify, pack and release");
|
|
1292
|
+
writeFileEnsured(
|
|
1293
|
+
(0, import_node_path2.join)(workspaceRoot, "azure-pipelines.yml"),
|
|
1294
|
+
azurePipelinesYaml(options.agent, options.variableGroup, publishUrl, options.registry.kind)
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
if (options.ci === "github" || options.ci === "both") {
|
|
1298
|
+
onProgress(".github/workflows/ci.yml and dependabot.yml");
|
|
1299
|
+
writeFileEnsured(
|
|
1300
|
+
(0, import_node_path2.join)(workspaceRoot, ".github/workflows/ci.yml"),
|
|
1301
|
+
githubActionsYaml(options.agent, publishUrl, options.registry.kind, options.ci)
|
|
1302
|
+
);
|
|
1303
|
+
writeFileEnsured((0, import_node_path2.join)(workspaceRoot, ".github/dependabot.yml"), DEPENDABOT_CONFIG);
|
|
1304
|
+
}
|
|
1380
1305
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1306
|
+
|
|
1307
|
+
// src/commands/add/shared.ts
|
|
1308
|
+
function hasPlugin(workspaceRoot, packageName) {
|
|
1309
|
+
const manifest = readJson((0, import_node_path3.join)(workspaceRoot, "package.json"));
|
|
1310
|
+
const installed = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
1311
|
+
return Object.hasOwn(installed, packageName);
|
|
1383
1312
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
return process.env.MNCI_ESLINT_CONFIG_SPEC ?? ESLINT_CONFIG_VERSION;
|
|
1313
|
+
function ensurePlugin(workspaceRoot, packageName) {
|
|
1314
|
+
if (hasPlugin(workspaceRoot, packageName)) {
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
logger.step(`Installing Nx plugin ${packageName}`);
|
|
1318
|
+
runNx(["add", packageName], workspaceRoot);
|
|
1391
1319
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
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
|
-
`;
|
|
1320
|
+
function markPrivate(manifestPath) {
|
|
1321
|
+
const manifest = readJson(manifestPath);
|
|
1322
|
+
writeFileEnsured(manifestPath, toJson({ ...manifest, private: true }));
|
|
1420
1323
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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: "."
|
|
1324
|
+
function markPublic(manifestPath) {
|
|
1325
|
+
const manifest = readJson(manifestPath);
|
|
1326
|
+
writeFileEnsured(manifestPath, toJson({ ...manifest, publishConfig: { access: "public" } }));
|
|
1327
|
+
}
|
|
1328
|
+
var WRONG_TYPES_PATH = "./dist/index.esm.d.ts";
|
|
1329
|
+
var ACTUAL_TYPES_PATH = "./dist/src/index.d.ts";
|
|
1330
|
+
var DECLARATION_MAP_EXCLUSION = "!**/*.d.ts.map";
|
|
1331
|
+
function repairPublishableManifest(manifestPath) {
|
|
1332
|
+
const manifest = readJson(manifestPath);
|
|
1333
|
+
if (manifest.types === WRONG_TYPES_PATH) {
|
|
1334
|
+
manifest.types = ACTUAL_TYPES_PATH;
|
|
1440
1335
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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
|
-
};
|
|
1336
|
+
const dot = manifest.exports?.["."];
|
|
1337
|
+
if (typeof dot === "object" && dot.types === WRONG_TYPES_PATH) {
|
|
1338
|
+
dot.types = ACTUAL_TYPES_PATH;
|
|
1339
|
+
}
|
|
1340
|
+
if (manifest.files && !manifest.files.includes(DECLARATION_MAP_EXCLUSION)) {
|
|
1341
|
+
manifest.files.push(DECLARATION_MAP_EXCLUSION);
|
|
1342
|
+
}
|
|
1343
|
+
writeFileEnsured(manifestPath, toJson(manifest));
|
|
1478
1344
|
}
|
|
1479
|
-
|
|
1480
|
-
"
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
]
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
"
|
|
1490
|
-
|
|
1491
|
-
"oxlint",
|
|
1492
|
-
"oxfmt",
|
|
1493
|
-
"@mnci/oxlint-config"
|
|
1494
|
-
]);
|
|
1495
|
-
return Object.fromEntries(Object.entries(devDeps).filter(([name]) => !retired.has(name)));
|
|
1345
|
+
function ensureAdmZip(workspaceRoot) {
|
|
1346
|
+
if (hasPlugin(workspaceRoot, "adm-zip")) {
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
logger.step("Installing the app packager (adm-zip)");
|
|
1350
|
+
if (runShell(
|
|
1351
|
+
"npm",
|
|
1352
|
+
["install", "--save-dev", "adm-zip", "--no-audit", "--no-fund"],
|
|
1353
|
+
workspaceRoot
|
|
1354
|
+
) !== 0) {
|
|
1355
|
+
throw new Error("npm install of adm-zip failed");
|
|
1356
|
+
}
|
|
1496
1357
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
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"
|
|
1358
|
+
function defaultScope(workspaceRoot) {
|
|
1359
|
+
const { name } = readJson((0, import_node_path3.join)(workspaceRoot, "package.json"));
|
|
1360
|
+
const base = (name.startsWith("@") ? name.slice(1) : name).split("/", 1)[0];
|
|
1361
|
+
return `@${base}`;
|
|
1362
|
+
}
|
|
1363
|
+
function addNxTargets(manifestPath, newTargets) {
|
|
1364
|
+
const manifest = fileExists(manifestPath) ? readJson(manifestPath) : {};
|
|
1365
|
+
const nx = manifest.nx ?? {};
|
|
1366
|
+
const targets = nx.targets ?? {};
|
|
1367
|
+
writeFileEnsured(
|
|
1368
|
+
manifestPath,
|
|
1369
|
+
toJson({ ...manifest, nx: { ...nx, targets: { ...targets, ...newTargets } } })
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
var ESLINT_CONFIG_FILENAMES = [
|
|
1373
|
+
"eslint.config.js",
|
|
1374
|
+
"eslint.config.mjs",
|
|
1375
|
+
"eslint.config.cjs",
|
|
1376
|
+
"eslint.config.ts",
|
|
1377
|
+
"eslint.config.mts",
|
|
1378
|
+
"eslint.config.cts"
|
|
1593
1379
|
];
|
|
1594
|
-
function
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
-
};
|
|
1380
|
+
function removeGeneratedEslintConfig(workspaceRoot, projectRoot) {
|
|
1381
|
+
for (const filename of ESLINT_CONFIG_FILENAMES) {
|
|
1382
|
+
(0, import_node_fs3.rmSync)((0, import_node_path3.join)(workspaceRoot, projectRoot, filename), { force: true });
|
|
1383
|
+
}
|
|
1384
|
+
(0, import_node_fs3.rmSync)((0, import_node_path3.join)(workspaceRoot, ".vscode"), { recursive: true, force: true });
|
|
1625
1385
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
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
|
-
}));
|
|
1386
|
+
function findCodeWorkspaceFile(workspaceRoot) {
|
|
1387
|
+
try {
|
|
1388
|
+
const entry = (0, import_node_fs3.readdirSync)(workspaceRoot).find((file) => file.endsWith(".code-workspace"));
|
|
1389
|
+
return entry ? (0, import_node_path3.join)(workspaceRoot, entry) : void 0;
|
|
1390
|
+
} catch {
|
|
1391
|
+
return void 0;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
function projectTask(name, kind) {
|
|
1395
|
+
const script = `${name}:${kind}`;
|
|
1396
|
+
const base = { label: `${name}: ${kind}`, type: "npm", script, problemMatcher: [] };
|
|
1397
|
+
return kind === "start" ? { ...base, isBackground: true } : { ...base, group: kind };
|
|
1642
1398
|
}
|
|
1643
|
-
function
|
|
1644
|
-
const
|
|
1645
|
-
|
|
1399
|
+
function registerProjectCommands(workspaceRoot, name, commands) {
|
|
1400
|
+
const scripts = {
|
|
1401
|
+
[`${name}:qa`]: `nx run ${name}:lint && nx run ${name}:test`
|
|
1402
|
+
};
|
|
1403
|
+
if (commands.build) {
|
|
1404
|
+
scripts[`${name}:build`] = `nx run ${name}:build`;
|
|
1405
|
+
}
|
|
1406
|
+
if (commands.start) {
|
|
1407
|
+
scripts[`${name}:start`] = commands.start;
|
|
1408
|
+
}
|
|
1409
|
+
const manifestPath = (0, import_node_path3.join)(workspaceRoot, "package.json");
|
|
1410
|
+
const manifest = readJson(manifestPath);
|
|
1411
|
+
const existingScripts = manifest.scripts ?? {};
|
|
1412
|
+
const overrides = {
|
|
1413
|
+
...manifest.overrides,
|
|
1414
|
+
...reactExpressPeerOverride(manifest)
|
|
1415
|
+
};
|
|
1416
|
+
writeFileEnsured(
|
|
1417
|
+
manifestPath,
|
|
1418
|
+
toJson({
|
|
1419
|
+
...manifest,
|
|
1420
|
+
scripts: { ...existingScripts, ...scripts },
|
|
1421
|
+
...Object.keys(overrides).length > 0 && { overrides }
|
|
1422
|
+
})
|
|
1646
1423
|
);
|
|
1647
|
-
const
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1424
|
+
const codeWorkspacePath = findCodeWorkspaceFile(workspaceRoot);
|
|
1425
|
+
if (!codeWorkspacePath) {
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
const workspaceFile = readCodeWorkspace(codeWorkspacePath) ?? {};
|
|
1429
|
+
const label = (task) => task.label ?? "";
|
|
1430
|
+
const existingTasks = (workspaceFile.tasks?.tasks ?? []).filter(
|
|
1431
|
+
(task) => !label(task).startsWith(`${name}: `)
|
|
1432
|
+
);
|
|
1433
|
+
const newTasks = [
|
|
1434
|
+
projectTask(name, "qa"),
|
|
1435
|
+
...commands.build ? [projectTask(name, "build")] : [],
|
|
1436
|
+
...commands.start ? [projectTask(name, "start")] : []
|
|
1437
|
+
];
|
|
1438
|
+
writeFileEnsured(
|
|
1439
|
+
codeWorkspacePath,
|
|
1440
|
+
toJson({
|
|
1441
|
+
...workspaceFile,
|
|
1653
1442
|
tasks: {
|
|
1654
|
-
version:
|
|
1655
|
-
tasks: existingTasks
|
|
1656
|
-
},
|
|
1657
|
-
launch: {
|
|
1658
|
-
version: existingLaunch?.version ?? "0.2.0",
|
|
1659
|
-
configurations: [...launchConfigurations(workspaceName), ...userConfigurations]
|
|
1443
|
+
version: workspaceFile.tasks?.version ?? "2.0.0",
|
|
1444
|
+
tasks: [...existingTasks, ...newTasks]
|
|
1660
1445
|
}
|
|
1661
|
-
}
|
|
1662
|
-
null,
|
|
1663
|
-
2
|
|
1446
|
+
})
|
|
1664
1447
|
);
|
|
1665
1448
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
-
};
|
|
1449
|
+
|
|
1450
|
+
// src/commands/add/flutter.ts
|
|
1451
|
+
function addProjectJsonTargets(projectJsonPath, newTargets) {
|
|
1452
|
+
const project = readJson(projectJsonPath);
|
|
1453
|
+
const targets = project.targets ?? {};
|
|
1454
|
+
writeFileEnsured(projectJsonPath, toJson({ ...project, targets: { ...targets, ...newTargets } }));
|
|
1686
1455
|
}
|
|
1687
|
-
function
|
|
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
|
-
};
|
|
1456
|
+
function flutterAppStartTarget(name) {
|
|
1698
1457
|
return {
|
|
1699
|
-
"
|
|
1700
|
-
|
|
1701
|
-
|
|
1458
|
+
executor: "nx:run-commands",
|
|
1459
|
+
continuous: true,
|
|
1460
|
+
options: { command: "flutter run -d chrome", cwd: `apps/${name}` }
|
|
1702
1461
|
};
|
|
1703
1462
|
}
|
|
1704
|
-
function
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
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
|
-
};
|
|
1463
|
+
function ensureFlutter(workspaceRoot) {
|
|
1464
|
+
if (runShell("flutter", ["--version"], workspaceRoot) !== 0) {
|
|
1465
|
+
throw new Error(
|
|
1466
|
+
"Flutter not found. Install the Flutter SDK first (3.27+, for Dart 3.6+ pub workspaces): https://docs.flutter.dev/get-started/install"
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1719
1469
|
}
|
|
1720
|
-
function
|
|
1721
|
-
|
|
1722
|
-
return nxJson.mnci ?? {};
|
|
1470
|
+
function nxFlutterPluginSpec() {
|
|
1471
|
+
return process.env.MNCI_NX_FLUTTER_SPEC ?? "@mnci/nx-flutter";
|
|
1723
1472
|
}
|
|
1724
|
-
function
|
|
1725
|
-
if (
|
|
1726
|
-
return
|
|
1473
|
+
function ensureNxFlutterPlugin(workspaceRoot) {
|
|
1474
|
+
if (hasPlugin(workspaceRoot, "@mnci/nx-flutter")) {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
const spec = nxFlutterPluginSpec();
|
|
1478
|
+
logger.step(`Installing the Flutter toolchain plugin (${spec})`);
|
|
1479
|
+
if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
|
|
1480
|
+
throw new Error("npm install of @mnci/nx-flutter failed");
|
|
1727
1481
|
}
|
|
1728
|
-
return void 0;
|
|
1729
1482
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
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)"`;
|
|
1483
|
+
function prepareFlutter(workspaceRoot) {
|
|
1484
|
+
ensureFlutter(workspaceRoot);
|
|
1485
|
+
ensureNxFlutterPlugin(workspaceRoot);
|
|
1753
1486
|
}
|
|
1754
|
-
function
|
|
1755
|
-
|
|
1487
|
+
function addFlutterApp(workspaceRoot, name) {
|
|
1488
|
+
prepareFlutter(workspaceRoot);
|
|
1489
|
+
ensureAdmZip(workspaceRoot);
|
|
1490
|
+
runNx(["g", "@mnci/nx-flutter:application", name, "--no-interactive"], workspaceRoot);
|
|
1491
|
+
addProjectJsonTargets((0, import_node_path4.join)(workspaceRoot, "apps", name, "project.json"), {
|
|
1492
|
+
start: flutterAppStartTarget(name)
|
|
1493
|
+
});
|
|
1494
|
+
registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
|
|
1495
|
+
}
|
|
1496
|
+
function addFlutterLib(workspaceRoot, name) {
|
|
1497
|
+
prepareFlutter(workspaceRoot);
|
|
1498
|
+
runNx(["g", "@mnci/nx-flutter:library", name, "--no-interactive"], workspaceRoot);
|
|
1499
|
+
registerProjectCommands(workspaceRoot, name, { build: false });
|
|
1500
|
+
}
|
|
1501
|
+
function addFlutterInternalLib(workspaceRoot, name) {
|
|
1502
|
+
prepareFlutter(workspaceRoot);
|
|
1503
|
+
runNx(["g", "@mnci/nx-flutter:internal-library", name, "--no-interactive"], workspaceRoot);
|
|
1504
|
+
registerProjectCommands(workspaceRoot, name, { build: false });
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/commands/add/go.ts
|
|
1508
|
+
var import_node_fs4 = require("fs");
|
|
1509
|
+
var import_node_path5 = require("path");
|
|
1510
|
+
function ensureGo(workspaceRoot) {
|
|
1511
|
+
if (runShell("go", ["version"], workspaceRoot) !== 0) {
|
|
1512
|
+
throw new Error("Go not found. Install Go 1.21+ first: https://go.dev/dl/");
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
function warnIfNoGolangciLint(workspaceRoot) {
|
|
1516
|
+
if (runShell("golangci-lint", ["--version"], workspaceRoot) !== 0) {
|
|
1517
|
+
logger.warn(
|
|
1518
|
+
"golangci-lint not found \u2014 the generated lint target needs it. Install: https://golangci-lint.run/welcome/install/"
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function nxGoPluginSpec() {
|
|
1523
|
+
return process.env.MNCI_NX_GO_SPEC ?? "@nx-go/nx-go";
|
|
1524
|
+
}
|
|
1525
|
+
function ensureNxGoPlugin(workspaceRoot) {
|
|
1526
|
+
if (hasPlugin(workspaceRoot, "@nx-go/nx-go")) {
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const spec = nxGoPluginSpec();
|
|
1530
|
+
logger.step(`Installing the Go toolchain plugin (${spec})`);
|
|
1531
|
+
if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
|
|
1532
|
+
throw new Error("npm install of @nx-go/nx-go failed");
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
function ensureGoModule(workspaceRoot) {
|
|
1536
|
+
if (fileExists((0, import_node_path5.join)(workspaceRoot, "go.mod"))) {
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
logger.step("Bootstrapping the workspace Go module (single root go.mod)");
|
|
1540
|
+
runNx(["g", "@nx-go/nx-go:init", "--no-interactive"], workspaceRoot);
|
|
1541
|
+
runNx(["g", "@nx-go/nx-go:convert-to-one-mod", "--no-interactive"], workspaceRoot);
|
|
1542
|
+
}
|
|
1543
|
+
function goModulePath(workspaceRoot) {
|
|
1544
|
+
try {
|
|
1545
|
+
const contents = (0, import_node_fs4.readFileSync)((0, import_node_path5.join)(workspaceRoot, "go.mod"), "utf8");
|
|
1546
|
+
return /^module\s+(\S+)/m.exec(contents)?.[1];
|
|
1547
|
+
} catch {
|
|
1548
|
+
return void 0;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
function addProjectJsonTargets2(projectJsonPath, newTargets) {
|
|
1552
|
+
const project = readJson(projectJsonPath);
|
|
1553
|
+
const targets = project.targets ?? {};
|
|
1554
|
+
writeFileEnsured(projectJsonPath, toJson({ ...project, targets: { ...targets, ...newTargets } }));
|
|
1555
|
+
}
|
|
1556
|
+
function goTestTarget() {
|
|
1557
|
+
return { executor: "@nx-go/nx-go:test" };
|
|
1558
|
+
}
|
|
1559
|
+
function goLintTarget() {
|
|
1560
|
+
return {
|
|
1561
|
+
executor: "@nx-go/nx-go:lint",
|
|
1562
|
+
parallelism: false,
|
|
1563
|
+
options: { linter: "golangci-lint", args: ["run"] }
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function goBuildTarget(name) {
|
|
1567
|
+
return {
|
|
1568
|
+
executor: "@nx-go/nx-go:build",
|
|
1569
|
+
outputs: [`{workspaceRoot}/dist/apps/${name}`],
|
|
1570
|
+
options: { outputPath: `../../dist/apps/${name}/${name}` }
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function goPackageTarget(tag, name) {
|
|
1574
|
+
const zip = `dist/drop/${tag}-${name}.zip`;
|
|
1575
|
+
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}')"`;
|
|
1576
|
+
return {
|
|
1577
|
+
executor: "nx:run-commands",
|
|
1578
|
+
dependsOn: ["build"],
|
|
1579
|
+
outputs: [`{workspaceRoot}/${zip}`],
|
|
1580
|
+
options: { command }
|
|
1581
|
+
};
|
|
1756
1582
|
}
|
|
1757
|
-
function
|
|
1758
|
-
return
|
|
1583
|
+
function goStartTarget(name) {
|
|
1584
|
+
return {
|
|
1585
|
+
executor: "nx:run-commands",
|
|
1586
|
+
continuous: true,
|
|
1587
|
+
options: { command: "go run .", cwd: `apps/${name}` }
|
|
1588
|
+
};
|
|
1759
1589
|
}
|
|
1760
|
-
function
|
|
1761
|
-
|
|
1590
|
+
function prepareGo(workspaceRoot) {
|
|
1591
|
+
ensureGo(workspaceRoot);
|
|
1592
|
+
warnIfNoGolangciLint(workspaceRoot);
|
|
1593
|
+
ensureNxGoPlugin(workspaceRoot);
|
|
1594
|
+
ensureGoModule(workspaceRoot);
|
|
1762
1595
|
}
|
|
1763
|
-
function
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
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
|
-
`;
|
|
1596
|
+
function addGoApp(workspaceRoot, name) {
|
|
1597
|
+
prepareGo(workspaceRoot);
|
|
1598
|
+
ensureAdmZip(workspaceRoot);
|
|
1599
|
+
runNx(
|
|
1600
|
+
[
|
|
1601
|
+
"g",
|
|
1602
|
+
"@nx-go/nx-go:application",
|
|
1603
|
+
`apps/${name}`,
|
|
1604
|
+
`--name=${name}`,
|
|
1605
|
+
"--tags=type:go-app",
|
|
1606
|
+
"--no-interactive"
|
|
1607
|
+
],
|
|
1608
|
+
workspaceRoot
|
|
1609
|
+
);
|
|
1610
|
+
addProjectJsonTargets2((0, import_node_path5.join)(workspaceRoot, "apps", name, "project.json"), {
|
|
1611
|
+
build: goBuildTarget(name),
|
|
1612
|
+
test: goTestTarget(),
|
|
1613
|
+
lint: goLintTarget(),
|
|
1614
|
+
package: goPackageTarget("go-app", name),
|
|
1615
|
+
start: goStartTarget(name)
|
|
1616
|
+
});
|
|
1617
|
+
registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
|
|
1995
1618
|
}
|
|
1996
|
-
function
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
1619
|
+
function addGoFunctionApp(workspaceRoot, name) {
|
|
1620
|
+
prepareGo(workspaceRoot);
|
|
1621
|
+
ensureAdmZip(workspaceRoot);
|
|
1622
|
+
runNx(
|
|
1623
|
+
[
|
|
1624
|
+
"g",
|
|
1625
|
+
"@nx-go/nx-go:application",
|
|
1626
|
+
`apps/${name}`,
|
|
1627
|
+
`--name=${name}`,
|
|
1628
|
+
"--tags=type:go-function-app",
|
|
1629
|
+
"--no-interactive"
|
|
1630
|
+
],
|
|
1631
|
+
workspaceRoot
|
|
2001
1632
|
);
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
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)
|
|
1633
|
+
addProjectJsonTargets2((0, import_node_path5.join)(workspaceRoot, "apps", name, "project.json"), {
|
|
1634
|
+
build: goBuildTarget(name),
|
|
1635
|
+
test: goTestTarget(),
|
|
1636
|
+
lint: goLintTarget(),
|
|
1637
|
+
package: goPackageTarget("go-function-app", name)
|
|
1638
|
+
});
|
|
1639
|
+
registerProjectCommands(workspaceRoot, name, { build: true });
|
|
1640
|
+
}
|
|
1641
|
+
function addGoLib(workspaceRoot, name) {
|
|
1642
|
+
prepareGo(workspaceRoot);
|
|
1643
|
+
runNx(
|
|
1644
|
+
[
|
|
1645
|
+
"g",
|
|
1646
|
+
"@nx-go/nx-go:library",
|
|
1647
|
+
`packages/${name}`,
|
|
1648
|
+
`--name=${name}`,
|
|
1649
|
+
"--tags=type:go-lib",
|
|
1650
|
+
"--no-interactive"
|
|
1651
|
+
],
|
|
1652
|
+
workspaceRoot
|
|
1653
|
+
);
|
|
1654
|
+
addProjectJsonTargets2((0, import_node_path5.join)(workspaceRoot, "packages", name, "project.json"), {
|
|
1655
|
+
test: goTestTarget(),
|
|
1656
|
+
lint: goLintTarget()
|
|
1657
|
+
});
|
|
1658
|
+
registerProjectCommands(workspaceRoot, name, { build: false });
|
|
1659
|
+
const module2 = goModulePath(workspaceRoot);
|
|
1660
|
+
if (module2) {
|
|
1661
|
+
logger.step(`Import it as ${module2}/packages/${name}`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
function addGoInternalLib(workspaceRoot, name) {
|
|
1665
|
+
prepareGo(workspaceRoot);
|
|
1666
|
+
runNx(
|
|
1667
|
+
[
|
|
1668
|
+
"g",
|
|
1669
|
+
"@nx-go/nx-go:library",
|
|
1670
|
+
`libs/${name}`,
|
|
1671
|
+
`--name=${name}`,
|
|
1672
|
+
"--tags=type:go-internal-lib",
|
|
1673
|
+
"--no-interactive"
|
|
1674
|
+
],
|
|
1675
|
+
workspaceRoot
|
|
1676
|
+
);
|
|
1677
|
+
addProjectJsonTargets2((0, import_node_path5.join)(workspaceRoot, "libs", name, "project.json"), {
|
|
1678
|
+
test: goTestTarget(),
|
|
1679
|
+
lint: goLintTarget()
|
|
1680
|
+
});
|
|
1681
|
+
registerProjectCommands(workspaceRoot, name, { build: false });
|
|
1682
|
+
const module2 = goModulePath(workspaceRoot);
|
|
1683
|
+
if (module2) {
|
|
1684
|
+
logger.step(`Import it as ${module2}/libs/${name}`);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
2130
1687
|
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
1688
|
+
// src/commands/add/node.ts
|
|
1689
|
+
var import_node_path6 = require("path");
|
|
1690
|
+
function ensurePlugin2(workspaceRoot, packageName) {
|
|
1691
|
+
if (hasPlugin(workspaceRoot, packageName)) {
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
logger.step(`Installing Nx plugin ${packageName}`);
|
|
1695
|
+
runNx(["add", packageName], workspaceRoot);
|
|
1696
|
+
}
|
|
1697
|
+
function runNodeApp(workspaceRoot, name, stack, framework) {
|
|
1698
|
+
ensurePlugin2(workspaceRoot, "@nx/node");
|
|
1699
|
+
runNx(
|
|
1700
|
+
[
|
|
1701
|
+
"g",
|
|
1702
|
+
"@nx/node:application",
|
|
1703
|
+
`apps/${name}`,
|
|
1704
|
+
"--bundler=esbuild",
|
|
1705
|
+
`--unitTestRunner=${stack.testRunner}`,
|
|
1706
|
+
"--linter=none",
|
|
1707
|
+
"--e2eTestRunner=none",
|
|
1708
|
+
`--framework=${framework}`,
|
|
1709
|
+
"--no-interactive"
|
|
1710
|
+
],
|
|
1711
|
+
workspaceRoot
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
function nodeAppPackageTarget(name) {
|
|
1715
|
+
const zip = `dist/drop/node-app-${name}.zip`;
|
|
1716
|
+
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}')"`;
|
|
1717
|
+
return {
|
|
1718
|
+
executor: "nx:run-commands",
|
|
1719
|
+
dependsOn: ["build"],
|
|
1720
|
+
outputs: [`{workspaceRoot}/${zip}`],
|
|
1721
|
+
options: { command }
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
function addNodeApp(workspaceRoot, name, stack, framework = "none") {
|
|
1725
|
+
runNodeApp(workspaceRoot, name, stack, framework);
|
|
1726
|
+
ensureAdmZip(workspaceRoot);
|
|
1727
|
+
addNxTargets((0, import_node_path6.join)(workspaceRoot, "apps", name, "package.json"), {
|
|
1728
|
+
package: nodeAppPackageTarget(name)
|
|
1729
|
+
});
|
|
1730
|
+
removeGeneratedEslintConfig(workspaceRoot, `apps/${name}`);
|
|
1731
|
+
registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:serve` });
|
|
1732
|
+
}
|
|
1733
|
+
function ensureAzureFunctionsPackage(workspaceRoot) {
|
|
1734
|
+
if (hasPlugin(workspaceRoot, "@azure/functions")) {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
logger.step("Installing @azure/functions");
|
|
1738
|
+
if (runShell("npm", ["install", "@azure/functions", "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
|
|
1739
|
+
throw new Error("npm install of @azure/functions failed");
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
var NODE_FUNCTION_APP_MAIN = `// esbuild only includes what is reachable from here, so add one import per
|
|
1743
|
+
// function file you create under src/functions/.
|
|
1744
|
+
import './functions/hello'
|
|
1745
|
+
`;
|
|
1746
|
+
var NODE_FUNCTION_APP_HOST_JSON = `{
|
|
1747
|
+
"version": "2.0",
|
|
1748
|
+
"extensionBundle": {
|
|
1749
|
+
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
|
1750
|
+
"version": "[4.*, 5.0.0)"
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
`;
|
|
1754
|
+
var NODE_FUNCTION_APP_HELLO = `import { app } from '@azure/functions'
|
|
1755
|
+
import type { HttpRequest, HttpResponseInit } from '@azure/functions'
|
|
1756
|
+
import { buildGreeting } from './greeting'
|
|
2138
1757
|
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
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)
|
|
1758
|
+
async function hello(request: HttpRequest): Promise<HttpResponseInit> {
|
|
1759
|
+
const name = request.query.get('name') ?? 'world'
|
|
1760
|
+
return { body: buildGreeting(name) }
|
|
1761
|
+
}
|
|
2151
1762
|
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
1763
|
+
app.http('hello', {
|
|
1764
|
+
methods: ['GET'],
|
|
1765
|
+
authLevel: 'anonymous',
|
|
1766
|
+
route: 'hello',
|
|
1767
|
+
handler: hello,
|
|
1768
|
+
})
|
|
1769
|
+
`;
|
|
1770
|
+
var NODE_FUNCTION_APP_GREETING = `export function buildGreeting (name: string): string {
|
|
1771
|
+
return 'Hello, ' + name + '!'
|
|
1772
|
+
}
|
|
1773
|
+
`;
|
|
1774
|
+
var NODE_FUNCTION_APP_GREETING_SPEC = `import { buildGreeting } from './greeting'
|
|
2157
1775
|
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
1776
|
+
describe('buildGreeting', () => {
|
|
1777
|
+
it('greets a name', () => {
|
|
1778
|
+
expect(buildGreeting('world')).toBe('Hello, world!')
|
|
1779
|
+
})
|
|
1780
|
+
})
|
|
1781
|
+
`;
|
|
1782
|
+
function repairNodeFunctionAppManifest(nodeFunctionAppRoot, workspaceRoot) {
|
|
1783
|
+
const manifestPath = (0, import_node_path6.join)(nodeFunctionAppRoot, "package.json");
|
|
1784
|
+
const manifest = readJson(manifestPath);
|
|
1785
|
+
const azureFunctionsVersion = readJson(
|
|
1786
|
+
(0, import_node_path6.join)(workspaceRoot, "node_modules/@azure/functions/package.json")
|
|
1787
|
+
).version;
|
|
1788
|
+
const dependencies = manifest.dependencies ?? {};
|
|
1789
|
+
writeFileEnsured(
|
|
1790
|
+
manifestPath,
|
|
1791
|
+
toJson({
|
|
1792
|
+
...manifest,
|
|
1793
|
+
main: "dist/main.js",
|
|
1794
|
+
dependencies: { ...dependencies, "@azure/functions": `^${azureFunctionsVersion}` }
|
|
1795
|
+
})
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
function nodeFunctionAppPackageTarget(name) {
|
|
1799
|
+
const zip = `dist/drop/node-function-app-${name}.zip`;
|
|
1800
|
+
const root = `apps/${name}`;
|
|
1801
|
+
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}')"`;
|
|
1802
|
+
return {
|
|
1803
|
+
executor: "nx:run-commands",
|
|
1804
|
+
dependsOn: ["build"],
|
|
1805
|
+
outputs: [`{workspaceRoot}/${zip}`],
|
|
1806
|
+
options: { command }
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
function nodeFunctionAppStartTarget(name) {
|
|
1810
|
+
return {
|
|
1811
|
+
executor: "nx:run-commands",
|
|
1812
|
+
dependsOn: ["build"],
|
|
1813
|
+
continuous: true,
|
|
1814
|
+
options: { command: "func start", cwd: `apps/${name}` }
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
function addNodeFunctionApp(workspaceRoot, name, stack) {
|
|
1818
|
+
runNodeApp(workspaceRoot, name, stack, "none");
|
|
1819
|
+
ensureAzureFunctionsPackage(workspaceRoot);
|
|
1820
|
+
const nodeFunctionAppRoot = (0, import_node_path6.join)(workspaceRoot, "apps", name);
|
|
1821
|
+
writeFileEnsured((0, import_node_path6.join)(nodeFunctionAppRoot, "src/main.ts"), NODE_FUNCTION_APP_MAIN);
|
|
1822
|
+
writeFileEnsured((0, import_node_path6.join)(nodeFunctionAppRoot, "src/functions/hello.ts"), NODE_FUNCTION_APP_HELLO);
|
|
1823
|
+
writeFileEnsured(
|
|
1824
|
+
(0, import_node_path6.join)(nodeFunctionAppRoot, "src/functions/greeting.ts"),
|
|
1825
|
+
NODE_FUNCTION_APP_GREETING
|
|
1826
|
+
);
|
|
1827
|
+
writeFileEnsured(
|
|
1828
|
+
(0, import_node_path6.join)(nodeFunctionAppRoot, "src/functions/greeting.spec.ts"),
|
|
1829
|
+
NODE_FUNCTION_APP_GREETING_SPEC
|
|
1830
|
+
);
|
|
1831
|
+
writeFileEnsured((0, import_node_path6.join)(nodeFunctionAppRoot, "host.json"), NODE_FUNCTION_APP_HOST_JSON);
|
|
1832
|
+
repairNodeFunctionAppManifest(nodeFunctionAppRoot, workspaceRoot);
|
|
1833
|
+
ensureAdmZip(workspaceRoot);
|
|
1834
|
+
addNxTargets((0, import_node_path6.join)(nodeFunctionAppRoot, "package.json"), {
|
|
1835
|
+
package: nodeFunctionAppPackageTarget(name),
|
|
1836
|
+
start: nodeFunctionAppStartTarget(name)
|
|
1837
|
+
});
|
|
1838
|
+
removeGeneratedEslintConfig(workspaceRoot, `apps/${name}`);
|
|
1839
|
+
registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
|
|
1840
|
+
}
|
|
2164
1841
|
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
name: Push release tags (nx release's own push never runs without a remote Release configured)
|
|
2189
|
-
if: \${{ ${onMain} }}
|
|
2190
|
-
`}`;
|
|
1842
|
+
// src/commands/add/npmLib.ts
|
|
1843
|
+
var import_node_path7 = require("path");
|
|
1844
|
+
async function addNpmLib(workspaceRoot, name, options, kindProvided, stack) {
|
|
1845
|
+
const scope = options.scope ?? (kindProvided ? defaultScope(workspaceRoot) : await promptText("npm scope for the published package", defaultScope(workspaceRoot)));
|
|
1846
|
+
runNx(
|
|
1847
|
+
[
|
|
1848
|
+
"g",
|
|
1849
|
+
"@nx/js:lib",
|
|
1850
|
+
`packages/${name}`,
|
|
1851
|
+
"--publishable",
|
|
1852
|
+
`--importPath=${scope}/${name}`,
|
|
1853
|
+
"--bundler=rollup",
|
|
1854
|
+
`--unitTestRunner=${stack.testRunner}`,
|
|
1855
|
+
"--linter=none",
|
|
1856
|
+
"--no-interactive"
|
|
1857
|
+
],
|
|
1858
|
+
workspaceRoot
|
|
1859
|
+
);
|
|
1860
|
+
const manifestPath = (0, import_node_path7.join)(workspaceRoot, "packages", name, "package.json");
|
|
1861
|
+
markPublic(manifestPath);
|
|
1862
|
+
repairPublishableManifest(manifestPath);
|
|
1863
|
+
removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
|
|
1864
|
+
registerProjectCommands(workspaceRoot, name, { build: true });
|
|
2191
1865
|
}
|
|
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
1866
|
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
1867
|
+
// src/commands/add/python.ts
|
|
1868
|
+
var import_node_fs5 = require("fs");
|
|
1869
|
+
var import_node_path8 = require("path");
|
|
1870
|
+
function ensurePython(workspaceRoot) {
|
|
1871
|
+
const hasPython = runShell("python3", ["--version"], workspaceRoot) === 0 || runShell("python", ["--version"], workspaceRoot) === 0;
|
|
1872
|
+
if (!hasPython) {
|
|
1873
|
+
throw new Error(
|
|
1874
|
+
"Python not found. Install Python 3.9+ first: https://www.python.org/downloads/"
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
function pythonPipPluginSpec() {
|
|
1879
|
+
return process.env.MNCI2_PYTHON_PIP_SPEC ?? "@mnci/nx-python-pip";
|
|
1880
|
+
}
|
|
1881
|
+
function ensurePythonPipPlugin(workspaceRoot) {
|
|
1882
|
+
if (hasPlugin(workspaceRoot, "@mnci/nx-python-pip")) {
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
const spec = pythonPipPluginSpec();
|
|
1886
|
+
logger.step(`Installing the Python toolchain plugin (${spec})`);
|
|
1887
|
+
if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
|
|
1888
|
+
throw new Error("npm install of @mnci/nx-python-pip failed");
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
var PYTHON_REQUIREMENTS_DEV = `build
|
|
1892
|
+
twine
|
|
1893
|
+
ruff
|
|
1894
|
+
pytest
|
|
1895
|
+
pip-audit
|
|
1896
|
+
`;
|
|
1897
|
+
function ensureRequirementsDev(workspaceRoot) {
|
|
1898
|
+
const requirementsDevPath = (0, import_node_path8.join)(workspaceRoot, "requirements-dev.txt");
|
|
1899
|
+
if (!fileExists(requirementsDevPath)) {
|
|
1900
|
+
writeFileEnsured(requirementsDevPath, PYTHON_REQUIREMENTS_DEV);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
function pythonModuleDirectory(name) {
|
|
1904
|
+
return name.replaceAll("-", "_");
|
|
1905
|
+
}
|
|
1906
|
+
function addProjectJsonTargets3(projectJsonPath, newTargets) {
|
|
1907
|
+
const project = readJson(projectJsonPath);
|
|
1908
|
+
const targets = project.targets ?? {};
|
|
1909
|
+
writeFileEnsured(projectJsonPath, toJson({ ...project, targets: { ...targets, ...newTargets } }));
|
|
1910
|
+
}
|
|
1911
|
+
function pythonAppMain(name) {
|
|
1912
|
+
return `def main() -> None:
|
|
1913
|
+
print('${name} is running')
|
|
2206
1914
|
|
|
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
1915
|
|
|
2218
|
-
|
|
2219
|
-
|
|
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
|
|
1916
|
+
if __name__ == '__main__':
|
|
1917
|
+
main()
|
|
2229
1918
|
`;
|
|
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
1919
|
}
|
|
2237
|
-
function
|
|
2238
|
-
|
|
1920
|
+
function pythonAppStartTarget(name) {
|
|
1921
|
+
return {
|
|
1922
|
+
executor: "nx:run-commands",
|
|
1923
|
+
continuous: true,
|
|
1924
|
+
options: { command: "python3 main.py", cwd: `apps/${name}` }
|
|
1925
|
+
};
|
|
2239
1926
|
}
|
|
2240
|
-
function
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
}
|
|
1927
|
+
function pythonFunctionAppStartTarget(name) {
|
|
1928
|
+
return {
|
|
1929
|
+
executor: "nx:run-commands",
|
|
1930
|
+
continuous: true,
|
|
1931
|
+
options: { command: "func start", cwd: `apps/${name}` }
|
|
1932
|
+
};
|
|
2247
1933
|
}
|
|
2248
|
-
function
|
|
2249
|
-
|
|
2250
|
-
const
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
1934
|
+
function pythonAppPackageTarget(name) {
|
|
1935
|
+
const zip = `dist/drop/python-app-${name}.zip`;
|
|
1936
|
+
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}')"`;
|
|
1937
|
+
return {
|
|
1938
|
+
executor: "nx:run-commands",
|
|
1939
|
+
dependsOn: ["build"],
|
|
1940
|
+
outputs: [`{workspaceRoot}/${zip}`],
|
|
1941
|
+
options: { command }
|
|
2255
1942
|
};
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
const
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
...manifest.scripts,
|
|
2266
|
-
...rootScripts()
|
|
1943
|
+
}
|
|
1944
|
+
function pythonFunctionAppPackageTarget(name, moduleDirectory) {
|
|
1945
|
+
const zip = `dist/drop/python-function-app-${name}.zip`;
|
|
1946
|
+
const root = `apps/${name}`;
|
|
1947
|
+
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}')"`;
|
|
1948
|
+
return {
|
|
1949
|
+
executor: "nx:run-commands",
|
|
1950
|
+
outputs: [`{workspaceRoot}/${zip}`],
|
|
1951
|
+
options: { command }
|
|
2267
1952
|
};
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
1953
|
+
}
|
|
1954
|
+
function addPythonApp(workspaceRoot, name) {
|
|
1955
|
+
ensurePython(workspaceRoot);
|
|
1956
|
+
ensurePythonPipPlugin(workspaceRoot);
|
|
1957
|
+
ensureRequirementsDev(workspaceRoot);
|
|
1958
|
+
ensureAdmZip(workspaceRoot);
|
|
1959
|
+
runNx(
|
|
1960
|
+
["g", "@mnci/nx-python-pip:application", name, `--directory=apps/${name}`, "--no-interactive"],
|
|
1961
|
+
workspaceRoot
|
|
1962
|
+
);
|
|
1963
|
+
writeFileEnsured((0, import_node_path8.join)(workspaceRoot, "apps", name, "main.py"), pythonAppMain(name));
|
|
1964
|
+
addProjectJsonTargets3((0, import_node_path8.join)(workspaceRoot, "apps", name, "project.json"), {
|
|
1965
|
+
package: pythonAppPackageTarget(name),
|
|
1966
|
+
start: pythonAppStartTarget(name)
|
|
2274
1967
|
});
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
toJson({
|
|
2292
|
-
...manifest,
|
|
2293
|
-
name: `${options.scope}/source`,
|
|
2294
|
-
scripts,
|
|
2295
|
-
devDependencies: devDeps,
|
|
2296
|
-
overrides,
|
|
2297
|
-
nx
|
|
2298
|
-
})
|
|
1968
|
+
registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:start` });
|
|
1969
|
+
}
|
|
1970
|
+
function addPythonFunctionApp(workspaceRoot, name) {
|
|
1971
|
+
ensurePython(workspaceRoot);
|
|
1972
|
+
ensurePythonPipPlugin(workspaceRoot);
|
|
1973
|
+
ensureRequirementsDev(workspaceRoot);
|
|
1974
|
+
ensureAdmZip(workspaceRoot);
|
|
1975
|
+
runNx(
|
|
1976
|
+
[
|
|
1977
|
+
"g",
|
|
1978
|
+
"@mnci/nx-python-pip:function-application",
|
|
1979
|
+
name,
|
|
1980
|
+
`--directory=apps/${name}`,
|
|
1981
|
+
"--no-interactive"
|
|
1982
|
+
],
|
|
1983
|
+
workspaceRoot
|
|
2299
1984
|
);
|
|
2300
|
-
|
|
2301
|
-
|
|
1985
|
+
const moduleDirectory = pythonModuleDirectory(name);
|
|
1986
|
+
addProjectJsonTargets3((0, import_node_path8.join)(workspaceRoot, "apps", name, "project.json"), {
|
|
1987
|
+
package: pythonFunctionAppPackageTarget(name, moduleDirectory),
|
|
1988
|
+
start: pythonFunctionAppStartTarget(name)
|
|
1989
|
+
});
|
|
1990
|
+
registerProjectCommands(workspaceRoot, name, { build: false, start: `nx run ${name}:start` });
|
|
1991
|
+
}
|
|
1992
|
+
function addPythonLib(workspaceRoot, name) {
|
|
1993
|
+
ensurePython(workspaceRoot);
|
|
1994
|
+
ensurePythonPipPlugin(workspaceRoot);
|
|
1995
|
+
ensureRequirementsDev(workspaceRoot);
|
|
1996
|
+
runNx(
|
|
1997
|
+
[
|
|
1998
|
+
"g",
|
|
1999
|
+
"@mnci/nx-python-pip:library",
|
|
2000
|
+
name,
|
|
2001
|
+
`--directory=python-packages/${name}`,
|
|
2002
|
+
"--no-interactive"
|
|
2003
|
+
],
|
|
2004
|
+
workspaceRoot
|
|
2302
2005
|
);
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2006
|
+
registerProjectCommands(workspaceRoot, name, { build: true });
|
|
2007
|
+
}
|
|
2008
|
+
function addPythonInternalLib(workspaceRoot, name) {
|
|
2009
|
+
ensurePython(workspaceRoot);
|
|
2010
|
+
ensurePythonPipPlugin(workspaceRoot);
|
|
2011
|
+
ensureRequirementsDev(workspaceRoot);
|
|
2012
|
+
runNx(
|
|
2013
|
+
[
|
|
2014
|
+
"g",
|
|
2015
|
+
"@mnci/nx-python-pip:internal-library",
|
|
2016
|
+
name,
|
|
2017
|
+
`--directory=libs/${name}`,
|
|
2018
|
+
"--no-interactive"
|
|
2019
|
+
],
|
|
2020
|
+
workspaceRoot
|
|
2021
|
+
);
|
|
2022
|
+
registerProjectCommands(workspaceRoot, name, { build: false });
|
|
2023
|
+
}
|
|
2024
|
+
var PYTHON_PROJECT_DIRS = ["apps", "python-packages", "libs"];
|
|
2025
|
+
function findPythonProjectDirectory(workspaceRoot, name) {
|
|
2026
|
+
return PYTHON_PROJECT_DIRS.find(
|
|
2027
|
+
(directory) => fileExists((0, import_node_path8.join)(workspaceRoot, directory, name, "pyproject.toml"))
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
function parseVendorEntries(pyprojectToml) {
|
|
2031
|
+
const match = /^\s*vendor\s*=\s*\[([^\]]*)\]/m.exec(pyprojectToml);
|
|
2032
|
+
if (!match) {
|
|
2033
|
+
return [];
|
|
2034
|
+
}
|
|
2035
|
+
return match[1].split(",").map((entry) => entry.trim().replaceAll(/^["']|["']$/g, "")).filter(Boolean);
|
|
2036
|
+
}
|
|
2037
|
+
function addVendorEntry(pyprojectToml, lib) {
|
|
2038
|
+
const existing = parseVendorEntries(pyprojectToml);
|
|
2039
|
+
if (existing.length > 0) {
|
|
2040
|
+
const merged = [...existing, lib].map((name) => `"${name}"`).join(", ");
|
|
2041
|
+
return pyprojectToml.replace(
|
|
2042
|
+
/^(\s*vendor\s*=\s*)\[[^\]]*\]/m,
|
|
2043
|
+
(_match, prefix) => `${prefix}[${merged}]`
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
const table = `[tool.mnci-python-pip]
|
|
2047
|
+
vendor = ["${lib}"]
|
|
2048
|
+
|
|
2049
|
+
`;
|
|
2050
|
+
return pyprojectToml.includes("[tool.pytest.ini_options]") ? pyprojectToml.replace("[tool.pytest.ini_options]", () => `${table}[tool.pytest.ini_options]`) : `${pyprojectToml}
|
|
2051
|
+
${table}`;
|
|
2052
|
+
}
|
|
2053
|
+
async function addPythonVendor(workspaceRoot, consumer, options, kindProvided) {
|
|
2054
|
+
const lib = options.lib ?? (kindProvided ? void 0 : await promptText("Internal Python library to vendor (an existing libs/<name>)"));
|
|
2055
|
+
if (!lib) {
|
|
2056
|
+
throw new Error(
|
|
2057
|
+
"`add python-vendor` needs the library to vendor: pass --lib <name> (an existing libs/<name> internal library)."
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
const consumerDirectory = findPythonProjectDirectory(workspaceRoot, consumer);
|
|
2061
|
+
if (!consumerDirectory) {
|
|
2062
|
+
throw new Error(
|
|
2063
|
+
`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.`
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
if (!fileExists((0, import_node_path8.join)(workspaceRoot, "libs", lib, "pyproject.toml"))) {
|
|
2067
|
+
throw new Error(
|
|
2068
|
+
`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.`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
if (consumerDirectory === "libs" && consumer === lib) {
|
|
2072
|
+
throw new Error(`'${consumer}' cannot vendor itself.`);
|
|
2073
|
+
}
|
|
2074
|
+
const pyprojectPath = (0, import_node_path8.join)(workspaceRoot, consumerDirectory, consumer, "pyproject.toml");
|
|
2075
|
+
const pyprojectToml = (0, import_node_fs5.readFileSync)(pyprojectPath, "utf8");
|
|
2076
|
+
if (parseVendorEntries(pyprojectToml).includes(lib)) {
|
|
2077
|
+
logger.info(`'${consumer}' already vendors '${lib}' \u2014 nothing to do.`);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
writeFileEnsured(pyprojectPath, addVendorEntry(pyprojectToml, lib));
|
|
2081
|
+
logger.success(
|
|
2082
|
+
`'${consumer}' now vendors '${lib}' \u2014 its wheel will include '${lib}''s module on the next build.`
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// src/commands/add/reactApp.ts
|
|
2087
|
+
var import_node_fs6 = require("fs");
|
|
2088
|
+
var import_node_path9 = require("path");
|
|
2089
|
+
var REACT_ENVIRONMENTS = ["dev", "uat", "prod"];
|
|
2090
|
+
function reactEnvironmentFile(environment) {
|
|
2091
|
+
return `# Vite compiles VITE_-prefixed vars into the '${environment}' bundle at build time.
|
|
2092
|
+
# They ship to the browser, so keep only PUBLIC config here \u2014 never secrets.
|
|
2093
|
+
VITE_ENVIRONMENT=${environment}
|
|
2094
|
+
VITE_API_URL=https://api.${environment}.example.com
|
|
2095
|
+
`;
|
|
2096
|
+
}
|
|
2097
|
+
function reactAppTargets(name) {
|
|
2098
|
+
const targets = {};
|
|
2099
|
+
for (const environment of REACT_ENVIRONMENTS) {
|
|
2100
|
+
targets[`build-${environment}`] = {
|
|
2101
|
+
executor: "nx:run-commands",
|
|
2102
|
+
outputs: [`{workspaceRoot}/apps/${name}/dist-${environment}`],
|
|
2103
|
+
options: {
|
|
2104
|
+
command: `vite build --mode ${environment} --outDir dist-${environment}`,
|
|
2105
|
+
cwd: `apps/${name}`
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
const zipStatements = REACT_ENVIRONMENTS.map(
|
|
2110
|
+
(environment) => `z=new A();z.addLocalFolder('apps/${name}/dist-${environment}');z.writeZip('dist/drop/react-app-${name}-${environment}.zip')`
|
|
2111
|
+
).join(";");
|
|
2112
|
+
targets.package = {
|
|
2113
|
+
executor: "nx:run-commands",
|
|
2114
|
+
dependsOn: REACT_ENVIRONMENTS.map((environment) => `build-${environment}`),
|
|
2115
|
+
outputs: REACT_ENVIRONMENTS.map(
|
|
2116
|
+
(environment) => `{workspaceRoot}/dist/drop/react-app-${name}-${environment}.zip`
|
|
2117
|
+
),
|
|
2118
|
+
options: {
|
|
2119
|
+
command: `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const A=require('adm-zip');let z;${zipStatements}"`
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
return targets;
|
|
2123
|
+
}
|
|
2124
|
+
function allowEnvFiles(workspaceRoot) {
|
|
2125
|
+
const gitignorePath = (0, import_node_path9.join)(workspaceRoot, ".gitignore");
|
|
2126
|
+
if (!fileExists(gitignorePath)) {
|
|
2127
|
+
return;
|
|
2313
2128
|
}
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2129
|
+
const marker = "# MoNecromanCI: keep the committed per-environment Vite config (public VITE_ values)";
|
|
2130
|
+
const current = (0, import_node_fs6.readFileSync)(gitignorePath, "utf8");
|
|
2131
|
+
if (current.includes(marker)) {
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
const allow = REACT_ENVIRONMENTS.map((environment) => `!apps/*/.env.${environment}`).join("\n");
|
|
2135
|
+
writeFileEnsured(gitignorePath, `${current.replace(/\n*$/, "\n")}
|
|
2136
|
+
${marker}
|
|
2137
|
+
${allow}
|
|
2138
|
+
`);
|
|
2139
|
+
}
|
|
2140
|
+
function addReactApp(workspaceRoot, name, stack) {
|
|
2141
|
+
ensurePlugin(workspaceRoot, "@nx/react");
|
|
2142
|
+
runNx(
|
|
2143
|
+
[
|
|
2144
|
+
"g",
|
|
2145
|
+
"@nx/react:app",
|
|
2146
|
+
`apps/${name}`,
|
|
2147
|
+
"--bundler=vite",
|
|
2148
|
+
`--unitTestRunner=${stack.testRunner}`,
|
|
2149
|
+
"--linter=none",
|
|
2150
|
+
"--style=css",
|
|
2151
|
+
"--e2eTestRunner=none",
|
|
2152
|
+
"--no-interactive"
|
|
2153
|
+
],
|
|
2154
|
+
workspaceRoot
|
|
2318
2155
|
);
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2156
|
+
ensureAdmZip(workspaceRoot);
|
|
2157
|
+
const reactAppRoot = (0, import_node_path9.join)(workspaceRoot, "apps", name);
|
|
2158
|
+
for (const environment of REACT_ENVIRONMENTS) {
|
|
2159
|
+
writeFileEnsured((0, import_node_path9.join)(reactAppRoot, `.env.${environment}`), reactEnvironmentFile(environment));
|
|
2160
|
+
}
|
|
2161
|
+
allowEnvFiles(workspaceRoot);
|
|
2162
|
+
addNxTargets((0, import_node_path9.join)(reactAppRoot, "package.json"), reactAppTargets(name));
|
|
2163
|
+
removeGeneratedEslintConfig(workspaceRoot, `apps/${name}`);
|
|
2164
|
+
registerProjectCommands(workspaceRoot, name, { build: true, start: `nx run ${name}:serve` });
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// src/commands/add/reactLib.ts
|
|
2168
|
+
var import_node_path10 = require("path");
|
|
2169
|
+
var REACT_LIB_BUNDLER = "rollup";
|
|
2170
|
+
async function addReactLib(workspaceRoot, name, options, kindProvided, stack) {
|
|
2171
|
+
ensurePlugin(workspaceRoot, "@nx/react");
|
|
2172
|
+
const scope = options.scope ?? (kindProvided ? defaultScope(workspaceRoot) : await promptText("npm scope for the published package", defaultScope(workspaceRoot)));
|
|
2173
|
+
runNx(
|
|
2174
|
+
[
|
|
2175
|
+
"g",
|
|
2176
|
+
"@nx/react:library",
|
|
2177
|
+
`packages/${name}`,
|
|
2178
|
+
`--name=${name}`,
|
|
2179
|
+
"--publishable",
|
|
2180
|
+
`--importPath=${scope}/${name}`,
|
|
2181
|
+
`--bundler=${REACT_LIB_BUNDLER}`,
|
|
2182
|
+
`--unitTestRunner=${stack.testRunner}`,
|
|
2183
|
+
"--linter=none",
|
|
2184
|
+
"--no-interactive"
|
|
2185
|
+
],
|
|
2186
|
+
workspaceRoot
|
|
2322
2187
|
);
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2188
|
+
const publishableManifest = (0, import_node_path10.join)(workspaceRoot, "packages", name, "package.json");
|
|
2189
|
+
markPublic(publishableManifest);
|
|
2190
|
+
repairPublishableManifest(publishableManifest);
|
|
2191
|
+
removeGeneratedEslintConfig(workspaceRoot, `packages/${name}`);
|
|
2192
|
+
registerProjectCommands(workspaceRoot, name, { build: true });
|
|
2193
|
+
}
|
|
2194
|
+
function addReactInternalLib(workspaceRoot, name, stack) {
|
|
2195
|
+
ensurePlugin(workspaceRoot, "@nx/react");
|
|
2196
|
+
runNx(
|
|
2197
|
+
[
|
|
2198
|
+
"g",
|
|
2199
|
+
"@nx/react:library",
|
|
2200
|
+
`libs/${name}`,
|
|
2201
|
+
`--name=${name}`,
|
|
2202
|
+
`--bundler=${REACT_LIB_BUNDLER}`,
|
|
2203
|
+
`--unitTestRunner=${stack.testRunner}`,
|
|
2204
|
+
"--linter=none",
|
|
2205
|
+
"--no-interactive"
|
|
2206
|
+
],
|
|
2207
|
+
workspaceRoot
|
|
2333
2208
|
);
|
|
2334
|
-
|
|
2335
|
-
|
|
2209
|
+
const privateManifest = (0, import_node_path10.join)(workspaceRoot, "libs", name, "package.json");
|
|
2210
|
+
markPrivate(privateManifest);
|
|
2211
|
+
repairPublishableManifest(privateManifest);
|
|
2212
|
+
removeGeneratedEslintConfig(workspaceRoot, `libs/${name}`);
|
|
2213
|
+
registerProjectCommands(workspaceRoot, name, { build: true });
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// src/commands/add.ts
|
|
2217
|
+
var PROJECT_KINDS = [
|
|
2218
|
+
"react-app",
|
|
2219
|
+
"react-lib",
|
|
2220
|
+
"react-internal-lib",
|
|
2221
|
+
"node-app",
|
|
2222
|
+
"node-function-app",
|
|
2223
|
+
"npm-lib",
|
|
2224
|
+
"internal-lib",
|
|
2225
|
+
"python-app",
|
|
2226
|
+
"python-function-app",
|
|
2227
|
+
"python-lib",
|
|
2228
|
+
"python-internal-lib",
|
|
2229
|
+
"python-vendor",
|
|
2230
|
+
"go-app",
|
|
2231
|
+
"go-function-app",
|
|
2232
|
+
"go-lib",
|
|
2233
|
+
"go-internal-lib",
|
|
2234
|
+
"flutter-app",
|
|
2235
|
+
"flutter-lib",
|
|
2236
|
+
"flutter-internal-lib"
|
|
2237
|
+
];
|
|
2238
|
+
async function runAdd(kind, name, options) {
|
|
2239
|
+
const workspaceRoot = process.cwd();
|
|
2240
|
+
if (!fileExists((0, import_node_path11.join)(workspaceRoot, "nx.json"))) {
|
|
2241
|
+
throw new Error("No nx.json found here. Run `add` from the workspace root.");
|
|
2336
2242
|
}
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2243
|
+
const stack = readWorkspaceStack(workspaceRoot);
|
|
2244
|
+
const kindProvided = kind !== void 0;
|
|
2245
|
+
const resolvedKind = kind ?? await (0, import_prompts5.select)({
|
|
2246
|
+
message: "What kind of project?",
|
|
2247
|
+
choices: PROJECT_KINDS.map((value) => ({ name: value, value }))
|
|
2248
|
+
});
|
|
2249
|
+
const resolvedName = name ?? await promptText("Project name");
|
|
2250
|
+
assertValidProjectName(resolvedName, "Project name");
|
|
2251
|
+
switch (resolvedKind) {
|
|
2252
|
+
case "react-app": {
|
|
2253
|
+
addReactApp(workspaceRoot, resolvedName, stack);
|
|
2254
|
+
break;
|
|
2255
|
+
}
|
|
2256
|
+
case "react-lib": {
|
|
2257
|
+
await addReactLib(workspaceRoot, resolvedName, options, kindProvided, stack);
|
|
2258
|
+
break;
|
|
2259
|
+
}
|
|
2260
|
+
case "react-internal-lib": {
|
|
2261
|
+
addReactInternalLib(workspaceRoot, resolvedName, stack);
|
|
2262
|
+
break;
|
|
2263
|
+
}
|
|
2264
|
+
case "node-app": {
|
|
2265
|
+
addNodeApp(workspaceRoot, resolvedName, stack, options.framework);
|
|
2266
|
+
break;
|
|
2267
|
+
}
|
|
2268
|
+
case "node-function-app": {
|
|
2269
|
+
addNodeFunctionApp(workspaceRoot, resolvedName, stack);
|
|
2270
|
+
break;
|
|
2271
|
+
}
|
|
2272
|
+
case "npm-lib": {
|
|
2273
|
+
await addNpmLib(workspaceRoot, resolvedName, options, kindProvided, stack);
|
|
2274
|
+
break;
|
|
2275
|
+
}
|
|
2276
|
+
case "internal-lib": {
|
|
2277
|
+
runNx(
|
|
2278
|
+
[
|
|
2279
|
+
"g",
|
|
2280
|
+
"@nx/js:lib",
|
|
2281
|
+
`libs/${resolvedName}`,
|
|
2282
|
+
"--bundler=tsc",
|
|
2283
|
+
`--unitTestRunner=${stack.testRunner}`,
|
|
2284
|
+
"--linter=none",
|
|
2285
|
+
"--no-interactive"
|
|
2286
|
+
],
|
|
2287
|
+
workspaceRoot
|
|
2288
|
+
);
|
|
2289
|
+
markPrivate((0, import_node_path11.join)(workspaceRoot, "libs", resolvedName, "package.json"));
|
|
2290
|
+
removeGeneratedEslintConfig(workspaceRoot, `libs/${resolvedName}`);
|
|
2291
|
+
registerProjectCommands(workspaceRoot, resolvedName, { build: true });
|
|
2292
|
+
break;
|
|
2293
|
+
}
|
|
2294
|
+
case "python-app": {
|
|
2295
|
+
addPythonApp(workspaceRoot, resolvedName);
|
|
2296
|
+
break;
|
|
2297
|
+
}
|
|
2298
|
+
case "python-function-app": {
|
|
2299
|
+
addPythonFunctionApp(workspaceRoot, resolvedName);
|
|
2300
|
+
break;
|
|
2301
|
+
}
|
|
2302
|
+
case "python-lib": {
|
|
2303
|
+
addPythonLib(workspaceRoot, resolvedName);
|
|
2304
|
+
break;
|
|
2305
|
+
}
|
|
2306
|
+
case "python-internal-lib": {
|
|
2307
|
+
addPythonInternalLib(workspaceRoot, resolvedName);
|
|
2308
|
+
break;
|
|
2309
|
+
}
|
|
2310
|
+
case "go-app": {
|
|
2311
|
+
addGoApp(workspaceRoot, resolvedName);
|
|
2312
|
+
break;
|
|
2313
|
+
}
|
|
2314
|
+
case "go-function-app": {
|
|
2315
|
+
addGoFunctionApp(workspaceRoot, resolvedName);
|
|
2316
|
+
break;
|
|
2317
|
+
}
|
|
2318
|
+
case "go-lib": {
|
|
2319
|
+
addGoLib(workspaceRoot, resolvedName);
|
|
2320
|
+
break;
|
|
2321
|
+
}
|
|
2322
|
+
case "go-internal-lib": {
|
|
2323
|
+
addGoInternalLib(workspaceRoot, resolvedName);
|
|
2324
|
+
break;
|
|
2325
|
+
}
|
|
2326
|
+
case "flutter-app": {
|
|
2327
|
+
addFlutterApp(workspaceRoot, resolvedName);
|
|
2328
|
+
break;
|
|
2329
|
+
}
|
|
2330
|
+
case "flutter-lib": {
|
|
2331
|
+
addFlutterLib(workspaceRoot, resolvedName);
|
|
2332
|
+
break;
|
|
2333
|
+
}
|
|
2334
|
+
case "flutter-internal-lib": {
|
|
2335
|
+
addFlutterInternalLib(workspaceRoot, resolvedName);
|
|
2336
|
+
break;
|
|
2337
|
+
}
|
|
2338
|
+
case "python-vendor": {
|
|
2339
|
+
await addPythonVendor(workspaceRoot, resolvedName, options, kindProvided);
|
|
2340
|
+
syncProjectReferences(workspaceRoot);
|
|
2341
|
+
runFormatter(workspaceRoot);
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
default: {
|
|
2345
|
+
const exhaustive = resolvedKind;
|
|
2346
|
+
throw new Error(
|
|
2347
|
+
`Unknown project kind '${exhaustive}'. Expected one of: ${PROJECT_KINDS.join(", ")}.`
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2344
2350
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2351
|
+
syncProjectReferences(workspaceRoot);
|
|
2352
|
+
runFormatter(workspaceRoot);
|
|
2353
|
+
logger.success(`Added ${resolvedKind} '${resolvedName}'.`);
|
|
2354
|
+
}
|
|
2355
|
+
function syncProjectReferences(workspaceRoot) {
|
|
2356
|
+
logger.step("Syncing TypeScript project references (nx sync)");
|
|
2357
|
+
if (runShell("npx", ["nx", "sync"], workspaceRoot) !== 0) {
|
|
2358
|
+
logger.warn(
|
|
2359
|
+
"nx sync did not complete \u2014 run `npx nx sync` yourself so cross-project imports resolve in your editor."
|
|
2350
2360
|
);
|
|
2351
|
-
writeFileEnsured((0, import_node_path11.join)(workspaceRoot, ".github/dependabot.yml"), DEPENDABOT_CONFIG);
|
|
2352
2361
|
}
|
|
2353
2362
|
}
|
|
2363
|
+
function readWorkspaceStack(workspaceRoot) {
|
|
2364
|
+
const nxJson = readJson(
|
|
2365
|
+
(0, import_node_path11.join)(workspaceRoot, "nx.json")
|
|
2366
|
+
);
|
|
2367
|
+
const stack = nxJson.mnci?.stack;
|
|
2368
|
+
return {
|
|
2369
|
+
testRunner: stack?.testRunner === "vitest" ? "vitest" : "jest"
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2354
2372
|
|
|
2355
2373
|
// src/commands/doctor.ts
|
|
2374
|
+
var import_node_fs7 = require("fs");
|
|
2375
|
+
var import_node_path12 = require("path");
|
|
2356
2376
|
var SUPPORTED_ESLINT_MAJOR = ESLINT_VERSION.replace(/^\D*/, "").split(".", 1)[0];
|
|
2357
2377
|
function toPosix(path) {
|
|
2358
2378
|
return path.replaceAll("\\", "/");
|