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.
@@ -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`)