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 +84 -0
- package/dist/assets/azure-pipelines.yml +33 -0
- package/dist/assets/build-templates/01-preparation.mjs +174 -0
- package/dist/assets/build-templates/01-preparation.yml +51 -0
- package/dist/assets/build-templates/02-quality-control.mjs +32 -0
- package/dist/assets/build-templates/02-quality-control.yml +24 -0
- package/dist/assets/build-templates/03-package-apps.mjs +573 -0
- package/dist/assets/build-templates/03-package-apps.yml +37 -0
- package/dist/assets/build-templates/04-publish-libs.mjs +155 -0
- package/dist/assets/build-templates/04-publish-libs.yml +20 -0
- package/dist/assets/build-templates/05-publish-documentation.mjs +88 -0
- package/dist/assets/build-templates/05-publish-documentation.yml +20 -0
- package/dist/assets/build-templates/06-summary.mjs +463 -0
- package/dist/assets/build-templates/06-summary.yml +4 -0
- package/dist/assets/build-templates/README.md +69 -0
- package/dist/assets/build-templates/lib/_h.mjs +299 -0
- package/dist/assets/build-templates/lib/context.mjs +359 -0
- package/dist/assets/build-templates/lib/nx.mjs +254 -0
- package/dist/assets/eslint.config.mjs +306 -0
- package/dist/assets/github/workflows/ci.yml +67 -0
- package/dist/assets/scripts/clean-config.mjs +35 -0
- package/dist/assets/scripts/generate-dist-package.mjs +205 -0
- package/dist/assets/scripts/next-build.mjs +37 -0
- package/dist/cli.js +2466 -0
- package/dist/cli.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Nx and git helpers used to resolve the affected project set.
|
|
5
|
+
*
|
|
6
|
+
* Nx is always invoked through the locally installed binary
|
|
7
|
+
* (`node_modules/.bin/nx`) so the workspace's pinned Nx version computes the
|
|
8
|
+
* project graph. `npx --yes` is intentionally avoided because it can download a
|
|
9
|
+
* different Nx version and produces an unreliable graph when run before install.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import process from 'node:process'
|
|
15
|
+
import { isWindows, log, run, runInherit, runSafe, shellEscape, warn } from './_h.mjs'
|
|
16
|
+
|
|
17
|
+
const WORKSPACE_ROOT = process.cwd()
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the locally installed Nx binary path.
|
|
21
|
+
*
|
|
22
|
+
* @returns {string | null} Returns the Nx binary path, or null when missing.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveLocalNxBin () {
|
|
25
|
+
const binaryName = isWindows() ? 'nx.cmd' : 'nx'
|
|
26
|
+
const binaryPath = path.join(WORKSPACE_ROOT, 'node_modules', '.bin', binaryName)
|
|
27
|
+
|
|
28
|
+
return existsSync(binaryPath) ? binaryPath : null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Builds the Nx command prefix, preferring the local binary.
|
|
33
|
+
*
|
|
34
|
+
* @returns {string} Returns the Nx invocation prefix.
|
|
35
|
+
*/
|
|
36
|
+
function getNxCommandPrefix () {
|
|
37
|
+
const localBin = resolveLocalNxBin()
|
|
38
|
+
|
|
39
|
+
if (localBin) {
|
|
40
|
+
return shellEscape(localBin)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
warn('Local Nx binary not found in node_modules/.bin. Falling back to "npx nx". Run "npm ci" first for reliable results.')
|
|
44
|
+
|
|
45
|
+
return 'npx nx'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Runs an Nx command and returns trimmed stdout.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} args The Nx command arguments.
|
|
52
|
+
* @returns {string} Returns trimmed command output.
|
|
53
|
+
*/
|
|
54
|
+
export function runNx (args) {
|
|
55
|
+
return run(`${getNxCommandPrefix()} ${args}`, { cwd: WORKSPACE_ROOT })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Runs an Nx command, returning an empty string on failure.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} args The Nx command arguments.
|
|
62
|
+
* @returns {string} Returns trimmed output or an empty string.
|
|
63
|
+
*/
|
|
64
|
+
export function runNxSafe (args) {
|
|
65
|
+
return runSafe(`${getNxCommandPrefix()} ${args}`, { cwd: WORKSPACE_ROOT })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Runs an Nx command while streaming output live (used for builds).
|
|
70
|
+
*
|
|
71
|
+
* @param {string} args The Nx command arguments.
|
|
72
|
+
*/
|
|
73
|
+
export function runNxInherit (args) {
|
|
74
|
+
runInherit(`${getNxCommandPrefix()} ${args}`, { cwd: WORKSPACE_ROOT })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parses an Nx project list from raw command output.
|
|
79
|
+
*
|
|
80
|
+
* Tolerates the Nx banner/warning text that can precede the JSON array, and
|
|
81
|
+
* falls back to line parsing when JSON is unavailable.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} output The raw command output.
|
|
84
|
+
* @returns {string[]} Returns parsed project names.
|
|
85
|
+
*/
|
|
86
|
+
export function parseProjectList (output) {
|
|
87
|
+
if (!output) {
|
|
88
|
+
return []
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const trimmed = output.trim()
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(trimmed)
|
|
95
|
+
if (Array.isArray(parsed)) {
|
|
96
|
+
return parsed.map(String)
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Falls through — Nx may have prepended banner text before the JSON array.
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const jsonMatch = trimmed.match(/\[[\s\S]*\]/)
|
|
103
|
+
if (jsonMatch) {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(jsonMatch[0])
|
|
106
|
+
if (Array.isArray(parsed)) {
|
|
107
|
+
return parsed.map(String)
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Falls through to line parsing.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return trimmed
|
|
115
|
+
.split('\n')
|
|
116
|
+
.map(line => line.trim())
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.filter(line => !line.startsWith('>'))
|
|
119
|
+
.filter(line => /^[\w-]/.test(line))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolves all Nx project names in the workspace.
|
|
124
|
+
*
|
|
125
|
+
* @returns {string[]} Returns all project names.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveAllProjects () {
|
|
128
|
+
const jsonProjects = parseProjectList(runNxSafe('show projects --json'))
|
|
129
|
+
if (jsonProjects.length > 0) {
|
|
130
|
+
return jsonProjects
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return parseProjectList(runNxSafe('show projects'))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolves the affected Nx project names between two commits.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} baseCommit The base commit SHA.
|
|
140
|
+
* @param {string} headCommit The head commit SHA.
|
|
141
|
+
* @returns {string[]} Returns affected project names.
|
|
142
|
+
*/
|
|
143
|
+
export function resolveAffectedProjects (baseCommit, headCommit) {
|
|
144
|
+
const range = `--base=${shellEscape(baseCommit)} --head=${shellEscape(headCommit)}`
|
|
145
|
+
|
|
146
|
+
const jsonOutput = runNxSafe(`show projects --affected ${range} --json`)
|
|
147
|
+
const jsonProjects = parseProjectList(jsonOutput)
|
|
148
|
+
if (jsonProjects.length > 0) {
|
|
149
|
+
return jsonProjects
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
log('[affected] JSON output empty — retrying without --json')
|
|
153
|
+
|
|
154
|
+
return parseProjectList(runNxSafe(`show projects --affected ${range}`))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Resolves Nx metadata for a single project.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} projectName The Nx project name.
|
|
161
|
+
* @returns {Record<string, any> | null} Returns project metadata or null.
|
|
162
|
+
*/
|
|
163
|
+
export function resolveProjectMetadata (projectName) {
|
|
164
|
+
const output = runNxSafe(`show project ${shellEscape(projectName)} --json`)
|
|
165
|
+
if (!output) {
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(output)
|
|
171
|
+
} catch {
|
|
172
|
+
return null
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Extracts a branch name from an Azure ref string.
|
|
178
|
+
*
|
|
179
|
+
* @param {string | undefined} reference The branch reference.
|
|
180
|
+
* @returns {string} Returns the branch name.
|
|
181
|
+
*/
|
|
182
|
+
export function getBranchNameFromRef (reference) {
|
|
183
|
+
if (!reference) {
|
|
184
|
+
return ''
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return reference.replace(/^refs\/heads\//, '')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolves the effective branch name used for build planning.
|
|
192
|
+
*
|
|
193
|
+
* @returns {string} Returns the effective branch name.
|
|
194
|
+
*/
|
|
195
|
+
export function resolveEffectiveBranchName () {
|
|
196
|
+
const isPullRequest = String(process.env.BUILD_REASON).toLowerCase() === 'pullrequest'
|
|
197
|
+
|
|
198
|
+
if (isPullRequest) {
|
|
199
|
+
return getBranchNameFromRef(process.env.SYSTEM_PULLREQUEST_TARGETBRANCH)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return process.env.BUILD_SOURCEBRANCHNAME || runSafe('git rev-parse --abbrev-ref HEAD')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolves the current HEAD commit used by Nx affected.
|
|
207
|
+
*
|
|
208
|
+
* @returns {string} Returns the HEAD commit SHA.
|
|
209
|
+
*/
|
|
210
|
+
export function resolveHeadCommit () {
|
|
211
|
+
return process.env.BUILD_SOURCEVERSION || run('git rev-parse HEAD')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Resolves the base commit used by Nx affected.
|
|
216
|
+
*
|
|
217
|
+
* Uses `~1` rather than `^` so the command never contains a caret, which the
|
|
218
|
+
* Windows command shell treats as an escape character.
|
|
219
|
+
*
|
|
220
|
+
* @param {string} headCommit The resolved head commit SHA.
|
|
221
|
+
* @returns {string} Returns the base commit SHA.
|
|
222
|
+
*/
|
|
223
|
+
export function resolveBaseCommit (headCommit) {
|
|
224
|
+
const isPullRequest = String(process.env.BUILD_REASON).toLowerCase() === 'pullrequest'
|
|
225
|
+
log(`[base] BUILD_REASON=${process.env.BUILD_REASON || '(none)'}, isPR=${isPullRequest}`)
|
|
226
|
+
|
|
227
|
+
if (isPullRequest) {
|
|
228
|
+
const targetBranchName = getBranchNameFromRef(process.env.SYSTEM_PULLREQUEST_TARGETBRANCH)
|
|
229
|
+
const remoteBranch = targetBranchName ? `origin/${targetBranchName}` : ''
|
|
230
|
+
log(`[base] PR target branch: ${remoteBranch || '(unknown)'}`)
|
|
231
|
+
|
|
232
|
+
const mergeBase = remoteBranch
|
|
233
|
+
? runSafe(`git merge-base ${shellEscape(headCommit)} ${shellEscape(remoteBranch)}`)
|
|
234
|
+
: ''
|
|
235
|
+
|
|
236
|
+
if (mergeBase) {
|
|
237
|
+
log(`[base] Resolved via merge-base: ${mergeBase}`)
|
|
238
|
+
return mergeBase
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
warn('[base] merge-base failed — falling back to parent commit')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const parentCommit = runSafe(`git rev-parse ${shellEscape(`${headCommit}~1`)}`)
|
|
245
|
+
if (parentCommit && parentCommit !== headCommit) {
|
|
246
|
+
log(`[base] Resolved via parent commit: ${parentCommit}`)
|
|
247
|
+
return parentCommit
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const initialCommit = runSafe(`git rev-list --max-parents=0 ${shellEscape(headCommit)}`).split('\n')[0]
|
|
251
|
+
log(`[base] Resolved via initial commit: ${initialCommit || '(none)'}`)
|
|
252
|
+
|
|
253
|
+
return initialCommit
|
|
254
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// Centralised ESLint flat config — generated by MoNecromanCI. Re-sync with 'monecromanci doctor'.
|
|
2
|
+
// Standard style (no semicolons) is hand-ported from neostandard (which has no ESLint 10
|
|
3
|
+
// release — peer pinned to ^9.0.0) onto @stylistic/eslint-plugin + typescript-eslint directly,
|
|
4
|
+
// plus unicorn, React, Jest, JSON/JSONC/JSON5, YAML, Markdown and TSDoc enforcement.
|
|
5
|
+
// NON type-checked on purpose (fast on large codebases).
|
|
6
|
+
import markdown from '@eslint/markdown'
|
|
7
|
+
import stylistic from '@stylistic/eslint-plugin'
|
|
8
|
+
import pluginJest from 'eslint-plugin-jest'
|
|
9
|
+
import jsonc from 'eslint-plugin-jsonc'
|
|
10
|
+
import n from 'eslint-plugin-n'
|
|
11
|
+
import promise from 'eslint-plugin-promise'
|
|
12
|
+
import react from 'eslint-plugin-react'
|
|
13
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
14
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
15
|
+
import tsdoc from 'eslint-plugin-tsdoc'
|
|
16
|
+
import tsdocRequire from 'eslint-plugin-tsdoc-require-2'
|
|
17
|
+
import unicorn from 'eslint-plugin-unicorn'
|
|
18
|
+
import unusedImports from 'eslint-plugin-unused-imports'
|
|
19
|
+
import yaml from 'eslint-plugin-yml'
|
|
20
|
+
import globalsConfig from 'globals'
|
|
21
|
+
import tseslint from 'typescript-eslint'
|
|
22
|
+
|
|
23
|
+
const code = ['**/*.{ts,mts,cts}', '**/*.{tsx,mtsx,ctsx}', '**/*.{js,mjs,cjs}', '**/*.{jsx,mjsx,cjsx}']
|
|
24
|
+
const tsx = ['**/*.{tsx,mtsx,ctsx}', '**/*.{jsx,mjsx,cjsx}']
|
|
25
|
+
const tsSource = ['**/*.{ts,mts,cts}', '**/*.{tsx,mtsx,ctsx}']
|
|
26
|
+
const tests = [
|
|
27
|
+
'**/*.{test,spec}.{ts,mts,cts}',
|
|
28
|
+
'**/*.{test,spec}.{tsx,mtsx,ctsx}',
|
|
29
|
+
'**/*.{test,spec}.{js,mjs,cjs}',
|
|
30
|
+
'**/*.{test,spec}.{jsx,mjsx,cjsx}',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
// Core ESLint rules + the n/promise rules neostandard enables on top — transcribed
|
|
34
|
+
// verbatim from neostandard's lib/configs/base.js, since neostandard itself has no
|
|
35
|
+
// ESLint-10-compatible release (peer pinned to ^9.0.0, no newer build exists).
|
|
36
|
+
const standardRules = {
|
|
37
|
+
'no-var': 'warn',
|
|
38
|
+
'object-shorthand': ['warn', 'properties'],
|
|
39
|
+
|
|
40
|
+
'accessor-pairs': ['error', { setWithoutGet: true, enforceForClassMembers: true }],
|
|
41
|
+
'array-callback-return': ['error', { allowImplicit: false, checkForEach: false }],
|
|
42
|
+
camelcase: ['error', { allow: ['^UNSAFE_'], properties: 'never', ignoreGlobals: true }],
|
|
43
|
+
'constructor-super': 'error',
|
|
44
|
+
curly: ['error', 'multi-line'],
|
|
45
|
+
'default-case-last': 'error',
|
|
46
|
+
'dot-notation': ['error', { allowKeywords: true }],
|
|
47
|
+
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
|
48
|
+
'new-cap': ['error', { newIsCap: true, capIsNew: false, properties: true }],
|
|
49
|
+
'no-array-constructor': 'error',
|
|
50
|
+
'no-async-promise-executor': 'error',
|
|
51
|
+
'no-caller': 'error',
|
|
52
|
+
'no-case-declarations': 'error',
|
|
53
|
+
'no-class-assign': 'error',
|
|
54
|
+
'no-compare-neg-zero': 'error',
|
|
55
|
+
'no-cond-assign': 'error',
|
|
56
|
+
'no-const-assign': 'error',
|
|
57
|
+
'no-constant-condition': ['error', { checkLoops: false }],
|
|
58
|
+
'no-control-regex': 'error',
|
|
59
|
+
'no-debugger': 'error',
|
|
60
|
+
'no-delete-var': 'error',
|
|
61
|
+
'no-dupe-args': 'error',
|
|
62
|
+
'no-dupe-class-members': 'error',
|
|
63
|
+
'no-dupe-keys': 'error',
|
|
64
|
+
'no-duplicate-case': 'error',
|
|
65
|
+
'no-useless-backreference': 'error',
|
|
66
|
+
'no-empty': ['error', { allowEmptyCatch: true }],
|
|
67
|
+
'no-empty-character-class': 'error',
|
|
68
|
+
'no-empty-pattern': 'error',
|
|
69
|
+
'no-eval': 'error',
|
|
70
|
+
'no-ex-assign': 'error',
|
|
71
|
+
'no-extend-native': 'error',
|
|
72
|
+
'no-extra-bind': 'error',
|
|
73
|
+
'no-extra-boolean-cast': 'error',
|
|
74
|
+
'no-fallthrough': 'error',
|
|
75
|
+
'no-func-assign': 'error',
|
|
76
|
+
'no-global-assign': 'error',
|
|
77
|
+
'no-implied-eval': 'error',
|
|
78
|
+
'no-import-assign': 'error',
|
|
79
|
+
'no-invalid-regexp': 'error',
|
|
80
|
+
'no-irregular-whitespace': 'error',
|
|
81
|
+
'no-iterator': 'error',
|
|
82
|
+
'no-labels': ['error', { allowLoop: false, allowSwitch: false }],
|
|
83
|
+
'no-lone-blocks': 'error',
|
|
84
|
+
'no-loss-of-precision': 'error',
|
|
85
|
+
'no-misleading-character-class': 'error',
|
|
86
|
+
'no-prototype-builtins': 'error',
|
|
87
|
+
'no-useless-catch': 'error',
|
|
88
|
+
'no-multi-str': 'error',
|
|
89
|
+
'no-new': 'error',
|
|
90
|
+
'no-new-func': 'error',
|
|
91
|
+
'no-object-constructor': 'error',
|
|
92
|
+
'no-new-native-nonconstructor': 'error',
|
|
93
|
+
'no-new-wrappers': 'error',
|
|
94
|
+
'no-obj-calls': 'error',
|
|
95
|
+
'no-octal': 'error',
|
|
96
|
+
'no-octal-escape': 'error',
|
|
97
|
+
'no-proto': 'error',
|
|
98
|
+
'no-redeclare': ['error', { builtinGlobals: false }],
|
|
99
|
+
'no-regex-spaces': 'error',
|
|
100
|
+
'no-return-assign': ['error', 'except-parens'],
|
|
101
|
+
'no-self-assign': ['error', { props: true }],
|
|
102
|
+
'no-self-compare': 'error',
|
|
103
|
+
'no-sequences': 'error',
|
|
104
|
+
'no-shadow-restricted-names': 'error',
|
|
105
|
+
'no-sparse-arrays': 'error',
|
|
106
|
+
'no-template-curly-in-string': 'error',
|
|
107
|
+
'no-this-before-super': 'error',
|
|
108
|
+
'no-throw-literal': 'error',
|
|
109
|
+
'no-undef': 'error',
|
|
110
|
+
'no-undef-init': 'error',
|
|
111
|
+
'no-unexpected-multiline': 'error',
|
|
112
|
+
'no-unmodified-loop-condition': 'error',
|
|
113
|
+
'no-unneeded-ternary': ['error', { defaultAssignment: false }],
|
|
114
|
+
'no-unreachable': 'error',
|
|
115
|
+
'no-unreachable-loop': 'error',
|
|
116
|
+
'no-unsafe-finally': 'error',
|
|
117
|
+
'no-unsafe-negation': 'error',
|
|
118
|
+
'no-unused-expressions': ['error', { allowShortCircuit: true, allowTernary: true, allowTaggedTemplates: true }],
|
|
119
|
+
'no-unused-vars': ['error', { args: 'none', caughtErrors: 'none', ignoreRestSiblings: true, vars: 'all' }],
|
|
120
|
+
'no-use-before-define': ['error', { functions: false, classes: false, variables: false }],
|
|
121
|
+
'no-useless-call': 'error',
|
|
122
|
+
'no-useless-computed-key': 'error',
|
|
123
|
+
'no-useless-constructor': 'error',
|
|
124
|
+
'no-useless-escape': 'error',
|
|
125
|
+
'no-useless-rename': 'error',
|
|
126
|
+
'no-useless-return': 'error',
|
|
127
|
+
'no-void': 'error',
|
|
128
|
+
'no-with': 'error',
|
|
129
|
+
'one-var': ['error', { initialized: 'never' }],
|
|
130
|
+
'prefer-const': ['error', { destructuring: 'all' }],
|
|
131
|
+
'prefer-promise-reject-errors': 'error',
|
|
132
|
+
'prefer-regex-literals': ['error', { disallowRedundantWrapping: true }],
|
|
133
|
+
'symbol-description': 'error',
|
|
134
|
+
'unicode-bom': ['error', 'never'],
|
|
135
|
+
'use-isnan': ['error', { enforceForSwitchCase: true, enforceForIndexOf: true }],
|
|
136
|
+
'valid-typeof': ['error', { requireStringLiterals: true }],
|
|
137
|
+
yoda: ['error', 'never'],
|
|
138
|
+
|
|
139
|
+
// TODO: Should only be active for server side scripts
|
|
140
|
+
'n/handle-callback-err': ['error', '^(err|error)$'],
|
|
141
|
+
'n/no-callback-literal': 'error',
|
|
142
|
+
'n/no-deprecated-api': 'error',
|
|
143
|
+
'n/no-exports-assign': 'error',
|
|
144
|
+
'n/no-new-require': 'error',
|
|
145
|
+
'n/no-path-concat': 'error',
|
|
146
|
+
'n/process-exit-as-throw': 'error',
|
|
147
|
+
|
|
148
|
+
'promise/param-names': 'error',
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// @stylistic and @typescript-eslint plugins are registered explicitly below; their
|
|
152
|
+
// rules resolve here because flat config merges plugins per matched file.
|
|
153
|
+
const projectRules = {
|
|
154
|
+
semi: ['error', 'never'],
|
|
155
|
+
'@stylistic/comma-dangle': ['error', 'always-multiline'],
|
|
156
|
+
'@stylistic/key-spacing': ['error', { align: { beforeColon: false, afterColon: true, on: 'value' } }],
|
|
157
|
+
// key-spacing's alignment inserts the multi-space runs that no-multi-spaces
|
|
158
|
+
// would otherwise strip back out on type literals/interfaces; exempt those.
|
|
159
|
+
'@stylistic/no-multi-spaces': ['error', { exceptions: { TSTypeAnnotation: true, TSIndexSignature: true, PropertyDefinition: true } }],
|
|
160
|
+
'@stylistic/jsx-quotes': ['error', 'prefer-single'],
|
|
161
|
+
// customize()'s factory defaults disagree with standard style here (named
|
|
162
|
+
// functions get no space; arrow-parens isn't part of standard style at all)
|
|
163
|
+
// — restore the long-standing convention instead of reformatting everything.
|
|
164
|
+
'@stylistic/space-before-function-paren': ['error', 'always'],
|
|
165
|
+
'@stylistic/arrow-parens': 'off',
|
|
166
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
167
|
+
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
168
|
+
'@typescript-eslint/no-unused-vars': 'off',
|
|
169
|
+
'unused-imports/no-unused-imports': 'error',
|
|
170
|
+
'unused-imports/no-unused-vars': ['warn', { vars: 'all', args: 'after-used', varsIgnorePattern: '^_', argsIgnorePattern: '^_' }],
|
|
171
|
+
'unicorn/no-null': 'off',
|
|
172
|
+
'unicorn/prefer-top-level-await': 'off',
|
|
173
|
+
'unicorn/prefer-module': 'off',
|
|
174
|
+
'unicorn/name-replacements': ['error', {
|
|
175
|
+
ignore: [/err/i, /dev/i, /db/i, /(pre)?(pro?d)/i, /conf(ig)?/i, /env/i, /dist/i, /req/i, /res/i, /lib/i, /vars?/i, /Props/],
|
|
176
|
+
checkShorthandProperties: false,
|
|
177
|
+
}],
|
|
178
|
+
'unicorn/filename-case': ['error', { cases: { camelCase: true, pascalCase: true }, ignore: [/^_.+\.[jt]s$/, /\.d\.ts$/] }],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const testRules = {
|
|
182
|
+
'@typescript-eslint/no-require-imports': 'off',
|
|
183
|
+
'@typescript-eslint/no-empty-function': 'off',
|
|
184
|
+
'unicorn/consistent-function-scoping': 'off',
|
|
185
|
+
// The declare-at-top/assign-in-beforeEach idiom is standard Jest fixture setup.
|
|
186
|
+
'unicorn/no-top-level-assignment-in-function': 'off',
|
|
187
|
+
'no-new': 'off',
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Several plugin "recommended" presets ship rule blocks with no `files` filter
|
|
191
|
+
// (meant to apply broadly to their own JS/JSON/YAML-shaped ASTs). @eslint/markdown
|
|
192
|
+
// parses .md through ESLint's newer "Language" API, whose SourceCode doesn't
|
|
193
|
+
// implement everything a generic rule may assume (e.g. `getAllComments`,
|
|
194
|
+
// `parserServices`), so those unscoped blocks crash instead of simply no-op'ing
|
|
195
|
+
// on markdown. Keep markdown out of every preset except the dedicated md block below.
|
|
196
|
+
const excludeMarkdown = (configs) => configs.map((config) => ({ ...config, ignores: [...(config.ignores ?? []), '**/*.md'] }))
|
|
197
|
+
|
|
198
|
+
export default [
|
|
199
|
+
{
|
|
200
|
+
ignores: [
|
|
201
|
+
'.nx/**', 'coverage/**', 'dist/**', 'dist-dev/**', 'dist-uat/**', 'dist-prod/**',
|
|
202
|
+
'doc/**', 'lib/**', 'node_modules/**', '**/node_modules', 'tmp/**',
|
|
203
|
+
'.next/**', 'out/**', '**/next-env.d.ts',
|
|
204
|
+
'.azurite/**', 'package-lock.json',
|
|
205
|
+
// Tool-owned scripts are regenerated by MoNecromanCI and exempt from the style gate.
|
|
206
|
+
'.build-templates/**', 'tools/**',
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
// @stylistic standard-style formatting (no semicolons, single quotes, 1tbs braces).
|
|
211
|
+
{
|
|
212
|
+
...stylistic.configs.customize({
|
|
213
|
+
braceStyle: '1tbs',
|
|
214
|
+
quoteProps: 'as-needed',
|
|
215
|
+
semi: false,
|
|
216
|
+
quotes: 'single',
|
|
217
|
+
commaDangle: 'always-multiline',
|
|
218
|
+
jsx: true,
|
|
219
|
+
}),
|
|
220
|
+
files: code,
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
// Standard-style core rules + cherry-picked n/promise rules (hand-ported from neostandard).
|
|
224
|
+
{
|
|
225
|
+
files: code,
|
|
226
|
+
plugins: { n, promise },
|
|
227
|
+
languageOptions: {
|
|
228
|
+
ecmaVersion: 2022,
|
|
229
|
+
sourceType: 'module',
|
|
230
|
+
globals: { ...globalsConfig.es2022, ...globalsConfig.node, ...globalsConfig.browser },
|
|
231
|
+
},
|
|
232
|
+
rules: standardRules,
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
// typescript-eslint, non type-checked. Already a strict superset of neostandard's
|
|
236
|
+
// separate ts-redundant rule list (turns off ESLint-core rules the TS compiler covers,
|
|
237
|
+
// plus a few opinionated upgrades), so it is not separately hand-rolled. Must precede
|
|
238
|
+
// `projectRules` below so this project's intentional overrides win.
|
|
239
|
+
...excludeMarkdown(tseslint.configs.recommended),
|
|
240
|
+
|
|
241
|
+
// Unicorn for all TS/JS.
|
|
242
|
+
{ ...unicorn.configs.recommended, files: code },
|
|
243
|
+
|
|
244
|
+
// Project rule tweaks + unused-imports.
|
|
245
|
+
{
|
|
246
|
+
files: code,
|
|
247
|
+
plugins: { 'unused-imports': unusedImports },
|
|
248
|
+
rules: projectRules,
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
// React (tsx/jsx) — declared explicitly to avoid plugin-config shape drift.
|
|
252
|
+
{
|
|
253
|
+
files: tsx,
|
|
254
|
+
plugins: { react },
|
|
255
|
+
settings: { react: { version: 'detect' } },
|
|
256
|
+
rules: {
|
|
257
|
+
'react/react-in-jsx-scope': 'off',
|
|
258
|
+
'react/jsx-uses-react': 'error',
|
|
259
|
+
'react/jsx-uses-vars': 'error',
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
files: tsx,
|
|
264
|
+
plugins: { 'react-hooks': reactHooks },
|
|
265
|
+
rules: { 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'off' },
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
files: tsx,
|
|
269
|
+
plugins: { 'react-refresh': reactRefresh },
|
|
270
|
+
rules: { 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }] },
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
// TSDoc enforcement — require docs on exported declarations (tsdoc-require-2) and
|
|
274
|
+
// validate TSDoc comment syntax wherever comments already exist (tsdoc).
|
|
275
|
+
{
|
|
276
|
+
// Require TSDoc on .ts logic/library code. Framework component files (.tsx —
|
|
277
|
+
// React/Next pages whose props are typed and whose return is JSX) are exempt.
|
|
278
|
+
...tsdocRequire.configs.tsdoc,
|
|
279
|
+
files: ['**/*.{ts,mts,cts}'],
|
|
280
|
+
ignores: [...tests, '**/*.d.ts'],
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
files: code,
|
|
284
|
+
plugins: { tsdoc },
|
|
285
|
+
rules: { 'tsdoc/syntax': 'error' },
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// Jest tests.
|
|
289
|
+
{
|
|
290
|
+
files: tests,
|
|
291
|
+
plugins: { jest: pluginJest },
|
|
292
|
+
languageOptions: { globals: { ...globalsConfig.jest } },
|
|
293
|
+
rules: testRules,
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
// JSON / JSONC / JSON5.
|
|
297
|
+
...excludeMarkdown(jsonc.configs['flat/recommended-with-json']),
|
|
298
|
+
...excludeMarkdown(jsonc.configs['flat/recommended-with-jsonc']),
|
|
299
|
+
...excludeMarkdown(jsonc.configs['flat/recommended-with-json5']),
|
|
300
|
+
|
|
301
|
+
// YAML.
|
|
302
|
+
...excludeMarkdown(yaml.configs['flat/standard']),
|
|
303
|
+
|
|
304
|
+
// Markdown.
|
|
305
|
+
{ files: ['**/*.md'], plugins: { markdown }, language: 'markdown/gfm' },
|
|
306
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Generated by MoNecromanCI. The .build-templates/*.mjs are the shared engine (same
|
|
4
|
+
# scripts Azure Pipelines runs); this workflow maps GitHub's context onto the
|
|
5
|
+
# BUILD_*/SYSTEM_* variables those scripts read. Re-sync with 'monecromanci doctor'.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches: [dev, development, uat, master, main]
|
|
10
|
+
paths-ignore: ['docs/**', '**/*.md']
|
|
11
|
+
pull_request:
|
|
12
|
+
branches: [dev, development, uat, master, main]
|
|
13
|
+
paths-ignore: ['docs/**', '**/*.md']
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
packages: write
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
ci:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
env:
|
|
23
|
+
BUILD_REASON: ${{ github.event_name == 'pull_request' && 'PullRequest' || 'IndividualCI' }}
|
|
24
|
+
BUILD_SOURCEBRANCHNAME: ${{ github.ref_name }}
|
|
25
|
+
BUILD_SOURCEVERSION: ${{ github.sha }}
|
|
26
|
+
SYSTEM_PULLREQUEST_TARGETBRANCH: ${{ github.base_ref }}
|
|
27
|
+
BUILD_SOURCESDIRECTORY: ${{ github.workspace }}
|
|
28
|
+
BUILD_BUILDID: ${{ github.run_id }}
|
|
29
|
+
BUILD_ARTIFACTSTAGINGDIRECTORY: ${{ runner.temp }}/artifacts
|
|
30
|
+
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN || github.token }}
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
with:
|
|
34
|
+
fetch-depth: 0
|
|
35
|
+
- name: Fetch all refs for affected detection
|
|
36
|
+
run: git fetch --all --prune --tags
|
|
37
|
+
- uses: actions/setup-node@v4
|
|
38
|
+
with:
|
|
39
|
+
node-version: 24
|
|
40
|
+
- name: Prepare artifact staging directory
|
|
41
|
+
run: mkdir -p "$BUILD_ARTIFACTSTAGINGDIRECTORY"
|
|
42
|
+
- name: Install dependencies
|
|
43
|
+
run: npm ci
|
|
44
|
+
env:
|
|
45
|
+
HUSKY: 0
|
|
46
|
+
- name: "[01] Preparation"
|
|
47
|
+
run: node .build-templates/01-preparation.mjs
|
|
48
|
+
- name: "[02] Quality control"
|
|
49
|
+
run: node .build-templates/02-quality-control.mjs
|
|
50
|
+
- name: "[03] Package apps"
|
|
51
|
+
run: node .build-templates/03-package-apps.mjs
|
|
52
|
+
- name: Upload app drops
|
|
53
|
+
if: ${{ always() }}
|
|
54
|
+
uses: actions/upload-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
name: drops
|
|
57
|
+
path: ${{ runner.temp }}/artifacts
|
|
58
|
+
if-no-files-found: ignore
|
|
59
|
+
- name: "[04] Publish libraries"
|
|
60
|
+
if: ${{ github.event_name != 'pull_request' && github.ref_name == github.event.repository.default_branch }}
|
|
61
|
+
run: node .build-templates/04-publish-libs.mjs
|
|
62
|
+
- name: "[05] Publish documentation"
|
|
63
|
+
if: ${{ vars.PUBLISH_DOCS == 'true' }}
|
|
64
|
+
run: node .build-templates/05-publish-documentation.mjs
|
|
65
|
+
- name: "[06] Summary"
|
|
66
|
+
if: ${{ always() }}
|
|
67
|
+
run: node .build-templates/06-summary.mjs
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* "Space-strips" a Function App's environment config files.
|
|
5
|
+
*
|
|
6
|
+
* Reads every JSON file under the project's `.configurations/` folder and
|
|
7
|
+
* rewrites it minified (no whitespace), so the classic release pipeline receives
|
|
8
|
+
* compact app-settings files. Run from the Function App directory (cwd = project
|
|
9
|
+
* root), typically by CI before the configs are copied to the build artifact:
|
|
10
|
+
* npm run clean:config -w <app>
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import process from 'node:process'
|
|
15
|
+
|
|
16
|
+
const configurationsDir = join(process.cwd(), '.configurations')
|
|
17
|
+
|
|
18
|
+
if (!existsSync(configurationsDir)) {
|
|
19
|
+
process.stdout.write('No .configurations folder — nothing to strip.\n')
|
|
20
|
+
process.exit(0)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let count = 0
|
|
24
|
+
for (const entry of readdirSync(configurationsDir)) {
|
|
25
|
+
if (!entry.endsWith('.json')) {
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const file = join(configurationsDir, entry)
|
|
30
|
+
const data = JSON.parse(readFileSync(file, 'utf8'))
|
|
31
|
+
writeFileSync(file, JSON.stringify(data), 'utf8')
|
|
32
|
+
count += 1
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process.stdout.write(`Stripped ${count} configuration file(s).\n`)
|