monecromanci 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # MoNecromanCI
2
+
3
+ > **MO**no(repo) + **NECROMAN**cy + **CI**. The CLI command is `monecromanci` (short alias `mnci`).
4
+
5
+ An interactive CLI that **summons**, **conjures**, **raises** and **validates** NX
6
+ monorepos — Node + TypeScript, Jest, ESLint, real VSCode `.ts` debugging, and a
7
+ complete CI pipeline (Azure DevOps **and/or** GitHub Actions), with near-zero
8
+ per-project configuration.
9
+
10
+ It generates nine project kinds and keeps every repo's tool-owned config in sync:
11
+
12
+ | Kind | What you get |
13
+ | ----------------- | ----------------------------------------------------------------------- |
14
+ | `internal-lib` | Source-resolved (`main → src/index.ts`) so you step into it while debugging and "find references" works across libs. |
15
+ | `publishable-lib` | Published via `nx release`; `dist/package.json` gets **real, resolved dependencies** even though all deps live in the root. |
16
+ | `cli-tool` | A publishable lib that also ships a bundled `bin` (esbuild + shebang). |
17
+ | `function-app` | Azure Functions v4, `.configurations/{dev,uat,prod}.json`, `clean:config` whitespace-strip, attach-debugging. |
18
+ | `node-app` | A framework-agnostic TS HTTP server (node:http) you extend with Express/Koa/Fastify/Nest/…; built, dependency-traced and zipped like a function app. |
19
+ | `react-app` | Vite with `dev`/`uat`/`prod` builds (`dist-dev`/`dist-uat`/`dist-prod`) and browser debugging. |
20
+ | `vue-app` | Vue 3 + Vite, same multi-env builds. |
21
+ | `svelte-app` | Svelte 5 + Vite, same multi-env builds. |
22
+ | `nextjs-app` | Full-stack Next.js (App Router). Per-env builds assemble `dist-<env>` in **server** (standalone) or **static-export** mode (`NEXT_OUTPUT`). |
23
+
24
+ ## Commands
25
+
26
+ Every command keeps its plain name and gains a necromancy-themed alias (in the
27
+ comment below) — use whichever reads better to you:
28
+
29
+ ```sh
30
+ monecromanci new [name] # summon · scaffold a brand-new monorepo (prompts: CI provider, registry, scope, …)
31
+ monecromanci add [type] # conjure · internal-lib | publishable-lib | cli-tool | function-app | node-app | react-app | vue-app | svelte-app | nextjs-app
32
+ monecromanci doctor [--fix] # raise · detect (and with --fix, repair) tool-owned config drift
33
+ monecromanci update # ascend · doctor --fix + re-stamp the template version
34
+ monecromanci validate [--all] # ritual · run lint/test/build locally (nx affected; --all = run-many) before pushing to CI
35
+ ```
36
+
37
+ `new` is fully scriptable: `monecromanci new demo --yes --ci github --registry github-packages --owner acme`.
38
+
39
+ ## CI providers & registry (chosen per repo)
40
+
41
+ `new` prompts for a **CI provider** — `azure`, `github`, or `both` — and a **package
42
+ registry** — Azure Artifacts, GitHub Packages, or public npm (defaulting to match
43
+ the CI). The `.build-templates/*.mjs` are the single engine for **both** providers;
44
+ only a thin wrapper differs: `azure-pipelines.yml` and/or `.github/workflows/ci.yml`.
45
+ The `.npmrc`, each publishable project's `publishConfig`, and the nx-release docs
46
+ are generated to match the registry.
47
+
48
+ ## What's centralised
49
+
50
+ One root `package.json` holds **all** dependencies. The root owns `nx.json`
51
+ (with `nx release`), `tsconfig.base.json`, `tsconfig.jest.json`, a Jest preset
52
+ factory, a **non-type-checked** ESLint flat config (standard/no-semi + @stylistic
53
+ + unicorn + React + Jest + TSDoc + JSON/JSONC/JSON5 + YAML + Markdown), the
54
+ `.code-workspace`, and a vendored CI pipeline. Per-project config is 2–4 tiny files.
55
+
56
+ ## Debugging (works on the TypeScript, including into libs)
57
+
58
+ The `.code-workspace` ships **breakpoint-capable** configs at the workspace top
59
+ level (where VSCode actually reads them): "Debug Jest (current file)"
60
+ (`--runInBand`, `resolveSourceMapLocations: null`, source maps on), a Function/Node
61
+ App attach config (`:9229`), and browser configs for Vite (`:5173`) and Next.js
62
+ (`:3000`) — plus the `Orta.vscode-jest` extension for per-test Debug lenses.
63
+
64
+ ## Developing MoNecromanCI
65
+
66
+ ```sh
67
+ npm install --legacy-peer-deps # ESLint 10 leads some plugins' peer ranges
68
+ npm run build # tsup bundle + copy assets to dist/
69
+ npm run lint
70
+ npm test
71
+ ```
72
+
73
+ ### Verify the generated output end-to-end
74
+
75
+ ```sh
76
+ node dist/cli.js new demo --yes --registry npm --lib helpers
77
+ cd demo
78
+ node ../dist/cli.js add nextjs-app web # or any other kind
79
+ npm install
80
+ npm run lint && npm test && npm run build
81
+ node ../dist/cli.js validate # (ritual) nx affected -t lint test build
82
+ # In VSCode: open demo.code-workspace, set a breakpoint in a *.test.ts, run
83
+ # "Debug Jest (current file)" → it should pause on the breakpoint.
84
+ ```
@@ -0,0 +1,33 @@
1
+ name: monorepo-ci-$(Date:yyyyMMdd)$(Rev:.r)
2
+
3
+ # Generated by MoNecromanCI. Re-sync the build-templates with 'monecromanci doctor'.
4
+ trigger:
5
+ branches:
6
+ include: [dev, development, uat, master, main]
7
+ paths:
8
+ exclude: [docs/**, "**/*.md"]
9
+
10
+ pr:
11
+ branches:
12
+ include: [dev, development, uat, master, main]
13
+ paths:
14
+ exclude: [docs/**, "**/*.md"]
15
+
16
+ pool:
17
+ name: AzurePipelineManagedPool-Windows
18
+ demands:
19
+ - npm
20
+
21
+ variables:
22
+ # Set to an Azure Resource Manager service connection to publish TypeDoc to a
23
+ # Storage blob; leave empty to skip documentation publishing.
24
+ - name: docsAzureSubscription
25
+ value: ""
26
+
27
+ steps:
28
+ - template: .build-templates/01-preparation.yml
29
+ - template: .build-templates/02-quality-control.yml
30
+ - template: .build-templates/03-package-apps.yml
31
+ - template: .build-templates/04-publish-libs.yml
32
+ - template: .build-templates/05-publish-documentation.yml
33
+ - template: .build-templates/06-summary.yml
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Step 01 — Preparation.
5
+ *
6
+ * Resolves the git range, the Nx affected project set, classifies every project
7
+ * from its tags, and writes a reusable context manifest consumed by every later
8
+ * step. Also emits the pipeline variables used for step gating and prints a
9
+ * human-readable execution plan so a run can be understood at a glance.
10
+ *
11
+ * Run locally with `npm run pipeline:plan` (after `npm ci`) to preview the plan
12
+ * without pushing.
13
+ */
14
+
15
+ import process from 'node:process'
16
+ import {
17
+ banner,
18
+ log,
19
+ section,
20
+ setBuildNumber,
21
+ setVariables,
22
+ table,
23
+ writeJson,
24
+ } from './lib/_h.mjs'
25
+ import {
26
+ buildContextManifest,
27
+ CONTEXT_FILE_PATH,
28
+ describeProjectAction,
29
+ describeProjectType,
30
+ enrichProject,
31
+ } from './lib/context.mjs'
32
+ import {
33
+ resolveAffectedProjects,
34
+ resolveAllProjects,
35
+ resolveBaseCommit,
36
+ resolveEffectiveBranchName,
37
+ resolveHeadCommit,
38
+ resolveProjectMetadata,
39
+ } from './lib/nx.mjs'
40
+
41
+ /**
42
+ * Resolves and enriches every project in the workspace.
43
+ *
44
+ * @param {string[]} projectNames All Nx project names.
45
+ * @param {Set<string>} affectedSet The affected project names.
46
+ * @param {string} branchName The effective branch name.
47
+ * @returns {Array<Record<string, any>>} Returns enriched project data.
48
+ */
49
+ function resolvePipelineProjects (projectNames, affectedSet, branchName) {
50
+ const projects = []
51
+
52
+ for (const projectName of [...projectNames].sort((left, right) => left.localeCompare(right))) {
53
+ const metadata = resolveProjectMetadata(projectName)
54
+ if (!metadata) {
55
+ log(`[projects] Skipping ${projectName}: no Nx metadata resolved`)
56
+ continue
57
+ }
58
+
59
+ projects.push(enrichProject(metadata, affectedSet.has(projectName), branchName))
60
+ }
61
+
62
+ return projects
63
+ }
64
+
65
+ /**
66
+ * Resolves the build number from releasable project versions.
67
+ *
68
+ * @param {Record<string, any>} context The context manifest.
69
+ * @returns {string} Returns the build number.
70
+ */
71
+ function resolveBuildNumber (context) {
72
+ const releasable = context.projects
73
+ .filter(project => !project.ignored)
74
+ .filter(project => project.type.functionApp || project.type.reactApp || project.type.externalPackage)
75
+ .filter(project => project.affected && project.version)
76
+
77
+ if (releasable.length === 0) {
78
+ return process.env.BUILD_BUILDNUMBER || 'monorepo-no-affected'
79
+ }
80
+
81
+ return releasable.map(project => `${project.name}_${project.version}`).join('-')
82
+ }
83
+
84
+ /**
85
+ * Builds the pipeline variables emitted for step gating.
86
+ *
87
+ * @param {Record<string, any>} context The context manifest.
88
+ * @returns {{name: string, value: string}[]} Returns the pipeline variables.
89
+ */
90
+ function buildPipelineVariables (context) {
91
+ const variables = [
92
+ { name: 'MONOREPO_CONTEXT_FILE', value: CONTEXT_FILE_PATH },
93
+ { name: 'NX_BASE', value: context.baseCommit },
94
+ { name: 'NX_HEAD', value: context.headCommit },
95
+ { name: 'HAS_AFFECTED', value: String(context.hasAffected) },
96
+ { name: 'AFFECTED_PROJECTS', value: context.affectedProjects.join(',') },
97
+ { name: 'FUNCTION_APPS', value: context.groups.functionApps.join(',') },
98
+ { name: 'NODE_APPS', value: context.groups.nodeApps.join(',') },
99
+ { name: 'REACT_APPS', value: context.groups.reactApps.join(',') },
100
+ { name: 'INTERNAL_PACKAGES', value: context.groups.internalPackages.join(',') },
101
+ { name: 'EXTERNAL_PACKAGES', value: context.groups.externalPackages.join(',') },
102
+ { name: 'IGNORED_PROJECTS', value: context.groups.ignoredProjects.join(',') },
103
+ { name: 'HAS_FUNCTION_APPS', value: String(context.groups.functionApps.length > 0) },
104
+ { name: 'HAS_NODE_APPS', value: String(context.groups.nodeApps.length > 0) },
105
+ { name: 'HAS_REACT_APPS', value: String(context.groups.reactApps.length > 0) },
106
+ { name: 'HAS_INTERNAL_PACKAGES', value: String(context.groups.internalPackages.length > 0) },
107
+ { name: 'HAS_PUBLISHABLE_LIBS', value: String(context.groups.externalPackages.length > 0) },
108
+ ]
109
+
110
+ for (const project of context.projects) {
111
+ variables.push({ name: `HAS_${project.sanitizedName}`, value: String(project.affected && !project.ignored) })
112
+ }
113
+
114
+ return variables
115
+ }
116
+
117
+ /**
118
+ * Prints the execution plan derived from the context manifest.
119
+ *
120
+ * @param {Record<string, any>} context The context manifest.
121
+ */
122
+ function printExecutionPlan (context) {
123
+ section('Execution plan')
124
+
125
+ table(context.projects.map(project => ({
126
+ project: project.name,
127
+ type: describeProjectType(project),
128
+ affected: project.affected && !project.ignored ? 'yes' : 'no',
129
+ version: project.version || 'n/a',
130
+ action: project.affected && !project.ignored ? describeProjectAction(project) : '—',
131
+ })))
132
+ }
133
+
134
+ /**
135
+ * Runs preparation discovery and emits the reusable pipeline context.
136
+ */
137
+ function main () {
138
+ banner('[01] Preparation — resolving monorepo context')
139
+ log(`Platform: ${process.platform}, cwd: ${process.cwd()}`)
140
+
141
+ const branchName = resolveEffectiveBranchName()
142
+ const headCommit = resolveHeadCommit()
143
+ const baseCommit = resolveBaseCommit(headCommit)
144
+ log(`Branch: ${branchName || '(unknown)'}`)
145
+ log(`Range: ${baseCommit} .. ${headCommit}`)
146
+
147
+ const affectedProjects = resolveAffectedProjects(baseCommit, headCommit)
148
+ const affectedSet = new Set(affectedProjects)
149
+ log(`Affected (${affectedProjects.length}): ${affectedProjects.join(', ') || '(none)'}`)
150
+
151
+ const projectNames = resolveAllProjects()
152
+ log(`All projects (${projectNames.length}): ${projectNames.join(', ')}`)
153
+
154
+ const projects = resolvePipelineProjects(projectNames, affectedSet, branchName)
155
+ const context = buildContextManifest({ baseCommit, branchName, headCommit, projects })
156
+
157
+ writeJson(CONTEXT_FILE_PATH, context)
158
+ log(`Context written: ${CONTEXT_FILE_PATH}`)
159
+
160
+ printExecutionPlan(context)
161
+
162
+ const buildNumber = resolveBuildNumber(context)
163
+ setBuildNumber(buildNumber)
164
+ log(`Build number: ${buildNumber}`)
165
+
166
+ section('Pipeline variables')
167
+ setVariables(buildPipelineVariables(context))
168
+
169
+ banner(context.hasAffected
170
+ ? `[01] Preparation complete — ${context.affectedProjects.length} affected project(s)`
171
+ : '[01] Preparation complete — no affected projects, downstream steps will skip')
172
+ }
173
+
174
+ main()
@@ -0,0 +1,51 @@
1
+ steps:
2
+ - checkout: self
3
+ fetchDepth: 0
4
+ persistCredentials: true
5
+
6
+ - task: PowerShell@2
7
+ displayName: "[01] Fetch all refs for affected detection"
8
+ inputs:
9
+ targetType: inline
10
+ script: |
11
+ Write-Host "=== Fetching all refs and tags ===" -ForegroundColor Cyan
12
+ git fetch --all --prune --tags
13
+ Write-Host "=== Recent commits ===" -ForegroundColor Cyan
14
+ git log --oneline -10
15
+
16
+ - task: UseNode@1
17
+ displayName: "[01] Use Node.js 24"
18
+ inputs:
19
+ version: 24.x
20
+
21
+ - task: PowerShell@2
22
+ displayName: "[01] Ensure npm cache directory exists"
23
+ inputs:
24
+ targetType: inline
25
+ script: New-Item -ItemType Directory -Path "$(Pipeline.Workspace)/.npm" -Force | Out-Null
26
+
27
+ - task: Cache@2
28
+ displayName: "[01] Restore npm cache"
29
+ inputs:
30
+ key: npm | "$(Agent.OS)" | $(Build.SourcesDirectory)/package-lock.json
31
+ restoreKeys: |
32
+ npm | "$(Agent.OS)"
33
+ path: $(Pipeline.Workspace)/.npm
34
+
35
+ # Authenticate against the registries declared in the repo's .npmrc. This
36
+ # injects credentials so the plain `npm ci` below works for any feed/registry.
37
+ - task: npmAuthenticate@0
38
+ displayName: "[01] Authenticate npm registry"
39
+ inputs:
40
+ workingFile: .npmrc
41
+
42
+ # Dependencies are installed BEFORE affected detection so Nx computes the
43
+ # project graph with the workspace's pinned Nx version (never a downloaded one).
44
+ - script: npm ci
45
+ displayName: "[01] Install monorepo dependencies"
46
+ workingDirectory: $(Build.SourcesDirectory)
47
+ env:
48
+ HUSKY: 0
49
+
50
+ - script: node .build-templates/01-preparation.mjs
51
+ displayName: "[01] Resolve context, affected projects and execution plan"
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Step 02 — Quality control.
5
+ *
6
+ * Runs lint, test and build for the affected projects over the resolved git
7
+ * range. Kept as a Node step (rather than a repo `qa` npm script) so the
8
+ * template stays self-contained — a consuming repo only needs the standard Nx
9
+ * `lint`/`test`/`build` targets, defined either in `project.json` or via the
10
+ * `@nx/*` inference plugins.
11
+ */
12
+
13
+ import process from 'node:process'
14
+ import { banner, shellEscape } from './lib/_h.mjs'
15
+ import { runNxInherit } from './lib/nx.mjs'
16
+
17
+ /**
18
+ * Runs lint, test and build for the affected project set.
19
+ */
20
+ function main () {
21
+ banner('[02] Quality control — lint, test and build affected projects')
22
+
23
+ const base = process.env.NX_BASE || ''
24
+ const head = process.env.NX_HEAD || ''
25
+ const range = base && head ? ` --base=${shellEscape(base)} --head=${shellEscape(head)}` : ''
26
+
27
+ runNxInherit(`affected -t lint,test,build${range} --outputStyle=static`)
28
+
29
+ banner('[02] Quality control complete')
30
+ }
31
+
32
+ main()
@@ -0,0 +1,24 @@
1
+ steps:
2
+ - script: node .build-templates/02-quality-control.mjs
3
+ displayName: "[02] Run affected projects lint, test and build"
4
+ condition: and(succeeded(), eq(variables['HAS_AFFECTED'], 'true'))
5
+
6
+ - task: PublishTestResults@2
7
+ displayName: "[02] Publish affected projects test results"
8
+ condition: and(succeededOrFailed(), eq(variables['HAS_AFFECTED'], 'true'))
9
+ inputs:
10
+ testResultsFormat: JUnit
11
+ testResultsFiles: "**/coverage/test-results.xml"
12
+ searchFolder: $(Build.SourcesDirectory)
13
+ mergeTestResults: true
14
+ failTaskOnFailedTests: true
15
+ testRunTitle: Affected Tests
16
+
17
+ - task: PublishCodeCoverageResults@2
18
+ displayName: "[02] Publish affected projects coverage results"
19
+ condition: and(succeededOrFailed(), eq(variables['HAS_AFFECTED'], 'true'))
20
+ inputs:
21
+ summaryFileLocation: "$(Build.SourcesDirectory)/**/coverage/cobertura-coverage.xml"
22
+ pathToSources: $(Build.SourcesDirectory)
23
+ reportDirectory: "$(Build.SourcesDirectory)/**/coverage"
24
+ failIfCoverageEmpty: false