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,573 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Step 03 — Package affected apps.
|
|
5
|
+
*
|
|
6
|
+
* Builds, packages and stages every affected Azure Function app and React app
|
|
7
|
+
* declared in the context manifest. Function apps receive a generated runtime
|
|
8
|
+
* `package.json` whose dependencies are discovered from the built output, with
|
|
9
|
+
* internal workspace libraries vendored as tarballs. React apps are zipped per
|
|
10
|
+
* build output directory. The Azure YAML step then publishes the staged folders
|
|
11
|
+
* as pipeline artifacts.
|
|
12
|
+
*
|
|
13
|
+
* Run locally with `npm run pipeline:package` (after `npm ci`) to exercise the
|
|
14
|
+
* build/zip flow into `./.pipeline-out` without a pipeline. The local dry run
|
|
15
|
+
* skips the production dependency install (which needs registry auth).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { builtinModules } from 'node:module'
|
|
19
|
+
import {
|
|
20
|
+
cpSync,
|
|
21
|
+
existsSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
rmSync,
|
|
26
|
+
} from 'node:fs'
|
|
27
|
+
import path from 'node:path'
|
|
28
|
+
import process from 'node:process'
|
|
29
|
+
import {
|
|
30
|
+
addBuildTag,
|
|
31
|
+
banner,
|
|
32
|
+
isWindows,
|
|
33
|
+
log,
|
|
34
|
+
readJsonSafe,
|
|
35
|
+
run,
|
|
36
|
+
runInherit,
|
|
37
|
+
section,
|
|
38
|
+
shellEscape,
|
|
39
|
+
warn,
|
|
40
|
+
writeJson,
|
|
41
|
+
} from './lib/_h.mjs'
|
|
42
|
+
import { loadContext, selectAffected } from './lib/context.mjs'
|
|
43
|
+
import { runNxInherit } from './lib/nx.mjs'
|
|
44
|
+
|
|
45
|
+
const BUILTIN_MODULES = new Set([
|
|
46
|
+
...builtinModules,
|
|
47
|
+
...builtinModules.map(moduleName => `node:${moduleName.replace(/^node:/, '')}`),
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
const WORKSPACE_ROOT = process.cwd()
|
|
51
|
+
const SOURCES_DIR = process.env.BUILD_SOURCESDIRECTORY || WORKSPACE_ROOT
|
|
52
|
+
// Folder that gets published as an artifact — must contain ONLY the final zips.
|
|
53
|
+
const STAGING_DIR = process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || path.join(WORKSPACE_ROOT, '.pipeline-out')
|
|
54
|
+
// Scratch area for the unzipped app folders (incl. node_modules). NOT published —
|
|
55
|
+
// keeping it out of STAGING_DIR is what stops the artifact upload from shipping
|
|
56
|
+
// thousands of loose node_modules files.
|
|
57
|
+
const STAGE_AREA = process.env.AGENT_TEMPDIRECTORY || path.join(WORKSPACE_ROOT, '.pipeline-staging')
|
|
58
|
+
const BUILD_ID = process.env.BUILD_BUILDID || 'local'
|
|
59
|
+
const NPM_BIN = isWindows() ? 'npm.cmd' : 'npm'
|
|
60
|
+
const NPM_USER_CONFIG = path.join(SOURCES_DIR, '.npmrc')
|
|
61
|
+
|
|
62
|
+
const DRY_RUN = process.argv.includes('--dry-run')
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Recreates a directory, removing any previous contents.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} directoryPath The directory to recreate.
|
|
68
|
+
*/
|
|
69
|
+
function recreateDirectory (directoryPath) {
|
|
70
|
+
if (existsSync(directoryPath)) {
|
|
71
|
+
rmSync(directoryPath, { recursive: true, force: true })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
mkdirSync(directoryPath, { recursive: true })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Compresses the contents of a directory into a zip archive.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} sourceDirectory The directory whose contents are archived.
|
|
81
|
+
* @param {string} zipPath The destination archive path.
|
|
82
|
+
*/
|
|
83
|
+
function zipDirectoryContents (sourceDirectory, zipPath) {
|
|
84
|
+
mkdirSync(path.dirname(zipPath), { recursive: true })
|
|
85
|
+
|
|
86
|
+
if (existsSync(zipPath)) {
|
|
87
|
+
rmSync(zipPath, { force: true })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// bsdtar (`tar.exe`, bundled with Windows 10+/Server 2019+) writes a real zip
|
|
91
|
+
// from the `.zip` extension and is dramatically faster than Compress-Archive on
|
|
92
|
+
// trees with many small files (node_modules). The System32 path is used
|
|
93
|
+
// explicitly so a GNU `tar` on PATH (e.g. from Git) can't shadow it. Falls back
|
|
94
|
+
// to `zip` on POSIX.
|
|
95
|
+
if (isWindows()) {
|
|
96
|
+
const bsdtar = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe')
|
|
97
|
+
run(`${shellEscape(bsdtar)} -a -c -f ${shellEscape(zipPath)} .`, { cwd: sourceDirectory })
|
|
98
|
+
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
run(`zip -rq ${shellEscape(zipPath)} .`, { cwd: sourceDirectory })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ---------------------------------------------------------------------------
|
|
106
|
+
* Runtime dependency manifest (function apps)
|
|
107
|
+
* ------------------------------------------------------------------------- */
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolves the package name from an import specifier.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} specifier The import specifier.
|
|
113
|
+
* @returns {string} Returns the package name.
|
|
114
|
+
*/
|
|
115
|
+
function getPackageNameFromSpecifier (specifier) {
|
|
116
|
+
if (specifier.startsWith('@')) {
|
|
117
|
+
const [scope, packageName] = specifier.split('/')
|
|
118
|
+
|
|
119
|
+
return `${scope}/${packageName || ''}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return specifier.split('/')[0]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns whether an import specifier is a relative or absolute path.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} specifier The import specifier.
|
|
129
|
+
* @returns {boolean} Returns true when the specifier is path-like.
|
|
130
|
+
*/
|
|
131
|
+
function isPathSpecifier (specifier) {
|
|
132
|
+
return (
|
|
133
|
+
specifier === '.'
|
|
134
|
+
|| specifier === '..'
|
|
135
|
+
|| specifier.startsWith('./')
|
|
136
|
+
|| specifier.startsWith('../')
|
|
137
|
+
|| specifier.startsWith('/')
|
|
138
|
+
|| /^[A-Za-z]:[\\/]/.test(specifier)
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Lists JavaScript runtime files recursively under a directory.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} directoryPath The directory path.
|
|
146
|
+
* @returns {string[]} Returns runtime file paths.
|
|
147
|
+
*/
|
|
148
|
+
function listRuntimeFiles (directoryPath) {
|
|
149
|
+
const files = []
|
|
150
|
+
|
|
151
|
+
for (const entry of readdirSync(directoryPath, { withFileTypes: true })) {
|
|
152
|
+
const fullPath = path.join(directoryPath, entry.name)
|
|
153
|
+
|
|
154
|
+
if (entry.isDirectory()) {
|
|
155
|
+
files.push(...listRuntimeFiles(fullPath))
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (/\.(c|m)?js$/i.test(entry.name)) {
|
|
160
|
+
files.push(fullPath)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return files
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Extracts import and require specifiers from JavaScript source.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} sourceCode The JavaScript source.
|
|
171
|
+
* @returns {string[]} Returns extracted specifiers.
|
|
172
|
+
*/
|
|
173
|
+
function extractSpecifiersFromSource (sourceCode) {
|
|
174
|
+
const specifiers = new Set()
|
|
175
|
+
const patterns = [
|
|
176
|
+
/\bimport\s+[^'"\n]+\s+from\s+['"]([^'"]+)['"]/g,
|
|
177
|
+
/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
178
|
+
/\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
179
|
+
/\bexport\s+[^'"\n]+\s+from\s+['"]([^'"]+)['"]/g,
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
for (const expression of patterns) {
|
|
183
|
+
for (const match of sourceCode.matchAll(expression)) {
|
|
184
|
+
if (match[1]) {
|
|
185
|
+
specifiers.add(match[1])
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return [...specifiers]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Resolves workspace package manifests from the root workspace globs.
|
|
195
|
+
*
|
|
196
|
+
* @param {string[]} workspaceGlobs The root workspace glob entries.
|
|
197
|
+
* @returns {Map<string, {name: string, version: string, directoryPath: string}>} Returns the workspace package map.
|
|
198
|
+
*/
|
|
199
|
+
function resolveWorkspacePackages (workspaceGlobs) {
|
|
200
|
+
const workspacePackages = new Map()
|
|
201
|
+
|
|
202
|
+
for (const workspaceGlob of workspaceGlobs) {
|
|
203
|
+
if (!workspaceGlob.endsWith('/*')) {
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const absoluteParent = path.join(WORKSPACE_ROOT, workspaceGlob.slice(0, -2))
|
|
208
|
+
if (!existsSync(absoluteParent)) {
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const child of readdirSync(absoluteParent, { withFileTypes: true })) {
|
|
213
|
+
if (!child.isDirectory()) {
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const packageDirectory = path.join(absoluteParent, child.name)
|
|
218
|
+
const packagePath = path.join(packageDirectory, 'package.json')
|
|
219
|
+
if (!existsSync(packagePath)) {
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const packageJson = readJsonSafe(packagePath)
|
|
224
|
+
if (!packageJson?.name || !packageJson?.version) {
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
workspacePackages.set(packageJson.name, {
|
|
229
|
+
name: packageJson.name,
|
|
230
|
+
version: packageJson.version,
|
|
231
|
+
directoryPath: packageDirectory,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return workspacePackages
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Reads the optional runtime dependency allowlist for an app.
|
|
241
|
+
*
|
|
242
|
+
* @param {string} appRoot The app root path.
|
|
243
|
+
* @returns {{externalPackages: string[], internalPackages: string[]}} Returns the allowlist.
|
|
244
|
+
*/
|
|
245
|
+
function readRuntimeAllowlist (appRoot) {
|
|
246
|
+
const allowlist = readJsonSafe(path.join(appRoot, 'runtimeDependencies.json'))
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
externalPackages: Array.isArray(allowlist.externalPackages) ? allowlist.externalPackages : [],
|
|
250
|
+
internalPackages: Array.isArray(allowlist.internalPackages) ? allowlist.internalPackages : [],
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Resolves the installed version of a package from the root node_modules.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} packageName The package name.
|
|
258
|
+
* @returns {string} Returns the installed version or an empty string.
|
|
259
|
+
*/
|
|
260
|
+
function resolveInstalledPackageVersion (packageName) {
|
|
261
|
+
const packagePath = path.join(WORKSPACE_ROOT, 'node_modules', packageName, 'package.json')
|
|
262
|
+
|
|
263
|
+
return String(readJsonSafe(packagePath).version || '').trim()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Packs a workspace package into a vendor directory and returns the tarball.
|
|
268
|
+
*
|
|
269
|
+
* @param {string} packageDirectory The package directory.
|
|
270
|
+
* @param {string} vendorDirectory The destination vendor directory.
|
|
271
|
+
* @returns {string} Returns the produced tarball file name.
|
|
272
|
+
*/
|
|
273
|
+
function packWorkspacePackage (packageDirectory, vendorDirectory) {
|
|
274
|
+
const output = run(`${NPM_BIN} pack --pack-destination ${shellEscape(vendorDirectory)}`, { cwd: packageDirectory })
|
|
275
|
+
|
|
276
|
+
return output.split('\n').map(line => line.trim()).filter(Boolean).at(-1) || ''
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Generates the runtime `package.json` and vendors internal dependencies.
|
|
281
|
+
*
|
|
282
|
+
* @param {{appName: string, appRoot: string, distRoot: string, stageRoot: string}} input The manifest input.
|
|
283
|
+
* @returns {{externalDependencies: string[], internalDependencies: string[], runtimeFilesScanned: number}} Returns the dependency report.
|
|
284
|
+
*/
|
|
285
|
+
function generateRuntimeManifest (input) {
|
|
286
|
+
const { appName, appRoot, distRoot, stageRoot } = input
|
|
287
|
+
const rootPackageJson = readJsonSafe(path.join(WORKSPACE_ROOT, 'package.json'))
|
|
288
|
+
const appPackageJson = readJsonSafe(path.join(appRoot, 'package.json'))
|
|
289
|
+
const workspacePackages = resolveWorkspacePackages(rootPackageJson.workspaces || [])
|
|
290
|
+
const allowlist = readRuntimeAllowlist(appRoot)
|
|
291
|
+
|
|
292
|
+
const externalPackages = new Set(allowlist.externalPackages)
|
|
293
|
+
const internalPackages = new Set(allowlist.internalPackages)
|
|
294
|
+
|
|
295
|
+
for (const runtimeFile of listRuntimeFiles(distRoot)) {
|
|
296
|
+
for (const specifier of extractSpecifiersFromSource(readFileSync(runtimeFile, 'utf8'))) {
|
|
297
|
+
if (isPathSpecifier(specifier) || BUILTIN_MODULES.has(specifier)) {
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const packageName = getPackageNameFromSpecifier(specifier)
|
|
302
|
+
if (!packageName) {
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (workspacePackages.has(packageName)) {
|
|
307
|
+
internalPackages.add(packageName)
|
|
308
|
+
} else {
|
|
309
|
+
externalPackages.add(packageName)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const runtimeFilesScanned = listRuntimeFiles(distRoot).length
|
|
315
|
+
const rootDependencies = {
|
|
316
|
+
...rootPackageJson.dependencies,
|
|
317
|
+
...rootPackageJson.optionalDependencies,
|
|
318
|
+
...rootPackageJson.devDependencies,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const externalDependencies = {}
|
|
322
|
+
const missingExternal = []
|
|
323
|
+
|
|
324
|
+
for (const packageName of [...externalPackages].sort((left, right) => left.localeCompare(right))) {
|
|
325
|
+
const version = rootDependencies[packageName] || resolveInstalledPackageVersion(packageName)
|
|
326
|
+
|
|
327
|
+
if (version) {
|
|
328
|
+
externalDependencies[packageName] = version
|
|
329
|
+
} else {
|
|
330
|
+
missingExternal.push(packageName)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (missingExternal.length > 0) {
|
|
335
|
+
throw new Error(`[${appName}] Missing versions in root dependencies or node_modules: ${missingExternal.join(', ')}`)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const internalDependencies = {}
|
|
339
|
+
if (internalPackages.size > 0) {
|
|
340
|
+
mkdirSync(path.join(stageRoot, 'vendor'), { recursive: true })
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (const packageName of [...internalPackages].sort((left, right) => left.localeCompare(right))) {
|
|
344
|
+
const workspacePackage = workspacePackages.get(packageName)
|
|
345
|
+
if (!workspacePackage) {
|
|
346
|
+
throw new Error(`[${appName}] Internal dependency '${packageName}' was detected but not found in the workspace.`)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const tarballName = packWorkspacePackage(workspacePackage.directoryPath, path.join(stageRoot, 'vendor'))
|
|
350
|
+
if (!tarballName) {
|
|
351
|
+
throw new Error(`[${appName}] Failed to pack workspace dependency '${packageName}'.`)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
internalDependencies[packageName] = `file:./vendor/${tarballName}`
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
writeJson(path.join(stageRoot, 'package.json'), {
|
|
358
|
+
name: appPackageJson.name,
|
|
359
|
+
version: appPackageJson.version,
|
|
360
|
+
description: appPackageJson.description,
|
|
361
|
+
license: appPackageJson.license,
|
|
362
|
+
main: appPackageJson.main,
|
|
363
|
+
repository: appPackageJson.repository,
|
|
364
|
+
dependencies: { ...externalDependencies, ...internalDependencies },
|
|
365
|
+
overrides: rootPackageJson.overrides || {},
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
const report = {
|
|
369
|
+
app: appName,
|
|
370
|
+
runtimeFilesScanned,
|
|
371
|
+
externalDependencies: Object.keys(externalDependencies),
|
|
372
|
+
internalDependencies: Object.keys(internalDependencies),
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
writeJson(path.join(stageRoot, 'runtime-dependency-report.json'), report)
|
|
376
|
+
log(`[${appName}] runtime files scanned: ${runtimeFilesScanned}`)
|
|
377
|
+
log(`[${appName}] external deps (${report.externalDependencies.length}): ${report.externalDependencies.join(', ') || 'none'}`)
|
|
378
|
+
log(`[${appName}] internal deps (${report.internalDependencies.length}): ${report.internalDependencies.join(', ') || 'none'}`)
|
|
379
|
+
|
|
380
|
+
return report
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* ---------------------------------------------------------------------------
|
|
384
|
+
* Packaging
|
|
385
|
+
* ------------------------------------------------------------------------- */
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Builds a server app, stages dist + a discovered runtime `package.json` (with
|
|
389
|
+
* internal libs vendored), installs production deps, and zips the result.
|
|
390
|
+
*
|
|
391
|
+
* The unzipped app (incl. node_modules) is staged in `stageRootBase/<name>`
|
|
392
|
+
* (a scratch area), and only the final zip lands in `dropRoot` (the published
|
|
393
|
+
* artifact folder). Shared by Azure Function apps and generic Node apps.
|
|
394
|
+
*
|
|
395
|
+
* @param {Record<string, any>} project The server app project data.
|
|
396
|
+
* @param {string} dropRoot The published artifact directory.
|
|
397
|
+
* @param {string} stageRootBase The scratch staging base directory.
|
|
398
|
+
* @returns {string} Returns the resolved app root path.
|
|
399
|
+
*/
|
|
400
|
+
function stageAndZipServerApp (project, dropRoot, stageRootBase) {
|
|
401
|
+
runNxInherit(`run ${project.name}:build`)
|
|
402
|
+
|
|
403
|
+
const appRoot = path.join(SOURCES_DIR, project.root)
|
|
404
|
+
const distRoot = path.join(appRoot, 'dist')
|
|
405
|
+
if (!existsSync(distRoot)) {
|
|
406
|
+
throw new Error(`[${project.name}] dist folder not found: ${distRoot}`)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const stageRoot = path.join(stageRootBase, project.name)
|
|
410
|
+
recreateDirectory(stageRoot)
|
|
411
|
+
cpSync(distRoot, path.join(stageRoot, 'dist'), { recursive: true })
|
|
412
|
+
|
|
413
|
+
const hostJsonPath = path.join(appRoot, 'host.json')
|
|
414
|
+
if (existsSync(hostJsonPath)) {
|
|
415
|
+
cpSync(hostJsonPath, path.join(stageRoot, 'host.json'))
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
generateRuntimeManifest({ appName: project.name, appRoot, distRoot, stageRoot })
|
|
419
|
+
|
|
420
|
+
if (DRY_RUN) {
|
|
421
|
+
warn(`[${project.name}] dry run — skipping production dependency install`)
|
|
422
|
+
} else {
|
|
423
|
+
runInherit(`${NPM_BIN} install --userconfig ${shellEscape(NPM_USER_CONFIG)} --prefix ${shellEscape(stageRoot)} --omit=dev --ignore-scripts --no-audit --no-fund --prefer-offline`)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const zipPath = path.join(dropRoot, `${project.name}-${BUILD_ID}.zip`)
|
|
427
|
+
zipDirectoryContents(stageRoot, zipPath)
|
|
428
|
+
log(`[${project.name}] packaged: ${zipPath}`)
|
|
429
|
+
addBuildTag(project.name)
|
|
430
|
+
|
|
431
|
+
return appRoot
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Builds, stages and zips a single Azure Function app, then stages its
|
|
436
|
+
* (whitespace-stripped) environment configuration files for the release pipeline.
|
|
437
|
+
*
|
|
438
|
+
* @param {Record<string, any>} project The function app project data.
|
|
439
|
+
* @param {{dropRoot: string, configRoot: string, stageRoot: string}} directories The output directories.
|
|
440
|
+
*/
|
|
441
|
+
function packageFunctionApp (project, directories) {
|
|
442
|
+
section(`Function app: ${project.name}`)
|
|
443
|
+
|
|
444
|
+
const appRoot = stageAndZipServerApp(project, directories.dropRoot, directories.stageRoot)
|
|
445
|
+
|
|
446
|
+
const scripts = readJsonSafe(path.join(appRoot, 'package.json')).scripts || {}
|
|
447
|
+
if (scripts['clean:config'] && project.packageName) {
|
|
448
|
+
runInherit(`${NPM_BIN} run clean:config -w ${shellEscape(project.packageName)} --userconfig ${shellEscape(NPM_USER_CONFIG)}`, { cwd: SOURCES_DIR })
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const configSource = path.join(appRoot, '.configurations')
|
|
452
|
+
if (existsSync(configSource)) {
|
|
453
|
+
cpSync(configSource, path.join(directories.configRoot, project.name), { recursive: true })
|
|
454
|
+
log(`[${project.name}] configurations staged`)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Builds, stages and zips a single generic Node.js app (no Functions host or
|
|
460
|
+
* environment configuration files).
|
|
461
|
+
*
|
|
462
|
+
* @param {Record<string, any>} project The Node app project data.
|
|
463
|
+
* @param {{dropRoot: string, stageRoot: string}} directories The output directories.
|
|
464
|
+
*/
|
|
465
|
+
function packageNodeApp (project, directories) {
|
|
466
|
+
section(`Node app: ${project.name}`)
|
|
467
|
+
stageAndZipServerApp(project, directories.dropRoot, directories.stageRoot)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Builds, stages and zips a single React app.
|
|
472
|
+
*
|
|
473
|
+
* @param {Record<string, any>} project The React app project data.
|
|
474
|
+
* @param {{artifactRoot: string}} directories The output directories.
|
|
475
|
+
*/
|
|
476
|
+
function packageReactApp (project, directories) {
|
|
477
|
+
section(`React app: ${project.name}`)
|
|
478
|
+
|
|
479
|
+
const buildCommand = project.reactBuild?.command
|
|
480
|
+
if (!buildCommand) {
|
|
481
|
+
throw new Error(`[${project.name}] no React build command resolved.`)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
log(`[${project.name}] building with "${buildCommand}"`)
|
|
485
|
+
runInherit(`${NPM_BIN} run ${buildCommand} -w ${shellEscape(project.packageName)}`, { cwd: SOURCES_DIR })
|
|
486
|
+
|
|
487
|
+
const appRoot = path.join(SOURCES_DIR, project.root)
|
|
488
|
+
const configuredDirs = Array.isArray(project.reactBuild?.distDirs) ? project.reactBuild.distDirs : []
|
|
489
|
+
const existingDirs = configuredDirs.filter(distDir => existsSync(path.join(appRoot, distDir)))
|
|
490
|
+
const missingDirs = configuredDirs.filter(distDir => !existsSync(path.join(appRoot, distDir)))
|
|
491
|
+
|
|
492
|
+
if (missingDirs.length > 0) {
|
|
493
|
+
warn(`[${project.name}] configured outputs not produced by "${buildCommand}": ${missingDirs.join(', ')}`)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (existingDirs.length === 0) {
|
|
497
|
+
throw new Error(`[${project.name}] no build output directories were produced (expected one of: ${configuredDirs.join(', ') || 'dist'}).`)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const packageRoot = path.join(directories.artifactRoot, project.name)
|
|
501
|
+
recreateDirectory(packageRoot)
|
|
502
|
+
|
|
503
|
+
for (const distDir of existingDirs) {
|
|
504
|
+
const zipPath = path.join(packageRoot, `${distDir}-${BUILD_ID}.zip`)
|
|
505
|
+
zipDirectoryContents(path.join(appRoot, distDir), zipPath)
|
|
506
|
+
log(`[${project.name}] packaged ${distDir}: ${zipPath}`)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
addBuildTag(project.name)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Packages every affected function app and React app from the context manifest.
|
|
514
|
+
*/
|
|
515
|
+
function main () {
|
|
516
|
+
banner(`[03] Package affected apps${DRY_RUN ? ' (dry run)' : ''}`)
|
|
517
|
+
|
|
518
|
+
const context = loadContext()
|
|
519
|
+
const functionApps = selectAffected(context, project => project.type.functionApp)
|
|
520
|
+
const nodeApps = selectAffected(context, project => project.type.nodeApp)
|
|
521
|
+
const reactApps = selectAffected(context, project => project.type.reactApp)
|
|
522
|
+
|
|
523
|
+
log(`Function apps (${functionApps.length}): ${functionApps.map(project => project.name).join(', ') || 'none'}`)
|
|
524
|
+
log(`Node apps (${nodeApps.length}): ${nodeApps.map(project => project.name).join(', ') || 'none'}`)
|
|
525
|
+
log(`React apps (${reactApps.length}): ${reactApps.map(project => project.name).join(', ') || 'none'}`)
|
|
526
|
+
|
|
527
|
+
if (functionApps.length === 0 && nodeApps.length === 0 && reactApps.length === 0) {
|
|
528
|
+
banner('[03] No affected apps to package')
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const functionDirectories = {
|
|
533
|
+
dropRoot: path.join(STAGING_DIR, 'function-apps'),
|
|
534
|
+
configRoot: path.join(STAGING_DIR, 'function-app-configurations'),
|
|
535
|
+
stageRoot: path.join(STAGE_AREA, 'function-apps'),
|
|
536
|
+
}
|
|
537
|
+
const nodeDirectories = {
|
|
538
|
+
dropRoot: path.join(STAGING_DIR, 'node-apps'),
|
|
539
|
+
stageRoot: path.join(STAGE_AREA, 'node-apps'),
|
|
540
|
+
}
|
|
541
|
+
const reactDirectories = { artifactRoot: path.join(STAGING_DIR, 'react-apps') }
|
|
542
|
+
|
|
543
|
+
if (functionApps.length > 0) {
|
|
544
|
+
recreateDirectory(functionDirectories.dropRoot)
|
|
545
|
+
recreateDirectory(functionDirectories.configRoot)
|
|
546
|
+
recreateDirectory(functionDirectories.stageRoot)
|
|
547
|
+
|
|
548
|
+
for (const project of functionApps) {
|
|
549
|
+
packageFunctionApp(project, functionDirectories)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (nodeApps.length > 0) {
|
|
554
|
+
recreateDirectory(nodeDirectories.dropRoot)
|
|
555
|
+
recreateDirectory(nodeDirectories.stageRoot)
|
|
556
|
+
|
|
557
|
+
for (const project of nodeApps) {
|
|
558
|
+
packageNodeApp(project, nodeDirectories)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (reactApps.length > 0) {
|
|
563
|
+
recreateDirectory(reactDirectories.artifactRoot)
|
|
564
|
+
|
|
565
|
+
for (const project of reactApps) {
|
|
566
|
+
packageReactApp(project, reactDirectories)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
banner(`[03] Packaging complete — ${functionApps.length} function app(s), ${nodeApps.length} node app(s), ${reactApps.length} React app(s)`)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
steps:
|
|
2
|
+
- script: node .build-templates/03-package-apps.mjs
|
|
3
|
+
displayName: "[03] Build, package and stage affected apps"
|
|
4
|
+
condition: >
|
|
5
|
+
and(succeeded(),
|
|
6
|
+
or(eq(variables['HAS_FUNCTION_APPS'], 'true'),
|
|
7
|
+
eq(variables['HAS_NODE_APPS'], 'true'),
|
|
8
|
+
eq(variables['HAS_REACT_APPS'], 'true')))
|
|
9
|
+
|
|
10
|
+
- task: PublishBuildArtifacts@1
|
|
11
|
+
displayName: "[03] Publish function app artifacts"
|
|
12
|
+
condition: and(succeeded(), eq(variables['HAS_FUNCTION_APPS'], 'true'))
|
|
13
|
+
inputs:
|
|
14
|
+
PathtoPublish: $(Build.ArtifactStagingDirectory)/function-apps
|
|
15
|
+
ArtifactName: drop-function-apps
|
|
16
|
+
|
|
17
|
+
- task: PublishPipelineArtifact@1
|
|
18
|
+
displayName: "[03] Publish function app configurations"
|
|
19
|
+
condition: and(succeeded(), eq(variables['HAS_FUNCTION_APPS'], 'true'))
|
|
20
|
+
inputs:
|
|
21
|
+
targetPath: $(Build.ArtifactStagingDirectory)/function-app-configurations
|
|
22
|
+
artifact: config-function-apps
|
|
23
|
+
publishLocation: pipeline
|
|
24
|
+
|
|
25
|
+
- task: PublishBuildArtifacts@1
|
|
26
|
+
displayName: "[03] Publish Node app artifacts"
|
|
27
|
+
condition: and(succeeded(), eq(variables['HAS_NODE_APPS'], 'true'))
|
|
28
|
+
inputs:
|
|
29
|
+
PathtoPublish: $(Build.ArtifactStagingDirectory)/node-apps
|
|
30
|
+
ArtifactName: drop-node-apps
|
|
31
|
+
|
|
32
|
+
- task: PublishBuildArtifacts@1
|
|
33
|
+
displayName: "[03] Publish React app artifacts"
|
|
34
|
+
condition: and(succeeded(), eq(variables['HAS_REACT_APPS'], 'true'))
|
|
35
|
+
inputs:
|
|
36
|
+
PathtoPublish: $(Build.ArtifactStagingDirectory)/react-apps
|
|
37
|
+
ArtifactName: drop-react-apps
|