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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2466 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
var import_node_fs5 = require("fs");
|
|
6
|
+
var import_node_path9 = require("path");
|
|
7
|
+
var import_commander = require("commander");
|
|
8
|
+
|
|
9
|
+
// src/engine/config.ts
|
|
10
|
+
var import_node_path2 = require("path");
|
|
11
|
+
|
|
12
|
+
// src/engine/constants.ts
|
|
13
|
+
var TEMPLATE_VERSION = "0.2.0";
|
|
14
|
+
var STAMP_FILE = ".monecromanci.json";
|
|
15
|
+
var DEFAULT_NODE_VERSION = "24";
|
|
16
|
+
var DEFAULT_BASE = "main";
|
|
17
|
+
var TAGS = {
|
|
18
|
+
functionApp: "type:function-app",
|
|
19
|
+
nodeApp: "type:node-app",
|
|
20
|
+
reactApp: "type:react-app",
|
|
21
|
+
vueApp: "type:vue-app",
|
|
22
|
+
svelteApp: "type:svelte-app",
|
|
23
|
+
nextjsApp: "type:nextjs-app",
|
|
24
|
+
publishableLib: "type:publishable-lib",
|
|
25
|
+
internalLib: "type:internal-lib",
|
|
26
|
+
ignore: "ci:ignore"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/engine/fsx.ts
|
|
30
|
+
var import_node_fs = require("fs");
|
|
31
|
+
var import_node_path = require("path");
|
|
32
|
+
var import_node_fs2 = require("fs");
|
|
33
|
+
function ensureDirectory(directory) {
|
|
34
|
+
if (!(0, import_node_fs.existsSync)(directory)) {
|
|
35
|
+
(0, import_node_fs.mkdirSync)(directory, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function writeFileEnsured(filePath, content) {
|
|
39
|
+
ensureDirectory((0, import_node_path.dirname)(filePath));
|
|
40
|
+
(0, import_node_fs.writeFileSync)(filePath, content, "utf8");
|
|
41
|
+
}
|
|
42
|
+
function readJsonSafe(filePath, fallback) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse((0, import_node_fs.readFileSync)(filePath, "utf8"));
|
|
45
|
+
} catch {
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function readTextSafe(filePath) {
|
|
50
|
+
try {
|
|
51
|
+
return (0, import_node_fs.readFileSync)(filePath, "utf8");
|
|
52
|
+
} catch {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function toJson(value) {
|
|
57
|
+
return `${JSON.stringify(value, void 0, 2)}
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/engine/config.ts
|
|
62
|
+
function stampPath(repoRoot) {
|
|
63
|
+
return (0, import_node_path2.join)(repoRoot, STAMP_FILE);
|
|
64
|
+
}
|
|
65
|
+
function isManagedRepo(repoRoot) {
|
|
66
|
+
return (0, import_node_fs2.existsSync)(stampPath(repoRoot));
|
|
67
|
+
}
|
|
68
|
+
function migrateConfig(raw) {
|
|
69
|
+
const registry = raw.registry ?? (raw.azure ? { kind: "azure-artifacts", ...raw.azure } : { kind: "npm" });
|
|
70
|
+
return { ...raw, ci: raw.ci ?? "azure", registry };
|
|
71
|
+
}
|
|
72
|
+
function loadConfig(repoRoot) {
|
|
73
|
+
if (!isManagedRepo(repoRoot)) {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
const raw = readJsonSafe(stampPath(repoRoot), void 0);
|
|
77
|
+
return raw ? migrateConfig(raw) : void 0;
|
|
78
|
+
}
|
|
79
|
+
function saveConfig(repoRoot, config) {
|
|
80
|
+
writeFileEnsured(stampPath(repoRoot), toJson(config));
|
|
81
|
+
}
|
|
82
|
+
function configFromVars(vars) {
|
|
83
|
+
return {
|
|
84
|
+
templateVersion: TEMPLATE_VERSION,
|
|
85
|
+
workspaceName: vars.workspaceName,
|
|
86
|
+
displayName: vars.displayName,
|
|
87
|
+
scope: vars.scope,
|
|
88
|
+
defaultBase: vars.defaultBase,
|
|
89
|
+
nodeVersion: vars.nodeVersion,
|
|
90
|
+
ci: vars.ci,
|
|
91
|
+
registry: vars.registry
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/util/logger.ts
|
|
96
|
+
var logger = {
|
|
97
|
+
info(message) {
|
|
98
|
+
console.log(message);
|
|
99
|
+
},
|
|
100
|
+
step(message) {
|
|
101
|
+
console.log(`\u2192 ${message}`);
|
|
102
|
+
},
|
|
103
|
+
success(message) {
|
|
104
|
+
console.log(`\u2713 ${message}`);
|
|
105
|
+
},
|
|
106
|
+
warn(message) {
|
|
107
|
+
console.warn(`! ${message}`);
|
|
108
|
+
},
|
|
109
|
+
error(message) {
|
|
110
|
+
console.error(`\u2717 ${message}`);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/util/prompts.ts
|
|
115
|
+
var import_prompts = require("@inquirer/prompts");
|
|
116
|
+
var import_prompts2 = require("@inquirer/prompts");
|
|
117
|
+
async function promptText(message, fallback) {
|
|
118
|
+
const value = await (0, import_prompts.input)({
|
|
119
|
+
message,
|
|
120
|
+
default: fallback,
|
|
121
|
+
validate: (value2) => value2.trim().length > 0 || "A value is required"
|
|
122
|
+
});
|
|
123
|
+
return value.trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/util/strings.ts
|
|
127
|
+
function toSlug(input3) {
|
|
128
|
+
return input3.trim().replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").replaceAll(/[^a-zA-Z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "").toLowerCase();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/engine/apply.ts
|
|
132
|
+
var import_node_path3 = require("path");
|
|
133
|
+
function applyFiles(repoRoot, files) {
|
|
134
|
+
const result = { created: [], overwritten: [], skipped: [] };
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
const absolute = (0, import_node_path3.join)(repoRoot, file.path);
|
|
137
|
+
const exists = (0, import_node_fs2.existsSync)(absolute);
|
|
138
|
+
if (file.ownership === "scaffold" && exists) {
|
|
139
|
+
result.skipped.push(file.path);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
writeFileEnsured(absolute, file.content);
|
|
143
|
+
if (exists) {
|
|
144
|
+
result.overwritten.push(file.path);
|
|
145
|
+
} else {
|
|
146
|
+
result.created.push(file.path);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
function reportApply(result) {
|
|
152
|
+
for (const path of result.created) {
|
|
153
|
+
logger.success(`created ${path}`);
|
|
154
|
+
}
|
|
155
|
+
for (const path of result.overwritten) {
|
|
156
|
+
logger.step(`updated ${path}`);
|
|
157
|
+
}
|
|
158
|
+
for (const path of result.skipped) {
|
|
159
|
+
logger.info(` kept ${path} (already exists)`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/engine/rootPackage.ts
|
|
164
|
+
var import_node_path4 = require("path");
|
|
165
|
+
function addRootDependencies(repoRoot, dependencies, section = "dependencies") {
|
|
166
|
+
const manifestPath = (0, import_node_path4.join)(repoRoot, "package.json");
|
|
167
|
+
const manifest = readJsonSafe(manifestPath, {});
|
|
168
|
+
const existing = manifest[section] ?? {};
|
|
169
|
+
const added = [];
|
|
170
|
+
for (const [name, version] of Object.entries(dependencies)) {
|
|
171
|
+
if (Object.hasOwn(existing, name)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
existing[name] = version;
|
|
175
|
+
added.push(name);
|
|
176
|
+
}
|
|
177
|
+
manifest[section] = Object.fromEntries(
|
|
178
|
+
Object.entries(existing).toSorted(([left], [right]) => left.localeCompare(right))
|
|
179
|
+
);
|
|
180
|
+
writeFileEnsured(manifestPath, toJson(manifest));
|
|
181
|
+
return added;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/templates/frontendApp.ts
|
|
185
|
+
function appPackageJson(vars) {
|
|
186
|
+
return toJson({
|
|
187
|
+
name: vars.packageName,
|
|
188
|
+
version: "0.0.0",
|
|
189
|
+
private: true,
|
|
190
|
+
type: "module",
|
|
191
|
+
scripts: {
|
|
192
|
+
dev: "vite",
|
|
193
|
+
"build:dev": "vite build --mode dev --outDir dist-dev",
|
|
194
|
+
"build:uat": "vite build --mode uat --outDir dist-uat",
|
|
195
|
+
"build:prod": "vite build --mode prod --outDir dist-prod",
|
|
196
|
+
"build:all": "npm run build:dev && npm run build:uat && npm run build:prod",
|
|
197
|
+
build: "npm run build:dev",
|
|
198
|
+
preview: "vite preview",
|
|
199
|
+
lint: "eslint . -c ../../eslint.config.mjs",
|
|
200
|
+
test: "jest --collectCoverage"
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function appProjectJson(vars, framework) {
|
|
205
|
+
const run = (target) => ({
|
|
206
|
+
executor: "nx:run-commands",
|
|
207
|
+
options: { command: `npm run ${target} -w ${vars.packageName}` }
|
|
208
|
+
});
|
|
209
|
+
return toJson({
|
|
210
|
+
name: vars.name,
|
|
211
|
+
$schema: "../../node_modules/nx/schemas/project-schema.json",
|
|
212
|
+
sourceRoot: `apps/${vars.name}/src`,
|
|
213
|
+
projectType: "application",
|
|
214
|
+
tags: [framework.tag],
|
|
215
|
+
targets: {
|
|
216
|
+
build: {
|
|
217
|
+
executor: "nx:run-commands",
|
|
218
|
+
outputs: ["{projectRoot}/dist-dev", "{projectRoot}/dist-uat", "{projectRoot}/dist-prod"],
|
|
219
|
+
options: { command: `npm run build -w ${vars.packageName}` }
|
|
220
|
+
},
|
|
221
|
+
serve: run("dev"),
|
|
222
|
+
test: run("test"),
|
|
223
|
+
lint: run("lint")
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function appTsconfig() {
|
|
228
|
+
return toJson({
|
|
229
|
+
extends: "../../tsconfig.base.json",
|
|
230
|
+
compilerOptions: {
|
|
231
|
+
target: "es2022",
|
|
232
|
+
lib: ["es2022", "DOM", "DOM.Iterable"],
|
|
233
|
+
module: "esnext",
|
|
234
|
+
moduleResolution: "bundler",
|
|
235
|
+
types: ["vite/client", "node"],
|
|
236
|
+
noEmit: true,
|
|
237
|
+
sourceMap: true,
|
|
238
|
+
allowSyntheticDefaultImports: true,
|
|
239
|
+
esModuleInterop: true
|
|
240
|
+
},
|
|
241
|
+
include: ["src", "vite.config.ts"]
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function appTypedoc() {
|
|
245
|
+
return toJson({
|
|
246
|
+
extends: ["../../typedoc.json"],
|
|
247
|
+
entryPoints: ["./src"],
|
|
248
|
+
out: "doc",
|
|
249
|
+
exclude: ["./node_modules/**", "./src/**/*.test.ts"]
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function viteConfig(framework) {
|
|
253
|
+
return `${framework.vitePluginImport}
|
|
254
|
+
import { defineConfig } from 'vite'
|
|
255
|
+
|
|
256
|
+
export default defineConfig({ plugins: [${framework.vitePluginCall}], server: { port: 5173 }, build: { sourcemap: true } })
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
function indexHtml(vars, framework) {
|
|
260
|
+
return `<!doctype html>
|
|
261
|
+
<html lang="en">
|
|
262
|
+
<head>
|
|
263
|
+
<meta charset="UTF-8" />
|
|
264
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
265
|
+
<title>${vars.name}</title>
|
|
266
|
+
</head>
|
|
267
|
+
<body>
|
|
268
|
+
<div id="${framework.mountId}"></div>
|
|
269
|
+
<script type="module" src="/src/main.ts"></script>
|
|
270
|
+
</body>
|
|
271
|
+
</html>
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
var greetingTs = `/**
|
|
275
|
+
* Returns the greeting rendered by the app.
|
|
276
|
+
*
|
|
277
|
+
* @remarks Shared TS logic \u2014 unit-tested directly so no SFC test transform is needed.
|
|
278
|
+
* @param name - The name to greet.
|
|
279
|
+
* @returns The greeting text.
|
|
280
|
+
* @throws Never - performs no I/O.
|
|
281
|
+
* @typeParam None - this function has no generic type parameters.
|
|
282
|
+
*/
|
|
283
|
+
export function greet (name: string): string {
|
|
284
|
+
return 'Hello from ' + name + '!'
|
|
285
|
+
}
|
|
286
|
+
`;
|
|
287
|
+
var greetingTestTs = `import { greet } from './greeting'
|
|
288
|
+
|
|
289
|
+
describe('greet', () => {
|
|
290
|
+
it('greets the given name', () => {
|
|
291
|
+
expect(greet('world')).toBe('Hello from world!')
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
`;
|
|
295
|
+
function envFile(environment) {
|
|
296
|
+
return `VITE_ENVIRONMENT=${environment}
|
|
297
|
+
VITE_API_URL=https://${environment}.example.com
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
var VUE = {
|
|
301
|
+
tag: TAGS.vueApp,
|
|
302
|
+
mountId: "app",
|
|
303
|
+
vitePluginImport: "import vue from '@vitejs/plugin-vue'",
|
|
304
|
+
vitePluginCall: "vue()",
|
|
305
|
+
componentPath: "src/App.vue",
|
|
306
|
+
componentContent: `<script setup lang="ts">
|
|
307
|
+
import { greet } from './greeting'
|
|
308
|
+
|
|
309
|
+
const message = greet('Vue')
|
|
310
|
+
</script>
|
|
311
|
+
|
|
312
|
+
<template>
|
|
313
|
+
<main>
|
|
314
|
+
<h1>{{ message }}</h1>
|
|
315
|
+
</main>
|
|
316
|
+
</template>
|
|
317
|
+
`,
|
|
318
|
+
mainContent: `import { createApp } from 'vue'
|
|
319
|
+
import App from './App.vue'
|
|
320
|
+
|
|
321
|
+
createApp(App).mount('#app')
|
|
322
|
+
`,
|
|
323
|
+
envContent: `/// <reference types="vite/client" />
|
|
324
|
+
|
|
325
|
+
declare module '*.vue' {
|
|
326
|
+
import type { DefineComponent } from 'vue'
|
|
327
|
+
|
|
328
|
+
const component: DefineComponent
|
|
329
|
+
export default component
|
|
330
|
+
}
|
|
331
|
+
`
|
|
332
|
+
};
|
|
333
|
+
var SVELTE = {
|
|
334
|
+
tag: TAGS.svelteApp,
|
|
335
|
+
mountId: "app",
|
|
336
|
+
vitePluginImport: "import { svelte } from '@sveltejs/vite-plugin-svelte'",
|
|
337
|
+
vitePluginCall: "svelte()",
|
|
338
|
+
componentPath: "src/App.svelte",
|
|
339
|
+
componentContent: `<script lang="ts">
|
|
340
|
+
import { greet } from './greeting'
|
|
341
|
+
|
|
342
|
+
const message = greet('Svelte')
|
|
343
|
+
</script>
|
|
344
|
+
|
|
345
|
+
<main>
|
|
346
|
+
<h1>{ message }</h1>
|
|
347
|
+
</main>
|
|
348
|
+
`,
|
|
349
|
+
mainContent: `import { mount } from 'svelte'
|
|
350
|
+
import App from './App.svelte'
|
|
351
|
+
|
|
352
|
+
mount(App, { target: document.querySelector('#app')! })
|
|
353
|
+
`,
|
|
354
|
+
envContent: `/// <reference types="vite/client" />
|
|
355
|
+
|
|
356
|
+
declare module '*.svelte' {
|
|
357
|
+
import type { Component } from 'svelte'
|
|
358
|
+
|
|
359
|
+
const component: Component
|
|
360
|
+
export default component
|
|
361
|
+
}
|
|
362
|
+
`
|
|
363
|
+
};
|
|
364
|
+
function frontendAppFiles(vars, framework) {
|
|
365
|
+
const root = `apps/${vars.name}`;
|
|
366
|
+
const file = (path, content, ownership) => ({ path: `${root}/${path}`, content, ownership });
|
|
367
|
+
return [
|
|
368
|
+
file("package.json", appPackageJson(vars), "scaffold"),
|
|
369
|
+
file("project.json", appProjectJson(vars, framework), "tool-owned"),
|
|
370
|
+
file("tsconfig.json", appTsconfig(), "tool-owned"),
|
|
371
|
+
file("vite.config.ts", viteConfig(framework), "scaffold"),
|
|
372
|
+
file("index.html", indexHtml(vars, framework), "scaffold"),
|
|
373
|
+
file("jest.config.mjs", `import { createConfig } from '../../jest.preset.mjs'
|
|
374
|
+
|
|
375
|
+
export default createConfig('${vars.name}')
|
|
376
|
+
`, "scaffold"),
|
|
377
|
+
file("typedoc.json", appTypedoc(), "tool-owned"),
|
|
378
|
+
file(".env.dev", envFile("dev"), "scaffold"),
|
|
379
|
+
file(".env.uat", envFile("uat"), "scaffold"),
|
|
380
|
+
file(".env.prod", envFile("prod"), "scaffold"),
|
|
381
|
+
file("src/vite-env.d.ts", framework.envContent, "scaffold"),
|
|
382
|
+
file("src/main.ts", framework.mainContent, "scaffold"),
|
|
383
|
+
file(framework.componentPath, framework.componentContent, "scaffold"),
|
|
384
|
+
file("src/greeting.ts", greetingTs, "scaffold"),
|
|
385
|
+
file("src/greeting.test.ts", greetingTestTs, "scaffold")
|
|
386
|
+
];
|
|
387
|
+
}
|
|
388
|
+
function vueAppFiles(vars) {
|
|
389
|
+
return frontendAppFiles(vars, VUE);
|
|
390
|
+
}
|
|
391
|
+
function svelteAppFiles(vars) {
|
|
392
|
+
return frontendAppFiles(vars, SVELTE);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/engine/assets.ts
|
|
396
|
+
var import_node_fs3 = require("fs");
|
|
397
|
+
var import_node_path5 = require("path");
|
|
398
|
+
var assetsRoot = /* @__PURE__ */ (() => {
|
|
399
|
+
let cachedRoot;
|
|
400
|
+
return () => {
|
|
401
|
+
if (cachedRoot) {
|
|
402
|
+
return cachedRoot;
|
|
403
|
+
}
|
|
404
|
+
let directory = __dirname;
|
|
405
|
+
for (let level = 0; level < 8; level++) {
|
|
406
|
+
const candidate = (0, import_node_path5.join)(directory, "assets");
|
|
407
|
+
if ((0, import_node_fs3.existsSync)(candidate)) {
|
|
408
|
+
cachedRoot = candidate;
|
|
409
|
+
return candidate;
|
|
410
|
+
}
|
|
411
|
+
const parent = (0, import_node_path5.dirname)(directory);
|
|
412
|
+
if (parent === directory) {
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
directory = parent;
|
|
416
|
+
}
|
|
417
|
+
throw new Error("MoNecromanCI assets directory not found");
|
|
418
|
+
};
|
|
419
|
+
})();
|
|
420
|
+
function readAsset(relativePath) {
|
|
421
|
+
return (0, import_node_fs3.readFileSync)((0, import_node_path5.join)(assetsRoot(), relativePath), "utf8");
|
|
422
|
+
}
|
|
423
|
+
function listAssetFiles(relativeDirectory) {
|
|
424
|
+
const base = (0, import_node_path5.join)(assetsRoot(), relativeDirectory);
|
|
425
|
+
const files = [];
|
|
426
|
+
const walk = (directory) => {
|
|
427
|
+
const entries = (0, import_node_fs3.readdirSync)(directory, { withFileTypes: true });
|
|
428
|
+
for (const entry of entries) {
|
|
429
|
+
const full = (0, import_node_path5.join)(directory, entry.name);
|
|
430
|
+
if (entry.isDirectory()) {
|
|
431
|
+
walk(full);
|
|
432
|
+
} else {
|
|
433
|
+
files.push((0, import_node_path5.relative)(base, full).split(import_node_path5.sep).join("/"));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
walk(base);
|
|
438
|
+
return files;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/templates/functionApp.ts
|
|
442
|
+
function appPackageJson2(vars) {
|
|
443
|
+
return toJson({
|
|
444
|
+
name: vars.packageName,
|
|
445
|
+
version: "0.0.0",
|
|
446
|
+
private: true,
|
|
447
|
+
type: "commonjs",
|
|
448
|
+
main: "dist/index.js",
|
|
449
|
+
dependencies: {},
|
|
450
|
+
scripts: {
|
|
451
|
+
build: "tsc -p tsconfig.app.json",
|
|
452
|
+
watch: "tsc -p tsconfig.app.json -w",
|
|
453
|
+
start: "func start",
|
|
454
|
+
"clean:config": "node ../../tools/clean-config.mjs",
|
|
455
|
+
lint: "eslint . -c ../../eslint.config.mjs",
|
|
456
|
+
test: "jest --collectCoverage",
|
|
457
|
+
doc: "typedoc --tsconfig tsconfig.app.json"
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function appProjectJson2(vars) {
|
|
462
|
+
const run = (target) => ({
|
|
463
|
+
executor: "nx:run-commands",
|
|
464
|
+
options: { command: `npm run ${target} -w ${vars.packageName}` }
|
|
465
|
+
});
|
|
466
|
+
return toJson({
|
|
467
|
+
name: vars.name,
|
|
468
|
+
$schema: "../../node_modules/nx/schemas/project-schema.json",
|
|
469
|
+
sourceRoot: `apps/${vars.name}/src`,
|
|
470
|
+
projectType: "application",
|
|
471
|
+
tags: [TAGS.functionApp],
|
|
472
|
+
targets: {
|
|
473
|
+
build: { executor: "nx:run-commands", outputs: ["{projectRoot}/dist"], options: { command: `npm run build -w ${vars.packageName}` } },
|
|
474
|
+
serve: run("start"),
|
|
475
|
+
test: run("test"),
|
|
476
|
+
lint: run("lint"),
|
|
477
|
+
doc: run("doc")
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
function appTsconfig2() {
|
|
482
|
+
return toJson({
|
|
483
|
+
extends: "../../tsconfig.base.json",
|
|
484
|
+
compilerOptions: {
|
|
485
|
+
baseUrl: ".",
|
|
486
|
+
rootDir: ".",
|
|
487
|
+
outDir: "./dist",
|
|
488
|
+
module: "commonjs",
|
|
489
|
+
moduleResolution: "node",
|
|
490
|
+
target: "es2022",
|
|
491
|
+
sourceMap: true,
|
|
492
|
+
declaration: false,
|
|
493
|
+
declarationMap: false,
|
|
494
|
+
esModuleInterop: true
|
|
495
|
+
},
|
|
496
|
+
exclude: ["./coverage/**", "./dist/**", "./doc/**", "./node_modules/**"]
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
function appTsconfigApp() {
|
|
500
|
+
return toJson({
|
|
501
|
+
extends: "./tsconfig.json",
|
|
502
|
+
compilerOptions: {
|
|
503
|
+
rootDir: "./src",
|
|
504
|
+
noEmit: false,
|
|
505
|
+
sourceMap: true,
|
|
506
|
+
removeComments: false
|
|
507
|
+
},
|
|
508
|
+
exclude: ["./coverage/**", "./dist/**", "./doc/**", "./node_modules/**", "./src/**/*.test.ts"]
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
function appTypedoc2() {
|
|
512
|
+
return toJson({
|
|
513
|
+
extends: ["../../typedoc.json"],
|
|
514
|
+
entryPoints: ["./src"],
|
|
515
|
+
out: "doc",
|
|
516
|
+
exclude: ["./node_modules/**", "./src/**/*.test.ts"]
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
function hostJson() {
|
|
520
|
+
return toJson({
|
|
521
|
+
version: "2.0",
|
|
522
|
+
logging: {
|
|
523
|
+
applicationInsights: {
|
|
524
|
+
samplingSettings: { isEnabled: true, excludedTypes: "Request" }
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
extensionBundle: {
|
|
528
|
+
id: "Microsoft.Azure.Functions.ExtensionBundle",
|
|
529
|
+
version: "[4.*, 5.0.0)"
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
function localSettingsJson() {
|
|
534
|
+
return toJson({
|
|
535
|
+
IsEncrypted: false,
|
|
536
|
+
Values: {
|
|
537
|
+
FUNCTIONS_WORKER_RUNTIME: "node",
|
|
538
|
+
AzureWebJobsStorage: "",
|
|
539
|
+
languageWorkers__node__arguments: "--inspect=9229"
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function configurationFile(environment) {
|
|
544
|
+
return toJson([
|
|
545
|
+
{ name: "FUNCTIONS_WORKER_RUNTIME", value: "node", slotSetting: false },
|
|
546
|
+
{ name: "WEBSITE_NODE_DEFAULT_VERSION", value: "~24", slotSetting: false },
|
|
547
|
+
{ name: "ENVIRONMENT", value: environment, slotSetting: false }
|
|
548
|
+
]);
|
|
549
|
+
}
|
|
550
|
+
var greetingTs2 = `/**
|
|
551
|
+
* Builds the greeting returned by the sample HTTP function.
|
|
552
|
+
*
|
|
553
|
+
* @remarks Pure string helper \u2014 replace with your own logic.
|
|
554
|
+
* @param name - The name to greet.
|
|
555
|
+
* @returns The greeting text.
|
|
556
|
+
* @throws Never - performs no I/O.
|
|
557
|
+
* @typeParam None - this function has no generic type parameters.
|
|
558
|
+
*/
|
|
559
|
+
export function buildGreeting (name: string): string {
|
|
560
|
+
return 'Hello, ' + name + '!'
|
|
561
|
+
}
|
|
562
|
+
`;
|
|
563
|
+
var greetingTestTs2 = `import { buildGreeting } from './greeting'
|
|
564
|
+
|
|
565
|
+
describe('buildGreeting', () => {
|
|
566
|
+
it('greets a name', () => {
|
|
567
|
+
expect(buildGreeting('world')).toBe('Hello, world!')
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
`;
|
|
571
|
+
var helloTs = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions'
|
|
572
|
+
import { buildGreeting } from '../greeting'
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Sample HTTP-triggered function.
|
|
576
|
+
*
|
|
577
|
+
* @remarks Registered with the Functions host via the app.http call below.
|
|
578
|
+
* @param request - The incoming HTTP request.
|
|
579
|
+
* @param context - The Azure Functions invocation context.
|
|
580
|
+
* @returns The HTTP response payload.
|
|
581
|
+
* @throws Never - failures are surfaced by the Functions host.
|
|
582
|
+
* @typeParam None - this function has no generic type parameters.
|
|
583
|
+
*/
|
|
584
|
+
export async function hello (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
|
|
585
|
+
context.log('HTTP function processed a request for ' + request.url)
|
|
586
|
+
const name = request.query.get('name') ?? 'world'
|
|
587
|
+
|
|
588
|
+
return { body: buildGreeting(name) }
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// app.http performs the Azure Functions v4 registration at import time.
|
|
592
|
+
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
|
593
|
+
app.http('hello', { methods: ['GET'], authLevel: 'anonymous', handler: hello })
|
|
594
|
+
`;
|
|
595
|
+
var indexTs = `import './functions/hello'
|
|
596
|
+
`;
|
|
597
|
+
function functionAppFiles(vars) {
|
|
598
|
+
const root = `apps/${vars.name}`;
|
|
599
|
+
const file = (path, content, ownership) => ({ path: `${root}/${path}`, content, ownership });
|
|
600
|
+
return [
|
|
601
|
+
file("package.json", appPackageJson2(vars), "scaffold"),
|
|
602
|
+
file("project.json", appProjectJson2(vars), "tool-owned"),
|
|
603
|
+
file("tsconfig.json", appTsconfig2(), "tool-owned"),
|
|
604
|
+
file("tsconfig.app.json", appTsconfigApp(), "tool-owned"),
|
|
605
|
+
file("host.json", hostJson(), "scaffold"),
|
|
606
|
+
file("local.settings.json", localSettingsJson(), "scaffold"),
|
|
607
|
+
file("jest.config.mjs", `import { createConfig } from '../../jest.preset.mjs'
|
|
608
|
+
|
|
609
|
+
export default createConfig('${vars.name}')
|
|
610
|
+
`, "scaffold"),
|
|
611
|
+
file("typedoc.json", appTypedoc2(), "tool-owned"),
|
|
612
|
+
file(".configurations/dev.json", configurationFile("dev"), "scaffold"),
|
|
613
|
+
file(".configurations/uat.json", configurationFile("uat"), "scaffold"),
|
|
614
|
+
file(".configurations/prod.json", configurationFile("prod"), "scaffold"),
|
|
615
|
+
file("src/index.ts", indexTs, "scaffold"),
|
|
616
|
+
file("src/greeting.ts", greetingTs2, "scaffold"),
|
|
617
|
+
file("src/greeting.test.ts", greetingTestTs2, "scaffold"),
|
|
618
|
+
file("src/functions/hello.ts", helloTs, "scaffold"),
|
|
619
|
+
{ path: "tools/clean-config.mjs", content: readAsset("scripts/clean-config.mjs"), ownership: "tool-owned" }
|
|
620
|
+
];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/templates/internalLib.ts
|
|
624
|
+
function libPackageJson(vars) {
|
|
625
|
+
return toJson({
|
|
626
|
+
name: vars.packageName,
|
|
627
|
+
version: "0.0.0",
|
|
628
|
+
private: true,
|
|
629
|
+
type: "commonjs",
|
|
630
|
+
// Point at source so consumers resolve TS directly: enables step-into-source
|
|
631
|
+
// debugging and editor "find references" across internal libraries.
|
|
632
|
+
main: "./src/index.ts",
|
|
633
|
+
types: "./src/index.ts",
|
|
634
|
+
dependencies: {},
|
|
635
|
+
scripts: {
|
|
636
|
+
build: "tsc -p ./tsconfig.lib.json",
|
|
637
|
+
test: "jest --collectCoverage",
|
|
638
|
+
lint: "eslint . -c ../../eslint.config.mjs",
|
|
639
|
+
doc: "typedoc --tsconfig tsconfig.lib.json"
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
function libProjectJson(vars) {
|
|
644
|
+
const run = (target) => ({
|
|
645
|
+
executor: "nx:run-commands",
|
|
646
|
+
options: { command: `npm run ${target} -w ${vars.packageName}` }
|
|
647
|
+
});
|
|
648
|
+
return toJson({
|
|
649
|
+
name: vars.name,
|
|
650
|
+
$schema: "../../node_modules/nx/schemas/project-schema.json",
|
|
651
|
+
sourceRoot: `libs/${vars.name}/src`,
|
|
652
|
+
projectType: "library",
|
|
653
|
+
tags: [TAGS.internalLib],
|
|
654
|
+
targets: {
|
|
655
|
+
build: { executor: "nx:run-commands", outputs: ["{projectRoot}/dist"], options: { command: `npm run build -w ${vars.packageName}` } },
|
|
656
|
+
test: run("test"),
|
|
657
|
+
lint: run("lint"),
|
|
658
|
+
doc: run("doc")
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
function libTsconfig() {
|
|
663
|
+
return toJson({
|
|
664
|
+
extends: "../../tsconfig.base.json",
|
|
665
|
+
compilerOptions: {
|
|
666
|
+
baseUrl: ".",
|
|
667
|
+
rootDir: ".",
|
|
668
|
+
outDir: "./dist",
|
|
669
|
+
module: "nodenext",
|
|
670
|
+
moduleResolution: "nodenext",
|
|
671
|
+
target: "es2024",
|
|
672
|
+
lib: ["es2024"],
|
|
673
|
+
noEmit: true,
|
|
674
|
+
emitDeclarationOnly: true,
|
|
675
|
+
sourceMap: true,
|
|
676
|
+
declaration: true,
|
|
677
|
+
declarationMap: true,
|
|
678
|
+
removeComments: false,
|
|
679
|
+
allowSyntheticDefaultImports: true,
|
|
680
|
+
importHelpers: true,
|
|
681
|
+
isolatedModules: true
|
|
682
|
+
},
|
|
683
|
+
exclude: ["./coverage/**", "./dist/**", "./doc/**", "./node_modules/**"]
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
function libTsconfigLib() {
|
|
687
|
+
return toJson({
|
|
688
|
+
extends: "./tsconfig.json",
|
|
689
|
+
compilerOptions: {
|
|
690
|
+
rootDir: "./src",
|
|
691
|
+
noEmit: false,
|
|
692
|
+
emitDeclarationOnly: false,
|
|
693
|
+
sourceMap: true,
|
|
694
|
+
declaration: true,
|
|
695
|
+
declarationMap: true,
|
|
696
|
+
removeComments: false
|
|
697
|
+
},
|
|
698
|
+
exclude: ["./coverage/**", "./dist/**", "./doc/**", "./node_modules/**", "./src/**/*.test.ts", "./src/_jest/**"]
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
function libTypedoc() {
|
|
702
|
+
return toJson({
|
|
703
|
+
extends: ["../../typedoc.json"],
|
|
704
|
+
entryPoints: ["./src"],
|
|
705
|
+
out: "doc",
|
|
706
|
+
exclude: ["./node_modules/**", "./src/**/*.test.ts"]
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
var indexTs2 = `export * from './greeter'
|
|
710
|
+
`;
|
|
711
|
+
var greeterTs = `/**
|
|
712
|
+
* Returns a friendly greeting for the given name.
|
|
713
|
+
*
|
|
714
|
+
* @remarks Demonstrates step-into-source debugging across internal libraries.
|
|
715
|
+
* @param name - The name to greet (surrounding whitespace is trimmed).
|
|
716
|
+
* @returns The greeting text.
|
|
717
|
+
* @throws Never - performs no I/O.
|
|
718
|
+
* @typeParam None - this function has no generic type parameters.
|
|
719
|
+
*/
|
|
720
|
+
export function greet (name: string): string {
|
|
721
|
+
const trimmed = name.trim() // \u2190 set a breakpoint here, then run "Debug Jest (current file)"
|
|
722
|
+
|
|
723
|
+
return 'Hello, ' + trimmed + '!'
|
|
724
|
+
}
|
|
725
|
+
`;
|
|
726
|
+
var greeterTestTs = `import { greet } from './index'
|
|
727
|
+
|
|
728
|
+
describe('greet', () => {
|
|
729
|
+
it('greets a trimmed name', () => {
|
|
730
|
+
expect(greet(' Ada ')).toBe('Hello, Ada!')
|
|
731
|
+
})
|
|
732
|
+
})
|
|
733
|
+
`;
|
|
734
|
+
function internalLibFiles(vars) {
|
|
735
|
+
const root = `libs/${vars.name}`;
|
|
736
|
+
const toolOwned = (path, content) => ({ path: `${root}/${path}`, content, ownership: "tool-owned" });
|
|
737
|
+
const scaffold = (path, content) => ({ path: `${root}/${path}`, content, ownership: "scaffold" });
|
|
738
|
+
return [
|
|
739
|
+
scaffold("package.json", libPackageJson(vars)),
|
|
740
|
+
toolOwned("project.json", libProjectJson(vars)),
|
|
741
|
+
toolOwned("tsconfig.json", libTsconfig()),
|
|
742
|
+
toolOwned("tsconfig.lib.json", libTsconfigLib()),
|
|
743
|
+
scaffold("jest.config.mjs", `import { createConfig } from '../../jest.preset.mjs'
|
|
744
|
+
|
|
745
|
+
export default createConfig('${vars.name}')
|
|
746
|
+
`),
|
|
747
|
+
toolOwned("typedoc.json", libTypedoc()),
|
|
748
|
+
scaffold("src/index.ts", indexTs2),
|
|
749
|
+
scaffold("src/greeter.ts", greeterTs),
|
|
750
|
+
scaffold("src/greeter.test.ts", greeterTestTs)
|
|
751
|
+
];
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/templates/nextApp.ts
|
|
755
|
+
var buildEnvScript = (environment) => `dotenv -e .env.${environment} -- node ../../tools/next-build.mjs ${environment}`;
|
|
756
|
+
function appPackageJson3(vars) {
|
|
757
|
+
return toJson({
|
|
758
|
+
name: vars.packageName,
|
|
759
|
+
version: "0.0.0",
|
|
760
|
+
private: true,
|
|
761
|
+
type: "module",
|
|
762
|
+
scripts: {
|
|
763
|
+
dev: "next dev",
|
|
764
|
+
"build:dev": buildEnvScript("dev"),
|
|
765
|
+
"build:uat": buildEnvScript("uat"),
|
|
766
|
+
"build:prod": buildEnvScript("prod"),
|
|
767
|
+
"build:all": "npm run build:dev && npm run build:uat && npm run build:prod",
|
|
768
|
+
build: "npm run build:dev",
|
|
769
|
+
start: "next start",
|
|
770
|
+
lint: "eslint . -c ../../eslint.config.mjs",
|
|
771
|
+
test: "jest --collectCoverage"
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
function appProjectJson3(vars) {
|
|
776
|
+
const run = (target) => ({
|
|
777
|
+
executor: "nx:run-commands",
|
|
778
|
+
options: { command: `npm run ${target} -w ${vars.packageName}` }
|
|
779
|
+
});
|
|
780
|
+
return toJson({
|
|
781
|
+
name: vars.name,
|
|
782
|
+
$schema: "../../node_modules/nx/schemas/project-schema.json",
|
|
783
|
+
sourceRoot: `apps/${vars.name}/src`,
|
|
784
|
+
projectType: "application",
|
|
785
|
+
tags: [TAGS.nextjsApp],
|
|
786
|
+
targets: {
|
|
787
|
+
build: {
|
|
788
|
+
executor: "nx:run-commands",
|
|
789
|
+
outputs: ["{projectRoot}/dist-dev", "{projectRoot}/dist-uat", "{projectRoot}/dist-prod"],
|
|
790
|
+
options: { command: `npm run build -w ${vars.packageName}` }
|
|
791
|
+
},
|
|
792
|
+
serve: run("dev"),
|
|
793
|
+
test: run("test"),
|
|
794
|
+
lint: run("lint")
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
function appTsconfig3() {
|
|
799
|
+
return toJson({
|
|
800
|
+
extends: "../../tsconfig.base.json",
|
|
801
|
+
compilerOptions: {
|
|
802
|
+
target: "es2022",
|
|
803
|
+
lib: ["es2022", "dom", "dom.iterable"],
|
|
804
|
+
jsx: "preserve",
|
|
805
|
+
module: "esnext",
|
|
806
|
+
moduleResolution: "bundler",
|
|
807
|
+
types: ["node"],
|
|
808
|
+
noEmit: true,
|
|
809
|
+
allowJs: true,
|
|
810
|
+
esModuleInterop: true,
|
|
811
|
+
incremental: true,
|
|
812
|
+
plugins: [{ name: "next" }]
|
|
813
|
+
},
|
|
814
|
+
include: ["next-env.d.ts", "src", ".next/types/**/*.ts"],
|
|
815
|
+
exclude: ["node_modules"]
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
function appTypedoc3() {
|
|
819
|
+
return toJson({
|
|
820
|
+
extends: ["../../typedoc.json"],
|
|
821
|
+
entryPoints: ["./src"],
|
|
822
|
+
out: "doc",
|
|
823
|
+
exclude: ["./node_modules/**", "./src/**/*.test.ts"]
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
var nextConfig = `import path from 'node:path'
|
|
827
|
+
|
|
828
|
+
// ESLint runs as its own monorepo target (npm run lint); Next type-checks the build.
|
|
829
|
+
const nextConfig = { output: process.env.NEXT_OUTPUT === 'export' ? 'export' : 'standalone', outputFileTracingRoot: path.join(import.meta.dirname, '..', '..'), eslint: { ignoreDuringBuilds: true } }
|
|
830
|
+
|
|
831
|
+
export default nextConfig
|
|
832
|
+
`;
|
|
833
|
+
function layoutTsx(vars) {
|
|
834
|
+
return `import type { ReactNode } from 'react'
|
|
835
|
+
|
|
836
|
+
export const metadata = { title: '${vars.name}' }
|
|
837
|
+
|
|
838
|
+
// Root layout required by the Next.js App Router; wraps every page.
|
|
839
|
+
export default function RootLayout ({ children }: { children: ReactNode }) {
|
|
840
|
+
return (
|
|
841
|
+
<html lang='en'>
|
|
842
|
+
<body>{ children }</body>
|
|
843
|
+
</html>
|
|
844
|
+
)
|
|
845
|
+
}
|
|
846
|
+
`;
|
|
847
|
+
}
|
|
848
|
+
var pageTsx = `import { greet } from '../greeting'
|
|
849
|
+
|
|
850
|
+
// Home page (App Router server component).
|
|
851
|
+
export default function Home () {
|
|
852
|
+
return (
|
|
853
|
+
<main>
|
|
854
|
+
<h1>{greet('Next.js')}</h1>
|
|
855
|
+
</main>
|
|
856
|
+
)
|
|
857
|
+
}
|
|
858
|
+
`;
|
|
859
|
+
var greetingTs3 = `/**
|
|
860
|
+
* Returns the greeting rendered on the home page.
|
|
861
|
+
*
|
|
862
|
+
* @remarks Shared TS logic \u2014 unit-tested directly (no Next.js test transform needed).
|
|
863
|
+
* @param name - The name to greet.
|
|
864
|
+
* @returns The greeting text.
|
|
865
|
+
* @throws Never - performs no I/O.
|
|
866
|
+
* @typeParam None - this function has no generic type parameters.
|
|
867
|
+
*/
|
|
868
|
+
export function greet (name: string): string {
|
|
869
|
+
return 'Hello from ' + name + '!'
|
|
870
|
+
}
|
|
871
|
+
`;
|
|
872
|
+
var greetingTestTs3 = `import { greet } from './greeting'
|
|
873
|
+
|
|
874
|
+
describe('greet', () => {
|
|
875
|
+
it('greets the given name', () => {
|
|
876
|
+
expect(greet('world')).toBe('Hello from world!')
|
|
877
|
+
})
|
|
878
|
+
})
|
|
879
|
+
`;
|
|
880
|
+
function envFile2(environment) {
|
|
881
|
+
return `NEXT_PUBLIC_ENVIRONMENT=${environment}
|
|
882
|
+
NEXT_PUBLIC_API_URL=https://${environment}.example.com
|
|
883
|
+
`;
|
|
884
|
+
}
|
|
885
|
+
function nextAppFiles(vars) {
|
|
886
|
+
const root = `apps/${vars.name}`;
|
|
887
|
+
const file = (path, content, ownership) => ({ path: `${root}/${path}`, content, ownership });
|
|
888
|
+
return [
|
|
889
|
+
file("package.json", appPackageJson3(vars), "scaffold"),
|
|
890
|
+
file("project.json", appProjectJson3(vars), "tool-owned"),
|
|
891
|
+
file("tsconfig.json", appTsconfig3(), "tool-owned"),
|
|
892
|
+
file("next.config.mjs", nextConfig, "scaffold"),
|
|
893
|
+
file("jest.config.mjs", `import { createConfig } from '../../jest.preset.mjs'
|
|
894
|
+
|
|
895
|
+
export default createConfig('${vars.name}')
|
|
896
|
+
`, "scaffold"),
|
|
897
|
+
file("typedoc.json", appTypedoc3(), "tool-owned"),
|
|
898
|
+
file(".env.dev", envFile2("dev"), "scaffold"),
|
|
899
|
+
file(".env.uat", envFile2("uat"), "scaffold"),
|
|
900
|
+
file(".env.prod", envFile2("prod"), "scaffold"),
|
|
901
|
+
file("src/app/layout.tsx", layoutTsx(vars), "scaffold"),
|
|
902
|
+
file("src/app/page.tsx", pageTsx, "scaffold"),
|
|
903
|
+
file("src/greeting.ts", greetingTs3, "scaffold"),
|
|
904
|
+
file("src/greeting.test.ts", greetingTestTs3, "scaffold"),
|
|
905
|
+
{ path: "tools/next-build.mjs", content: readAsset("scripts/next-build.mjs"), ownership: "tool-owned" }
|
|
906
|
+
];
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/templates/nodeApp.ts
|
|
910
|
+
function appPackageJson4(vars) {
|
|
911
|
+
return toJson({
|
|
912
|
+
name: vars.packageName,
|
|
913
|
+
version: "0.0.0",
|
|
914
|
+
private: true,
|
|
915
|
+
type: "commonjs",
|
|
916
|
+
main: "dist/index.js",
|
|
917
|
+
dependencies: {},
|
|
918
|
+
scripts: {
|
|
919
|
+
build: "tsc -p tsconfig.app.json",
|
|
920
|
+
watch: "tsc -p tsconfig.app.json -w",
|
|
921
|
+
start: "node dist/index.js",
|
|
922
|
+
dev: "tsx watch src/index.ts",
|
|
923
|
+
lint: "eslint . -c ../../eslint.config.mjs",
|
|
924
|
+
test: "jest --collectCoverage",
|
|
925
|
+
doc: "typedoc --tsconfig tsconfig.app.json"
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
function appProjectJson4(vars) {
|
|
930
|
+
const run = (target) => ({
|
|
931
|
+
executor: "nx:run-commands",
|
|
932
|
+
options: { command: `npm run ${target} -w ${vars.packageName}` }
|
|
933
|
+
});
|
|
934
|
+
return toJson({
|
|
935
|
+
name: vars.name,
|
|
936
|
+
$schema: "../../node_modules/nx/schemas/project-schema.json",
|
|
937
|
+
sourceRoot: `apps/${vars.name}/src`,
|
|
938
|
+
projectType: "application",
|
|
939
|
+
tags: [TAGS.nodeApp],
|
|
940
|
+
targets: {
|
|
941
|
+
build: { executor: "nx:run-commands", outputs: ["{projectRoot}/dist"], options: { command: `npm run build -w ${vars.packageName}` } },
|
|
942
|
+
serve: run("start"),
|
|
943
|
+
test: run("test"),
|
|
944
|
+
lint: run("lint"),
|
|
945
|
+
doc: run("doc")
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
function appTsconfig4() {
|
|
950
|
+
return toJson({
|
|
951
|
+
extends: "../../tsconfig.base.json",
|
|
952
|
+
compilerOptions: {
|
|
953
|
+
baseUrl: ".",
|
|
954
|
+
rootDir: ".",
|
|
955
|
+
outDir: "./dist",
|
|
956
|
+
module: "commonjs",
|
|
957
|
+
moduleResolution: "node",
|
|
958
|
+
target: "es2022",
|
|
959
|
+
sourceMap: true,
|
|
960
|
+
declaration: false,
|
|
961
|
+
declarationMap: false,
|
|
962
|
+
esModuleInterop: true
|
|
963
|
+
},
|
|
964
|
+
exclude: ["./coverage/**", "./dist/**", "./doc/**", "./node_modules/**"]
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
function appTsconfigApp2() {
|
|
968
|
+
return toJson({
|
|
969
|
+
extends: "./tsconfig.json",
|
|
970
|
+
compilerOptions: {
|
|
971
|
+
rootDir: "./src",
|
|
972
|
+
noEmit: false,
|
|
973
|
+
sourceMap: true,
|
|
974
|
+
removeComments: false
|
|
975
|
+
},
|
|
976
|
+
exclude: ["./coverage/**", "./dist/**", "./doc/**", "./node_modules/**", "./src/**/*.test.ts"]
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
function appTypedoc4() {
|
|
980
|
+
return toJson({
|
|
981
|
+
extends: ["../../typedoc.json"],
|
|
982
|
+
entryPoints: ["./src"],
|
|
983
|
+
out: "doc",
|
|
984
|
+
exclude: ["./node_modules/**", "./src/**/*.test.ts"]
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
var serverTs = `import { IncomingMessage, ServerResponse } from 'node:http'
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Builds the greeting returned by the server.
|
|
991
|
+
*
|
|
992
|
+
* @remarks Pure string helper \u2014 replace with your own logic.
|
|
993
|
+
* @param name - The name to greet.
|
|
994
|
+
* @returns The greeting text.
|
|
995
|
+
* @throws Never - performs no I/O.
|
|
996
|
+
* @typeParam None - this function has no generic type parameters.
|
|
997
|
+
*/
|
|
998
|
+
export function buildGreeting (name: string): string {
|
|
999
|
+
return 'Hello, ' + name + '!'
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Handles an HTTP request, replying with a plain-text greeting.
|
|
1004
|
+
*
|
|
1005
|
+
* @remarks Swap in Express, Koa, Fastify, Nest or any framework you prefer.
|
|
1006
|
+
* @param request - The incoming HTTP request.
|
|
1007
|
+
* @param response - The HTTP response to write to.
|
|
1008
|
+
* @returns Nothing.
|
|
1009
|
+
* @throws Never - failures surface through the Node HTTP server.
|
|
1010
|
+
* @typeParam None - this function has no generic type parameters.
|
|
1011
|
+
*/
|
|
1012
|
+
export function handleRequest (request: IncomingMessage, response: ServerResponse): void {
|
|
1013
|
+
const requestUrl = new URL(request.url ?? '/', 'http://localhost')
|
|
1014
|
+
const name = requestUrl.searchParams.get('name') ?? 'world'
|
|
1015
|
+
|
|
1016
|
+
response.writeHead(200, { 'content-type': 'text/plain' })
|
|
1017
|
+
response.end(buildGreeting(name))
|
|
1018
|
+
}
|
|
1019
|
+
`;
|
|
1020
|
+
var indexTs3 = `import { createServer } from 'node:http'
|
|
1021
|
+
import { handleRequest } from './server'
|
|
1022
|
+
|
|
1023
|
+
const port = Number(process.env.PORT ?? 3000)
|
|
1024
|
+
|
|
1025
|
+
createServer(handleRequest).listen(port, () => {
|
|
1026
|
+
console.log('Server listening on http://localhost:' + port)
|
|
1027
|
+
})
|
|
1028
|
+
`;
|
|
1029
|
+
var serverTestTs = `import { buildGreeting } from './server'
|
|
1030
|
+
|
|
1031
|
+
describe('buildGreeting', () => {
|
|
1032
|
+
it('greets a name', () => {
|
|
1033
|
+
expect(buildGreeting('world')).toBe('Hello, world!')
|
|
1034
|
+
})
|
|
1035
|
+
})
|
|
1036
|
+
`;
|
|
1037
|
+
function nodeAppFiles(vars) {
|
|
1038
|
+
const root = `apps/${vars.name}`;
|
|
1039
|
+
const file = (path, content, ownership) => ({ path: `${root}/${path}`, content, ownership });
|
|
1040
|
+
return [
|
|
1041
|
+
file("package.json", appPackageJson4(vars), "scaffold"),
|
|
1042
|
+
file("project.json", appProjectJson4(vars), "tool-owned"),
|
|
1043
|
+
file("tsconfig.json", appTsconfig4(), "tool-owned"),
|
|
1044
|
+
file("tsconfig.app.json", appTsconfigApp2(), "tool-owned"),
|
|
1045
|
+
file("jest.config.mjs", `import { createConfig } from '../../jest.preset.mjs'
|
|
1046
|
+
|
|
1047
|
+
export default createConfig('${vars.name}')
|
|
1048
|
+
`, "scaffold"),
|
|
1049
|
+
file("typedoc.json", appTypedoc4(), "tool-owned"),
|
|
1050
|
+
file("src/index.ts", indexTs3, "scaffold"),
|
|
1051
|
+
file("src/server.ts", serverTs, "scaffold"),
|
|
1052
|
+
file("src/server.test.ts", serverTestTs, "scaffold")
|
|
1053
|
+
];
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/engine/registry.ts
|
|
1057
|
+
function registryUrl(registry) {
|
|
1058
|
+
switch (registry.kind) {
|
|
1059
|
+
case "azure-artifacts": {
|
|
1060
|
+
return `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}/npm/registry/`;
|
|
1061
|
+
}
|
|
1062
|
+
case "github-packages": {
|
|
1063
|
+
return "https://npm.pkg.github.com/";
|
|
1064
|
+
}
|
|
1065
|
+
default: {
|
|
1066
|
+
return void 0;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
function npmrcContent(registry, scope) {
|
|
1071
|
+
const scopeName = scope.replace(/^@/, "");
|
|
1072
|
+
const url = registryUrl(registry);
|
|
1073
|
+
const lines = [
|
|
1074
|
+
"registry=https://registry.npmjs.org/",
|
|
1075
|
+
"; ESLint 10 lands ahead of some plugins' peer ranges; accept the resolved tree.",
|
|
1076
|
+
"legacy-peer-deps=true"
|
|
1077
|
+
];
|
|
1078
|
+
if (url) {
|
|
1079
|
+
const host = url.replace(/^https:\/\//, "");
|
|
1080
|
+
lines.push(`@${scopeName}:registry=${url}`, `//${host}:_authToken=\${NODE_AUTH_TOKEN}`);
|
|
1081
|
+
}
|
|
1082
|
+
lines.push("");
|
|
1083
|
+
return lines.join("\n");
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/templates/publishableLib.ts
|
|
1087
|
+
function publishConfig(vars) {
|
|
1088
|
+
const url = vars.registry ? registryUrl(vars.registry) : void 0;
|
|
1089
|
+
return url ? { registry: url } : void 0;
|
|
1090
|
+
}
|
|
1091
|
+
function tsconfig() {
|
|
1092
|
+
return toJson({
|
|
1093
|
+
extends: "../../tsconfig.base.json",
|
|
1094
|
+
compilerOptions: {
|
|
1095
|
+
baseUrl: ".",
|
|
1096
|
+
rootDir: ".",
|
|
1097
|
+
outDir: "./dist",
|
|
1098
|
+
module: "nodenext",
|
|
1099
|
+
moduleResolution: "nodenext",
|
|
1100
|
+
target: "es2024",
|
|
1101
|
+
lib: ["es2024"],
|
|
1102
|
+
noEmit: true,
|
|
1103
|
+
emitDeclarationOnly: true,
|
|
1104
|
+
sourceMap: true,
|
|
1105
|
+
declaration: true,
|
|
1106
|
+
declarationMap: true,
|
|
1107
|
+
removeComments: false,
|
|
1108
|
+
allowSyntheticDefaultImports: true,
|
|
1109
|
+
importHelpers: true,
|
|
1110
|
+
isolatedModules: true
|
|
1111
|
+
},
|
|
1112
|
+
exclude: ["./coverage/**", "./dist/**", "./doc/**", "./node_modules/**"]
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
function tsconfigLib() {
|
|
1116
|
+
return toJson({
|
|
1117
|
+
extends: "./tsconfig.json",
|
|
1118
|
+
compilerOptions: {
|
|
1119
|
+
rootDir: "./src",
|
|
1120
|
+
noEmit: false,
|
|
1121
|
+
emitDeclarationOnly: false,
|
|
1122
|
+
sourceMap: true,
|
|
1123
|
+
declaration: true,
|
|
1124
|
+
declarationMap: true,
|
|
1125
|
+
removeComments: false
|
|
1126
|
+
},
|
|
1127
|
+
exclude: ["./coverage/**", "./dist/**", "./doc/**", "./node_modules/**", "./src/**/*.test.ts"]
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
function typedoc() {
|
|
1131
|
+
return toJson({
|
|
1132
|
+
extends: ["../../typedoc.json"],
|
|
1133
|
+
entryPoints: ["./src"],
|
|
1134
|
+
out: "doc",
|
|
1135
|
+
exclude: ["./node_modules/**", "./src/**/*.test.ts"]
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
function projectJson(vars, buildCommand) {
|
|
1139
|
+
const run = (target) => ({
|
|
1140
|
+
executor: "nx:run-commands",
|
|
1141
|
+
options: { command: `npm run ${target} -w ${vars.packageName}` }
|
|
1142
|
+
});
|
|
1143
|
+
return toJson({
|
|
1144
|
+
name: vars.name,
|
|
1145
|
+
$schema: "../../node_modules/nx/schemas/project-schema.json",
|
|
1146
|
+
sourceRoot: `libs/${vars.name}/src`,
|
|
1147
|
+
projectType: vars.kind === "cli-tool" ? "application" : "library",
|
|
1148
|
+
tags: [TAGS.publishableLib],
|
|
1149
|
+
targets: {
|
|
1150
|
+
build: { executor: "nx:run-commands", outputs: ["{projectRoot}/dist"], options: { command: buildCommand } },
|
|
1151
|
+
test: run("test"),
|
|
1152
|
+
lint: run("lint"),
|
|
1153
|
+
doc: run("doc")
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
var greeterTs2 = `/**
|
|
1158
|
+
* Returns a friendly greeting for the given name.
|
|
1159
|
+
*
|
|
1160
|
+
* @remarks The package's public API entry point.
|
|
1161
|
+
* @param name - The name to greet (surrounding whitespace is trimmed).
|
|
1162
|
+
* @returns The greeting text.
|
|
1163
|
+
* @throws Never - performs no I/O.
|
|
1164
|
+
* @typeParam None - this function has no generic type parameters.
|
|
1165
|
+
*/
|
|
1166
|
+
export function greet (name: string): string {
|
|
1167
|
+
const trimmed = name.trim() // \u2190 breakpoint here works under "Debug Jest (current file)"
|
|
1168
|
+
|
|
1169
|
+
return 'Hello, ' + trimmed + '!'
|
|
1170
|
+
}
|
|
1171
|
+
`;
|
|
1172
|
+
var greeterTestTs2 = `import { greet } from './index'
|
|
1173
|
+
|
|
1174
|
+
describe('greet', () => {
|
|
1175
|
+
it('greets a trimmed name', () => {
|
|
1176
|
+
expect(greet(' Ada ')).toBe('Hello, Ada!')
|
|
1177
|
+
})
|
|
1178
|
+
})
|
|
1179
|
+
`;
|
|
1180
|
+
function distPackageScript() {
|
|
1181
|
+
return {
|
|
1182
|
+
path: "tools/generate-dist-package.mjs",
|
|
1183
|
+
content: readAsset("scripts/generate-dist-package.mjs"),
|
|
1184
|
+
ownership: "tool-owned"
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
function publishableLibFiles(vars) {
|
|
1188
|
+
const root = `libs/${vars.name}`;
|
|
1189
|
+
const buildCommand = `npm run build -w ${vars.packageName}`;
|
|
1190
|
+
const packageJson2 = toJson({
|
|
1191
|
+
name: vars.packageName,
|
|
1192
|
+
version: "0.0.0",
|
|
1193
|
+
type: "commonjs",
|
|
1194
|
+
main: "./src/index.ts",
|
|
1195
|
+
types: "./src/index.ts",
|
|
1196
|
+
publishConfig: publishConfig(vars),
|
|
1197
|
+
monecromanci: { dist: { main: "./index.js", types: "./index.d.ts" } },
|
|
1198
|
+
dependencies: {},
|
|
1199
|
+
scripts: {
|
|
1200
|
+
build: "tsc -p ./tsconfig.lib.json && node ../../tools/generate-dist-package.mjs",
|
|
1201
|
+
test: "jest --collectCoverage",
|
|
1202
|
+
lint: "eslint . -c ../../eslint.config.mjs",
|
|
1203
|
+
doc: "typedoc --tsconfig tsconfig.lib.json",
|
|
1204
|
+
publish: "npm publish ./dist"
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
const file = (path, content, ownership) => ({ path: `${root}/${path}`, content, ownership });
|
|
1208
|
+
return [
|
|
1209
|
+
file("package.json", packageJson2, "scaffold"),
|
|
1210
|
+
file("project.json", projectJson(vars, buildCommand), "tool-owned"),
|
|
1211
|
+
file("tsconfig.json", tsconfig(), "tool-owned"),
|
|
1212
|
+
file("tsconfig.lib.json", tsconfigLib(), "tool-owned"),
|
|
1213
|
+
file("jest.config.mjs", `import { createConfig } from '../../jest.preset.mjs'
|
|
1214
|
+
|
|
1215
|
+
export default createConfig('${vars.name}')
|
|
1216
|
+
`, "scaffold"),
|
|
1217
|
+
file("typedoc.json", typedoc(), "tool-owned"),
|
|
1218
|
+
file("src/index.ts", "export * from './greeter'\n", "scaffold"),
|
|
1219
|
+
file("src/greeter.ts", greeterTs2, "scaffold"),
|
|
1220
|
+
file("src/greeter.test.ts", greeterTestTs2, "scaffold"),
|
|
1221
|
+
distPackageScript()
|
|
1222
|
+
];
|
|
1223
|
+
}
|
|
1224
|
+
var cliMainTs = String.raw`/** Sample CLI entry point. Replace with your own command logic. */
|
|
1225
|
+
function main (argv: string[]): void {
|
|
1226
|
+
const name = argv[0] ?? 'world'
|
|
1227
|
+
process.stdout.write('Hello, ' + name + '!\n')
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
main(process.argv.slice(2))
|
|
1231
|
+
`;
|
|
1232
|
+
var cliMainTestTs = `import { greet } from './greeter'
|
|
1233
|
+
|
|
1234
|
+
describe('greet', () => {
|
|
1235
|
+
it('greets a name', () => {
|
|
1236
|
+
expect(greet('world')).toBe('Hello, world!')
|
|
1237
|
+
})
|
|
1238
|
+
})
|
|
1239
|
+
`;
|
|
1240
|
+
var cliGreeterTs = `/**
|
|
1241
|
+
* Returns the greeting printed by the CLI.
|
|
1242
|
+
*
|
|
1243
|
+
* @remarks Replace with your own command logic.
|
|
1244
|
+
* @param name - The name to greet.
|
|
1245
|
+
* @returns The greeting text.
|
|
1246
|
+
* @throws Never - performs no I/O.
|
|
1247
|
+
* @typeParam None - this function has no generic type parameters.
|
|
1248
|
+
*/
|
|
1249
|
+
export function greet (name: string): string {
|
|
1250
|
+
return 'Hello, ' + name + '!'
|
|
1251
|
+
}
|
|
1252
|
+
`;
|
|
1253
|
+
function cliToolFiles(vars) {
|
|
1254
|
+
const root = `libs/${vars.name}`;
|
|
1255
|
+
const buildCommand = `npm run build -w ${vars.packageName}`;
|
|
1256
|
+
const esbuild = "esbuild src/cli.ts --bundle --platform=node --target=node24 --outfile=dist/cli.js";
|
|
1257
|
+
const packageJson2 = toJson({
|
|
1258
|
+
name: vars.packageName,
|
|
1259
|
+
version: "0.0.0",
|
|
1260
|
+
type: "commonjs",
|
|
1261
|
+
main: "./src/cli.ts",
|
|
1262
|
+
bin: { [vars.name]: "./dist/cli.js" },
|
|
1263
|
+
publishConfig: publishConfig(vars),
|
|
1264
|
+
monecromanci: { dist: { main: "./cli.js", bin: { [vars.name]: "./cli.js" } } },
|
|
1265
|
+
dependencies: {},
|
|
1266
|
+
scripts: {
|
|
1267
|
+
build: `${esbuild} && node ../../tools/generate-dist-package.mjs`,
|
|
1268
|
+
test: "jest --collectCoverage",
|
|
1269
|
+
lint: "eslint . -c ../../eslint.config.mjs",
|
|
1270
|
+
doc: "typedoc --tsconfig tsconfig.lib.json",
|
|
1271
|
+
publish: "npm publish ./dist"
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
const file = (path, content, ownership) => ({ path: `${root}/${path}`, content, ownership });
|
|
1275
|
+
return [
|
|
1276
|
+
file("package.json", packageJson2, "scaffold"),
|
|
1277
|
+
file("project.json", projectJson(vars, buildCommand), "tool-owned"),
|
|
1278
|
+
file("tsconfig.json", tsconfig(), "tool-owned"),
|
|
1279
|
+
file("tsconfig.lib.json", tsconfigLib(), "tool-owned"),
|
|
1280
|
+
file("jest.config.mjs", `import { createConfig } from '../../jest.preset.mjs'
|
|
1281
|
+
|
|
1282
|
+
export default createConfig('${vars.name}')
|
|
1283
|
+
`, "scaffold"),
|
|
1284
|
+
file("typedoc.json", typedoc(), "tool-owned"),
|
|
1285
|
+
file("src/cli.ts", cliMainTs, "scaffold"),
|
|
1286
|
+
file("src/greeter.ts", cliGreeterTs, "scaffold"),
|
|
1287
|
+
file("src/greeter.test.ts", cliMainTestTs, "scaffold"),
|
|
1288
|
+
distPackageScript()
|
|
1289
|
+
];
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// src/templates/reactApp.ts
|
|
1293
|
+
function appPackageJson5(vars) {
|
|
1294
|
+
return toJson({
|
|
1295
|
+
name: vars.packageName,
|
|
1296
|
+
version: "0.0.0",
|
|
1297
|
+
private: true,
|
|
1298
|
+
type: "module",
|
|
1299
|
+
scripts: {
|
|
1300
|
+
dev: "vite",
|
|
1301
|
+
"build:dev": "vite build --mode dev --outDir dist-dev",
|
|
1302
|
+
"build:uat": "vite build --mode uat --outDir dist-uat",
|
|
1303
|
+
"build:prod": "vite build --mode prod --outDir dist-prod",
|
|
1304
|
+
"build:all": "npm run build:dev && npm run build:uat && npm run build:prod",
|
|
1305
|
+
build: "npm run build:dev",
|
|
1306
|
+
preview: "vite preview",
|
|
1307
|
+
lint: "eslint . -c ../../eslint.config.mjs",
|
|
1308
|
+
test: "jest --collectCoverage"
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
function appProjectJson5(vars) {
|
|
1313
|
+
const run = (target) => ({
|
|
1314
|
+
executor: "nx:run-commands",
|
|
1315
|
+
options: { command: `npm run ${target} -w ${vars.packageName}` }
|
|
1316
|
+
});
|
|
1317
|
+
return toJson({
|
|
1318
|
+
name: vars.name,
|
|
1319
|
+
$schema: "../../node_modules/nx/schemas/project-schema.json",
|
|
1320
|
+
sourceRoot: `apps/${vars.name}/src`,
|
|
1321
|
+
projectType: "application",
|
|
1322
|
+
tags: [TAGS.reactApp],
|
|
1323
|
+
targets: {
|
|
1324
|
+
build: {
|
|
1325
|
+
executor: "nx:run-commands",
|
|
1326
|
+
outputs: ["{projectRoot}/dist-dev", "{projectRoot}/dist-uat", "{projectRoot}/dist-prod"],
|
|
1327
|
+
options: { command: `npm run build -w ${vars.packageName}` }
|
|
1328
|
+
},
|
|
1329
|
+
serve: run("dev"),
|
|
1330
|
+
test: run("test"),
|
|
1331
|
+
lint: run("lint")
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
function appTsconfig5() {
|
|
1336
|
+
return toJson({
|
|
1337
|
+
extends: "../../tsconfig.base.json",
|
|
1338
|
+
compilerOptions: {
|
|
1339
|
+
target: "es2022",
|
|
1340
|
+
lib: ["es2022", "DOM", "DOM.Iterable"],
|
|
1341
|
+
module: "esnext",
|
|
1342
|
+
moduleResolution: "bundler",
|
|
1343
|
+
jsx: "react-jsx",
|
|
1344
|
+
types: ["vite/client", "node"],
|
|
1345
|
+
noEmit: true,
|
|
1346
|
+
sourceMap: true,
|
|
1347
|
+
allowSyntheticDefaultImports: true,
|
|
1348
|
+
esModuleInterop: true
|
|
1349
|
+
},
|
|
1350
|
+
include: ["src", "vite.config.ts"]
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
function appTsconfigSpec() {
|
|
1354
|
+
return toJson({
|
|
1355
|
+
extends: "../../tsconfig.jest.json",
|
|
1356
|
+
compilerOptions: {
|
|
1357
|
+
jsx: "react-jsx",
|
|
1358
|
+
module: "commonjs",
|
|
1359
|
+
moduleResolution: "node",
|
|
1360
|
+
types: ["jest", "node"]
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
var viteConfigTs = `import react from '@vitejs/plugin-react'
|
|
1365
|
+
import { defineConfig } from 'vite'
|
|
1366
|
+
|
|
1367
|
+
export default defineConfig({ plugins: [react()], server: { port: 5173 }, build: { sourcemap: true } })
|
|
1368
|
+
`;
|
|
1369
|
+
var viteEnvDts = `/// <reference types="vite/client" />
|
|
1370
|
+
`;
|
|
1371
|
+
function indexHtml2(vars) {
|
|
1372
|
+
return `<!doctype html>
|
|
1373
|
+
<html lang="en">
|
|
1374
|
+
<head>
|
|
1375
|
+
<meta charset="UTF-8" />
|
|
1376
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1377
|
+
<title>${vars.name}</title>
|
|
1378
|
+
</head>
|
|
1379
|
+
<body>
|
|
1380
|
+
<div id="root"></div>
|
|
1381
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
1382
|
+
</body>
|
|
1383
|
+
</html>
|
|
1384
|
+
`;
|
|
1385
|
+
}
|
|
1386
|
+
var mainTsx = `import { StrictMode } from 'react'
|
|
1387
|
+
import { createRoot } from 'react-dom/client'
|
|
1388
|
+
import { App } from './App'
|
|
1389
|
+
|
|
1390
|
+
// Vite exposes the per-environment .env values on import.meta.env (build --mode).
|
|
1391
|
+
const environment = import.meta.env.VITE_ENVIRONMENT ?? 'local'
|
|
1392
|
+
console.info('Starting app in ' + environment + ' mode')
|
|
1393
|
+
|
|
1394
|
+
createRoot(document.querySelector('#root')!).render(
|
|
1395
|
+
<StrictMode>
|
|
1396
|
+
<App />
|
|
1397
|
+
</StrictMode>,
|
|
1398
|
+
)
|
|
1399
|
+
`;
|
|
1400
|
+
var appTsx = `/**
|
|
1401
|
+
* Root application component.
|
|
1402
|
+
*
|
|
1403
|
+
* @remarks The app's UI entry point, rendered by main.tsx.
|
|
1404
|
+
* @returns The rendered application markup.
|
|
1405
|
+
* @throws Never - a pure render.
|
|
1406
|
+
* @typeParam None - this component has no generic type parameters.
|
|
1407
|
+
*/
|
|
1408
|
+
export function App () {
|
|
1409
|
+
return (
|
|
1410
|
+
<main>
|
|
1411
|
+
<h1>Hello from your new app</h1>
|
|
1412
|
+
</main>
|
|
1413
|
+
)
|
|
1414
|
+
}
|
|
1415
|
+
`;
|
|
1416
|
+
var appTestTsx = `import '@testing-library/jest-dom'
|
|
1417
|
+
import { render, screen } from '@testing-library/react'
|
|
1418
|
+
import { App } from './App'
|
|
1419
|
+
|
|
1420
|
+
describe('App', () => {
|
|
1421
|
+
it('renders a heading', () => {
|
|
1422
|
+
render(<App />)
|
|
1423
|
+
expect(screen.getByRole('heading')).toBeInTheDocument()
|
|
1424
|
+
})
|
|
1425
|
+
})
|
|
1426
|
+
`;
|
|
1427
|
+
var jestConfigMjs = (name) => String.raw`import { createConfig } from '../../jest.preset.mjs'
|
|
1428
|
+
|
|
1429
|
+
const base = createConfig('${name}')
|
|
1430
|
+
|
|
1431
|
+
export default { ...base, testEnvironment: 'jsdom', transform: { '^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: './tsconfig.spec.json' }] } }
|
|
1432
|
+
`;
|
|
1433
|
+
function envFile3(environment) {
|
|
1434
|
+
return `VITE_ENVIRONMENT=${environment}
|
|
1435
|
+
VITE_API_URL=https://${environment}.example.com
|
|
1436
|
+
`;
|
|
1437
|
+
}
|
|
1438
|
+
function reactAppFiles(vars) {
|
|
1439
|
+
const root = `apps/${vars.name}`;
|
|
1440
|
+
const file = (path, content, ownership) => ({ path: `${root}/${path}`, content, ownership });
|
|
1441
|
+
return [
|
|
1442
|
+
file("package.json", appPackageJson5(vars), "scaffold"),
|
|
1443
|
+
file("project.json", appProjectJson5(vars), "tool-owned"),
|
|
1444
|
+
file("tsconfig.json", appTsconfig5(), "tool-owned"),
|
|
1445
|
+
file("tsconfig.spec.json", appTsconfigSpec(), "tool-owned"),
|
|
1446
|
+
file("vite.config.ts", viteConfigTs, "scaffold"),
|
|
1447
|
+
file("index.html", indexHtml2(vars), "scaffold"),
|
|
1448
|
+
file("jest.config.mjs", jestConfigMjs(vars.name), "scaffold"),
|
|
1449
|
+
file(".env.dev", envFile3("dev"), "scaffold"),
|
|
1450
|
+
file(".env.uat", envFile3("uat"), "scaffold"),
|
|
1451
|
+
file(".env.prod", envFile3("prod"), "scaffold"),
|
|
1452
|
+
file("src/vite-env.d.ts", viteEnvDts, "scaffold"),
|
|
1453
|
+
file("src/main.tsx", mainTsx, "scaffold"),
|
|
1454
|
+
file("src/App.tsx", appTsx, "scaffold"),
|
|
1455
|
+
file("src/App.test.tsx", appTestTsx, "scaffold")
|
|
1456
|
+
];
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/generators/scaffold.ts
|
|
1460
|
+
var ROOT_DEPENDENCIES = {
|
|
1461
|
+
"function-app": {
|
|
1462
|
+
dependencies: { "@azure/functions": "^4.16.0" }
|
|
1463
|
+
},
|
|
1464
|
+
"node-app": {
|
|
1465
|
+
devDependencies: { tsx: "^4.20.6" }
|
|
1466
|
+
},
|
|
1467
|
+
"vue-app": {
|
|
1468
|
+
dependencies: { vue: "^3.5.13" },
|
|
1469
|
+
devDependencies: { "@vitejs/plugin-vue": "^5.2.4", vite: "^6.0.7" }
|
|
1470
|
+
},
|
|
1471
|
+
"svelte-app": {
|
|
1472
|
+
devDependencies: { "@sveltejs/vite-plugin-svelte": "^5.0.3", svelte: "^5.19.0", vite: "^6.0.7" }
|
|
1473
|
+
},
|
|
1474
|
+
"nextjs-app": {
|
|
1475
|
+
dependencies: { next: "^15.1.4", react: "^19.2.0", "react-dom": "^19.2.0" },
|
|
1476
|
+
devDependencies: { "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "dotenv-cli": "^8.0.0" }
|
|
1477
|
+
},
|
|
1478
|
+
"react-app": {
|
|
1479
|
+
dependencies: { react: "^19.2.0", "react-dom": "^19.2.0" },
|
|
1480
|
+
devDependencies: {
|
|
1481
|
+
"@testing-library/dom": "^10.4.0",
|
|
1482
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
1483
|
+
"@testing-library/react": "^16.1.0",
|
|
1484
|
+
"@types/react": "^19.2.0",
|
|
1485
|
+
"@types/react-dom": "^19.2.0",
|
|
1486
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
1487
|
+
"jest-environment-jsdom": "^30.0.0",
|
|
1488
|
+
vite: "^6.0.7"
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
function filesForKind(kind, vars) {
|
|
1493
|
+
switch (kind) {
|
|
1494
|
+
case "internal-lib": {
|
|
1495
|
+
return internalLibFiles(vars);
|
|
1496
|
+
}
|
|
1497
|
+
case "publishable-lib": {
|
|
1498
|
+
return publishableLibFiles(vars);
|
|
1499
|
+
}
|
|
1500
|
+
case "cli-tool": {
|
|
1501
|
+
return cliToolFiles(vars);
|
|
1502
|
+
}
|
|
1503
|
+
case "function-app": {
|
|
1504
|
+
return functionAppFiles(vars);
|
|
1505
|
+
}
|
|
1506
|
+
case "node-app": {
|
|
1507
|
+
return nodeAppFiles(vars);
|
|
1508
|
+
}
|
|
1509
|
+
case "react-app": {
|
|
1510
|
+
return reactAppFiles(vars);
|
|
1511
|
+
}
|
|
1512
|
+
case "vue-app": {
|
|
1513
|
+
return vueAppFiles(vars);
|
|
1514
|
+
}
|
|
1515
|
+
case "svelte-app": {
|
|
1516
|
+
return svelteAppFiles(vars);
|
|
1517
|
+
}
|
|
1518
|
+
case "nextjs-app": {
|
|
1519
|
+
return nextAppFiles(vars);
|
|
1520
|
+
}
|
|
1521
|
+
default: {
|
|
1522
|
+
throw new Error(`The '${kind}' generator is not implemented yet.`);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function projectFiles(kind, vars) {
|
|
1527
|
+
return filesForKind(kind, vars);
|
|
1528
|
+
}
|
|
1529
|
+
function applyRootDependencies(repoRoot, kind) {
|
|
1530
|
+
const root = ROOT_DEPENDENCIES[kind];
|
|
1531
|
+
if (!root) {
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
const added = [
|
|
1535
|
+
...root.dependencies ? addRootDependencies(repoRoot, root.dependencies, "dependencies") : [],
|
|
1536
|
+
...root.devDependencies ? addRootDependencies(repoRoot, root.devDependencies, "devDependencies") : []
|
|
1537
|
+
];
|
|
1538
|
+
if (added.length > 0) {
|
|
1539
|
+
logger.step(`added root dependencies: ${added.join(", ")}`);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
function generateProject(repoRoot, kind, name, config) {
|
|
1543
|
+
const vars = {
|
|
1544
|
+
kind,
|
|
1545
|
+
name,
|
|
1546
|
+
packageName: `${config.scope}/${name}`,
|
|
1547
|
+
scope: config.scope,
|
|
1548
|
+
registry: config.registry
|
|
1549
|
+
};
|
|
1550
|
+
logger.step(`Adding ${kind} '${name}' (${vars.packageName})`);
|
|
1551
|
+
reportApply(applyFiles(repoRoot, filesForKind(kind, vars)));
|
|
1552
|
+
applyRootDependencies(repoRoot, kind);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/generators/addProject.ts
|
|
1556
|
+
async function runAdd(options) {
|
|
1557
|
+
const repoRoot = process.cwd();
|
|
1558
|
+
if (!isManagedRepo(repoRoot)) {
|
|
1559
|
+
logger.error("No .monecromanci.json found here. Run this from the monorepo root, or create one with `monecromanci new`.");
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const config = loadConfig(repoRoot);
|
|
1563
|
+
if (!config) {
|
|
1564
|
+
logger.error("Could not read .monecromanci.json.");
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const kind = options.type ?? await (0, import_prompts2.select)({
|
|
1568
|
+
message: "What do you want to add?",
|
|
1569
|
+
choices: [
|
|
1570
|
+
{ name: "Internal library", value: "internal-lib" },
|
|
1571
|
+
{ name: "Publishable library", value: "publishable-lib" },
|
|
1572
|
+
{ name: "CLI tool", value: "cli-tool" },
|
|
1573
|
+
{ name: "Azure Function App", value: "function-app" },
|
|
1574
|
+
{ name: "Node.js app (generic server)", value: "node-app" },
|
|
1575
|
+
{ name: "React app", value: "react-app" },
|
|
1576
|
+
{ name: "Vue app", value: "vue-app" },
|
|
1577
|
+
{ name: "Svelte app", value: "svelte-app" },
|
|
1578
|
+
{ name: "Next.js app (full-stack)", value: "nextjs-app" }
|
|
1579
|
+
]
|
|
1580
|
+
});
|
|
1581
|
+
const name = toSlug(options.name ?? await promptText("Project name"));
|
|
1582
|
+
generateProject(repoRoot, kind, name, config);
|
|
1583
|
+
logger.success("Done. Run `npm install` to link the new workspace, then `npm run graph`.");
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/engine/projects.ts
|
|
1587
|
+
var import_node_fs4 = require("fs");
|
|
1588
|
+
var import_node_path6 = require("path");
|
|
1589
|
+
function readTags(projectJson2) {
|
|
1590
|
+
return Array.isArray(projectJson2.tags) ? projectJson2.tags.map(String) : [];
|
|
1591
|
+
}
|
|
1592
|
+
function hasBin(packageJson2) {
|
|
1593
|
+
if (packageJson2.bin) {
|
|
1594
|
+
return true;
|
|
1595
|
+
}
|
|
1596
|
+
const marker = packageJson2.monecromanci;
|
|
1597
|
+
return Boolean(marker?.dist?.bin);
|
|
1598
|
+
}
|
|
1599
|
+
function kindFromProject(projectJson2, packageJson2) {
|
|
1600
|
+
const tags = readTags(projectJson2);
|
|
1601
|
+
if (tags.includes(TAGS.functionApp)) {
|
|
1602
|
+
return "function-app";
|
|
1603
|
+
}
|
|
1604
|
+
if (tags.includes(TAGS.nodeApp)) {
|
|
1605
|
+
return "node-app";
|
|
1606
|
+
}
|
|
1607
|
+
if (tags.includes(TAGS.reactApp)) {
|
|
1608
|
+
return "react-app";
|
|
1609
|
+
}
|
|
1610
|
+
if (tags.includes(TAGS.vueApp)) {
|
|
1611
|
+
return "vue-app";
|
|
1612
|
+
}
|
|
1613
|
+
if (tags.includes(TAGS.svelteApp)) {
|
|
1614
|
+
return "svelte-app";
|
|
1615
|
+
}
|
|
1616
|
+
if (tags.includes(TAGS.nextjsApp)) {
|
|
1617
|
+
return "nextjs-app";
|
|
1618
|
+
}
|
|
1619
|
+
if (tags.includes(TAGS.internalLib)) {
|
|
1620
|
+
return "internal-lib";
|
|
1621
|
+
}
|
|
1622
|
+
if (tags.includes(TAGS.publishableLib)) {
|
|
1623
|
+
return hasBin(packageJson2) ? "cli-tool" : "publishable-lib";
|
|
1624
|
+
}
|
|
1625
|
+
return void 0;
|
|
1626
|
+
}
|
|
1627
|
+
function scanArea(areaDirectory, config) {
|
|
1628
|
+
const projects = [];
|
|
1629
|
+
const entries = (0, import_node_fs4.readdirSync)(areaDirectory, { withFileTypes: true });
|
|
1630
|
+
for (const entry of entries) {
|
|
1631
|
+
if (!entry.isDirectory()) {
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
const projectDirectory = (0, import_node_path6.join)(areaDirectory, entry.name);
|
|
1635
|
+
const projectJson2 = readJsonSafe((0, import_node_path6.join)(projectDirectory, "project.json"), {});
|
|
1636
|
+
const packageJson2 = readJsonSafe((0, import_node_path6.join)(projectDirectory, "package.json"), {});
|
|
1637
|
+
const kind = kindFromProject(projectJson2, packageJson2);
|
|
1638
|
+
if (!kind) {
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
const packageName = typeof packageJson2.name === "string" ? packageJson2.name : `${config.scope}/${entry.name}`;
|
|
1642
|
+
projects.push({ kind, name: entry.name, packageName, scope: config.scope, registry: config.registry });
|
|
1643
|
+
}
|
|
1644
|
+
return projects;
|
|
1645
|
+
}
|
|
1646
|
+
function discoverProjects(repoRoot, config) {
|
|
1647
|
+
const projects = [];
|
|
1648
|
+
for (const area of ["apps", "libs"]) {
|
|
1649
|
+
const areaDirectory = (0, import_node_path6.join)(repoRoot, area);
|
|
1650
|
+
if ((0, import_node_fs4.existsSync)(areaDirectory)) {
|
|
1651
|
+
projects.push(...scanArea(areaDirectory, config));
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return projects;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/engine/sync.ts
|
|
1658
|
+
var import_node_path7 = require("path");
|
|
1659
|
+
function checkFile(repoRoot, spec) {
|
|
1660
|
+
const absolute = (0, import_node_path7.join)(repoRoot, spec.path);
|
|
1661
|
+
if (!(0, import_node_fs2.existsSync)(absolute)) {
|
|
1662
|
+
return "missing";
|
|
1663
|
+
}
|
|
1664
|
+
return readTextSafe(absolute) === spec.content ? "ok" : "drift";
|
|
1665
|
+
}
|
|
1666
|
+
function syncToolOwned(repoRoot, specs, shouldApply) {
|
|
1667
|
+
const report = { ok: [], missing: [], drift: [], fixed: [] };
|
|
1668
|
+
for (const spec of specs) {
|
|
1669
|
+
if (spec.ownership !== "tool-owned") {
|
|
1670
|
+
continue;
|
|
1671
|
+
}
|
|
1672
|
+
const status = checkFile(repoRoot, spec);
|
|
1673
|
+
if (status === "ok") {
|
|
1674
|
+
report.ok.push(spec.path);
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
if (status === "missing") {
|
|
1678
|
+
report.missing.push(spec.path);
|
|
1679
|
+
} else {
|
|
1680
|
+
report.drift.push(spec.path);
|
|
1681
|
+
}
|
|
1682
|
+
if (shouldApply) {
|
|
1683
|
+
writeFileEnsured((0, import_node_path7.join)(repoRoot, spec.path), spec.content);
|
|
1684
|
+
report.fixed.push(spec.path);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
return report;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// package.json
|
|
1691
|
+
var package_default = {
|
|
1692
|
+
name: "monecromanci",
|
|
1693
|
+
version: "0.0.0",
|
|
1694
|
+
description: "MoNecromanCI \u2014 interactive CLI to summon, conjure, raise and validate NX monorepos (Azure DevOps & GitHub Actions): function/node apps, React/Vue/Svelte/Next.js apps, internal/publishable libs, CLI tools.",
|
|
1695
|
+
bin: {
|
|
1696
|
+
monecromanci: "./dist/cli.js",
|
|
1697
|
+
mnci: "./dist/cli.js"
|
|
1698
|
+
},
|
|
1699
|
+
files: [
|
|
1700
|
+
"dist"
|
|
1701
|
+
],
|
|
1702
|
+
engines: {
|
|
1703
|
+
node: "^22.13.0 || >=24"
|
|
1704
|
+
},
|
|
1705
|
+
scripts: {
|
|
1706
|
+
build: "tsup && node scripts/copyAssets.mjs",
|
|
1707
|
+
dev: "tsup --watch",
|
|
1708
|
+
lint: "eslint .",
|
|
1709
|
+
"lint:fix": "eslint . --fix",
|
|
1710
|
+
test: "jest",
|
|
1711
|
+
"test:cov": "jest --coverage",
|
|
1712
|
+
typecheck: "tsc --noEmit",
|
|
1713
|
+
start: "node dist/cli.js"
|
|
1714
|
+
},
|
|
1715
|
+
keywords: [
|
|
1716
|
+
"nx",
|
|
1717
|
+
"monorepo",
|
|
1718
|
+
"azure-devops",
|
|
1719
|
+
"cli",
|
|
1720
|
+
"scaffold",
|
|
1721
|
+
"typescript"
|
|
1722
|
+
],
|
|
1723
|
+
author: "Eduardo Russo",
|
|
1724
|
+
license: "MIT",
|
|
1725
|
+
dependencies: {
|
|
1726
|
+
"@inquirer/prompts": "^8.5.2",
|
|
1727
|
+
commander: "^15.0.0"
|
|
1728
|
+
},
|
|
1729
|
+
devDependencies: {
|
|
1730
|
+
"@eslint/markdown": "^8.0.2",
|
|
1731
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
1732
|
+
"@types/jest": "^30.0.0",
|
|
1733
|
+
"@types/node": "^26.0.1",
|
|
1734
|
+
eslint: "^10.6.0",
|
|
1735
|
+
"eslint-plugin-jest": "^29.15.3",
|
|
1736
|
+
"eslint-plugin-jsonc": "^3.2.0",
|
|
1737
|
+
"eslint-plugin-n": "^18.2.1",
|
|
1738
|
+
"eslint-plugin-promise": "^7.3.0",
|
|
1739
|
+
"eslint-plugin-react": "^7.37.5",
|
|
1740
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
1741
|
+
"eslint-plugin-react-refresh": "^0.5.3",
|
|
1742
|
+
"eslint-plugin-tsdoc": "^0.5.2",
|
|
1743
|
+
"eslint-plugin-tsdoc-require-2": "^1.2.3",
|
|
1744
|
+
"eslint-plugin-unicorn": "^69.0.0",
|
|
1745
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
1746
|
+
"eslint-plugin-yml": "^3.5.0",
|
|
1747
|
+
globals: "^17.7.0",
|
|
1748
|
+
jest: "^30.4.2",
|
|
1749
|
+
"ts-jest": "^29.4.11",
|
|
1750
|
+
tsup: "^8.5.1",
|
|
1751
|
+
typescript: "^6.0.3",
|
|
1752
|
+
"typescript-eslint": "^8.62.0"
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
|
|
1756
|
+
// src/templates/monorepo.ts
|
|
1757
|
+
var sharedDependency = (name) => package_default.devDependencies[name];
|
|
1758
|
+
var DEV_DEPENDENCIES = {
|
|
1759
|
+
"@commitlint/cli": "^21.1.0",
|
|
1760
|
+
"@commitlint/config-conventional": "^21.1.0",
|
|
1761
|
+
"@eslint/markdown": sharedDependency("@eslint/markdown"),
|
|
1762
|
+
"@stylistic/eslint-plugin": sharedDependency("@stylistic/eslint-plugin"),
|
|
1763
|
+
"@types/jest": sharedDependency("@types/jest"),
|
|
1764
|
+
"@types/node": sharedDependency("@types/node"),
|
|
1765
|
+
esbuild: "^0.28.1",
|
|
1766
|
+
eslint: sharedDependency("eslint"),
|
|
1767
|
+
"eslint-plugin-jest": sharedDependency("eslint-plugin-jest"),
|
|
1768
|
+
"eslint-plugin-jsonc": sharedDependency("eslint-plugin-jsonc"),
|
|
1769
|
+
"eslint-plugin-n": sharedDependency("eslint-plugin-n"),
|
|
1770
|
+
"eslint-plugin-promise": sharedDependency("eslint-plugin-promise"),
|
|
1771
|
+
"eslint-plugin-react": sharedDependency("eslint-plugin-react"),
|
|
1772
|
+
"eslint-plugin-react-hooks": sharedDependency("eslint-plugin-react-hooks"),
|
|
1773
|
+
"eslint-plugin-react-refresh": sharedDependency("eslint-plugin-react-refresh"),
|
|
1774
|
+
"eslint-plugin-tsdoc": sharedDependency("eslint-plugin-tsdoc"),
|
|
1775
|
+
"eslint-plugin-tsdoc-require-2": sharedDependency("eslint-plugin-tsdoc-require-2"),
|
|
1776
|
+
"eslint-plugin-unicorn": sharedDependency("eslint-plugin-unicorn"),
|
|
1777
|
+
"eslint-plugin-unused-imports": sharedDependency("eslint-plugin-unused-imports"),
|
|
1778
|
+
"eslint-plugin-yml": sharedDependency("eslint-plugin-yml"),
|
|
1779
|
+
globals: sharedDependency("globals"),
|
|
1780
|
+
husky: "^9.1.7",
|
|
1781
|
+
jest: sharedDependency("jest"),
|
|
1782
|
+
"jest-junit": "^17.0.0",
|
|
1783
|
+
nx: "^23.0.1",
|
|
1784
|
+
"ts-jest": sharedDependency("ts-jest"),
|
|
1785
|
+
"tsc-alias": "^1.8.17",
|
|
1786
|
+
tslib: "^2.8.1",
|
|
1787
|
+
typedoc: "^0.28.19",
|
|
1788
|
+
"typedoc-plugin-missing-exports": "^4.1.3",
|
|
1789
|
+
typescript: sharedDependency("typescript"),
|
|
1790
|
+
"typescript-eslint": sharedDependency("typescript-eslint")
|
|
1791
|
+
};
|
|
1792
|
+
function packageJson(vars) {
|
|
1793
|
+
return toJson({
|
|
1794
|
+
name: vars.workspaceName,
|
|
1795
|
+
version: "0.0.0",
|
|
1796
|
+
private: true,
|
|
1797
|
+
license: "UNLICENSED",
|
|
1798
|
+
workspaces: ["apps/*", "libs/*"],
|
|
1799
|
+
scripts: {
|
|
1800
|
+
build: "nx run-many -t build --all",
|
|
1801
|
+
"build:affected": "nx affected -t build",
|
|
1802
|
+
lint: "nx run-many -t lint --all",
|
|
1803
|
+
"lint:affected": "nx affected -t lint",
|
|
1804
|
+
test: "nx run-many -t test --all",
|
|
1805
|
+
"test:affected": "nx affected -t test",
|
|
1806
|
+
doc: "nx run-many -t doc --all",
|
|
1807
|
+
"doc:affected": "nx affected -t doc",
|
|
1808
|
+
affected: "nx affected -t lint,test,build",
|
|
1809
|
+
projects: "nx show projects",
|
|
1810
|
+
graph: "nx graph",
|
|
1811
|
+
"nx:reset": "nx reset",
|
|
1812
|
+
"pipeline:plan": "node .build-templates/01-preparation.mjs",
|
|
1813
|
+
"pipeline:package": "node .build-templates/03-package-apps.mjs --dry-run",
|
|
1814
|
+
release: "nx release",
|
|
1815
|
+
"release:version": "nx release version",
|
|
1816
|
+
"release:publish": "nx release publish",
|
|
1817
|
+
prepare: "husky"
|
|
1818
|
+
},
|
|
1819
|
+
dependencies: {
|
|
1820
|
+
tslib: "^2.8.1"
|
|
1821
|
+
},
|
|
1822
|
+
devDependencies: DEV_DEPENDENCIES,
|
|
1823
|
+
engines: {
|
|
1824
|
+
node: `>=${vars.nodeVersion}`
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
function nxJson(vars) {
|
|
1829
|
+
return toJson({
|
|
1830
|
+
$schema: "./node_modules/nx/schemas/nx-schema.json",
|
|
1831
|
+
workspaceLayout: { appsDir: "apps", libsDir: "libs" },
|
|
1832
|
+
defaultBase: vars.defaultBase,
|
|
1833
|
+
namedInputs: {
|
|
1834
|
+
sharedGlobals: [
|
|
1835
|
+
"{workspaceRoot}/package.json",
|
|
1836
|
+
"{workspaceRoot}/package-lock.json",
|
|
1837
|
+
"{workspaceRoot}/nx.json",
|
|
1838
|
+
"{workspaceRoot}/tsconfig.base.json",
|
|
1839
|
+
"{workspaceRoot}/jest.preset.mjs",
|
|
1840
|
+
"{workspaceRoot}/eslint.config.mjs"
|
|
1841
|
+
],
|
|
1842
|
+
default: ["{projectRoot}/**/*", "sharedGlobals"],
|
|
1843
|
+
production: [
|
|
1844
|
+
"default",
|
|
1845
|
+
"!{projectRoot}/coverage/**",
|
|
1846
|
+
"!{projectRoot}/dist/**",
|
|
1847
|
+
"!{projectRoot}/doc/**",
|
|
1848
|
+
"!{projectRoot}/**/*.test.ts",
|
|
1849
|
+
"!{projectRoot}/src/_jest/**"
|
|
1850
|
+
]
|
|
1851
|
+
},
|
|
1852
|
+
targetDefaults: {
|
|
1853
|
+
build: { dependsOn: ["^build"], inputs: ["production", "^production"], cache: true },
|
|
1854
|
+
lint: { inputs: ["default", "^production"], cache: true },
|
|
1855
|
+
test: { dependsOn: ["build"], inputs: ["default", "^production"], cache: true },
|
|
1856
|
+
doc: { inputs: ["production", "^production"], cache: true }
|
|
1857
|
+
},
|
|
1858
|
+
release: {
|
|
1859
|
+
projectsRelationship: "independent",
|
|
1860
|
+
projects: ["tag:type:publishable-lib"],
|
|
1861
|
+
releaseTagPattern: "{projectName}@{version}",
|
|
1862
|
+
version: { conventionalCommits: true },
|
|
1863
|
+
changelog: { projectChangelogs: true }
|
|
1864
|
+
},
|
|
1865
|
+
analytics: false
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
function tsconfigBase() {
|
|
1869
|
+
return toJson({
|
|
1870
|
+
$schema: "https://json.schemastore.org/tsconfig",
|
|
1871
|
+
compilerOptions: {
|
|
1872
|
+
ignoreDeprecations: "6.0",
|
|
1873
|
+
target: "es2024",
|
|
1874
|
+
types: ["jest", "node"],
|
|
1875
|
+
sourceMap: true,
|
|
1876
|
+
declaration: true,
|
|
1877
|
+
declarationMap: true,
|
|
1878
|
+
removeComments: false,
|
|
1879
|
+
forceConsistentCasingInFileNames: true,
|
|
1880
|
+
isolatedModules: true,
|
|
1881
|
+
noFallthroughCasesInSwitch: true,
|
|
1882
|
+
noUnusedLocals: true,
|
|
1883
|
+
noUnusedParameters: true,
|
|
1884
|
+
resolveJsonModule: true,
|
|
1885
|
+
skipLibCheck: true,
|
|
1886
|
+
strict: true,
|
|
1887
|
+
strictNullChecks: true,
|
|
1888
|
+
strictPropertyInitialization: false
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
function tsconfigJest() {
|
|
1893
|
+
return toJson({
|
|
1894
|
+
extends: "./tsconfig.base.json",
|
|
1895
|
+
compilerOptions: {
|
|
1896
|
+
target: "es2022",
|
|
1897
|
+
module: "commonjs",
|
|
1898
|
+
moduleResolution: "node",
|
|
1899
|
+
noEmit: false,
|
|
1900
|
+
emitDeclarationOnly: false,
|
|
1901
|
+
declaration: false,
|
|
1902
|
+
declarationMap: false,
|
|
1903
|
+
sourceMap: true,
|
|
1904
|
+
esModuleInterop: true
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
function typedocJson() {
|
|
1909
|
+
return toJson({
|
|
1910
|
+
$schema: "https://typedoc.org/schema.json",
|
|
1911
|
+
entryPointStrategy: "expand",
|
|
1912
|
+
plugin: ["typedoc-plugin-missing-exports"],
|
|
1913
|
+
excludePrivate: false,
|
|
1914
|
+
categorizeByGroup: true,
|
|
1915
|
+
cleanOutputDir: true
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
var jestConfigMjs2 = String.raw`import { globSync } from 'node:fs'
|
|
1919
|
+
|
|
1920
|
+
const projects = globSync('{libs,apps}/*/jest.config.mjs').map((path) => path.replaceAll('\\', '/'))
|
|
1921
|
+
|
|
1922
|
+
export default {
|
|
1923
|
+
projects: projects.length > 0 ? projects : ['<rootDir>'],
|
|
1924
|
+
maxWorkers: '75%',
|
|
1925
|
+
}
|
|
1926
|
+
`;
|
|
1927
|
+
var jestPresetMjs = String.raw`/** Shared Jest preset — generated by MoNecromanCI. Re-sync with 'monecromanci doctor'. */
|
|
1928
|
+
export function createConfig (projectName) {
|
|
1929
|
+
return {
|
|
1930
|
+
displayName: projectName,
|
|
1931
|
+
testEnvironment: 'node',
|
|
1932
|
+
rootDir: '.',
|
|
1933
|
+
roots: ['<rootDir>/src'],
|
|
1934
|
+
setupFilesAfterEnv: [
|
|
1935
|
+
'<rootDir>/../../jest.setup.mjs',
|
|
1936
|
+
'<rootDir>/../../jest.clear.mjs',
|
|
1937
|
+
],
|
|
1938
|
+
transform: {
|
|
1939
|
+
'^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/../../tsconfig.jest.json' }],
|
|
1940
|
+
},
|
|
1941
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'json'],
|
|
1942
|
+
collectCoverageFrom: [
|
|
1943
|
+
'<rootDir>/src/**/*.ts',
|
|
1944
|
+
'!<rootDir>/src/**/*.d.ts',
|
|
1945
|
+
'!<rootDir>/src/index.ts',
|
|
1946
|
+
],
|
|
1947
|
+
coverageProvider: 'v8',
|
|
1948
|
+
coverageDirectory: './coverage',
|
|
1949
|
+
coverageReporters: ['text', 'cobertura', 'html', 'lcov'],
|
|
1950
|
+
reporters: [
|
|
1951
|
+
'default',
|
|
1952
|
+
['jest-junit', { outputDirectory: './coverage', outputName: 'test-results.xml' }],
|
|
1953
|
+
],
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
`;
|
|
1957
|
+
var jestSetupMjs = `process.env.TZ = 'UTC'
|
|
1958
|
+
|
|
1959
|
+
beforeAll(() => {
|
|
1960
|
+
jest.useFakeTimers()
|
|
1961
|
+
jest.setSystemTime(new Date('2000-01-01T00:00:00.000Z'))
|
|
1962
|
+
})
|
|
1963
|
+
|
|
1964
|
+
afterAll(() => {
|
|
1965
|
+
jest.useRealTimers()
|
|
1966
|
+
})
|
|
1967
|
+
`;
|
|
1968
|
+
var jestClearMjs = `afterEach(() => {
|
|
1969
|
+
jest.restoreAllMocks()
|
|
1970
|
+
jest.clearAllMocks()
|
|
1971
|
+
jest.resetModules()
|
|
1972
|
+
})
|
|
1973
|
+
`;
|
|
1974
|
+
function npmrc(vars) {
|
|
1975
|
+
return npmrcContent(vars.registry, vars.scope);
|
|
1976
|
+
}
|
|
1977
|
+
var editorconfig = `root = true
|
|
1978
|
+
|
|
1979
|
+
[*]
|
|
1980
|
+
charset = utf-8
|
|
1981
|
+
end_of_line = lf
|
|
1982
|
+
indent_style = space
|
|
1983
|
+
indent_size = 2
|
|
1984
|
+
insert_final_newline = true
|
|
1985
|
+
trim_trailing_whitespace = true
|
|
1986
|
+
|
|
1987
|
+
[*.md]
|
|
1988
|
+
trim_trailing_whitespace = false
|
|
1989
|
+
`;
|
|
1990
|
+
var gitignore = `node_modules/
|
|
1991
|
+
dist/
|
|
1992
|
+
dist-dev/
|
|
1993
|
+
dist-uat/
|
|
1994
|
+
dist-prod/
|
|
1995
|
+
coverage/
|
|
1996
|
+
doc/
|
|
1997
|
+
.nx/
|
|
1998
|
+
tmp/
|
|
1999
|
+
.azurite/
|
|
2000
|
+
.next/
|
|
2001
|
+
next-env.d.ts
|
|
2002
|
+
.pipeline-out/
|
|
2003
|
+
.pipeline-staging/
|
|
2004
|
+
*.log
|
|
2005
|
+
.DS_Store
|
|
2006
|
+
local.settings.json
|
|
2007
|
+
`;
|
|
2008
|
+
var commitlintConfigMjs = `export default {
|
|
2009
|
+
extends: ['@commitlint/config-conventional'],
|
|
2010
|
+
}
|
|
2011
|
+
`;
|
|
2012
|
+
var huskyCommitMessage = `npx --no -- commitlint --edit "$1"
|
|
2013
|
+
`;
|
|
2014
|
+
function readme(vars) {
|
|
2015
|
+
return `# ${vars.displayName}
|
|
2016
|
+
|
|
2017
|
+
NX monorepo generated by [MoNecromanCI](https://github.com/russoedu/monecromanci).
|
|
2018
|
+
|
|
2019
|
+
## Common commands
|
|
2020
|
+
|
|
2021
|
+
\`\`\`sh
|
|
2022
|
+
npm run build # build all projects
|
|
2023
|
+
npm run test # run all tests
|
|
2024
|
+
npm run lint # lint everything
|
|
2025
|
+
npm run affected # lint + test + build only what changed
|
|
2026
|
+
npm run graph # open the project graph
|
|
2027
|
+
\`\`\`
|
|
2028
|
+
|
|
2029
|
+
## Debugging
|
|
2030
|
+
|
|
2031
|
+
Open \`${vars.displayName}.code-workspace\` in VSCode. Use the **Run and Debug** panel:
|
|
2032
|
+
breakpoints work in \`.ts\` test files (and step into internal libs). The
|
|
2033
|
+
\`Orta.vscode-jest\` extension also adds a **Debug** lens above each test.
|
|
2034
|
+
|
|
2035
|
+
## Adding projects
|
|
2036
|
+
|
|
2037
|
+
\`\`\`sh
|
|
2038
|
+
npx monecromanci add # (alias: conjure) function-app | node-app | react-app | vue-app | svelte-app | nextjs-app | internal-lib | publishable-lib | cli-tool
|
|
2039
|
+
\`\`\`
|
|
2040
|
+
`;
|
|
2041
|
+
}
|
|
2042
|
+
function registryLabelFor(registry) {
|
|
2043
|
+
switch (registry.kind) {
|
|
2044
|
+
case "azure-artifacts": {
|
|
2045
|
+
return "the Azure Artifacts feed `" + registry.artifactsFeed + "`";
|
|
2046
|
+
}
|
|
2047
|
+
case "github-packages": {
|
|
2048
|
+
return "GitHub Packages";
|
|
2049
|
+
}
|
|
2050
|
+
default: {
|
|
2051
|
+
return "the public npm registry";
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
function nxReleaseDocument(vars) {
|
|
2056
|
+
const registryLabel = registryLabelFor(vars.registry);
|
|
2057
|
+
return `# Releasing publishable libraries & CLI tools
|
|
2058
|
+
|
|
2059
|
+
This monorepo uses **\`nx release\`** with **independent** versioning driven by
|
|
2060
|
+
**Conventional Commits**. Only projects tagged \`type:publishable-lib\` (libraries
|
|
2061
|
+
and CLI tools) are released; internal libs and apps are never published.
|
|
2062
|
+
|
|
2063
|
+
## How versions are decided (auto-bump)
|
|
2064
|
+
|
|
2065
|
+
You do **not** hand-edit \`version\` in any \`package.json\`. \`nx release\` reads the
|
|
2066
|
+
Conventional Commit messages since each project's last release tag and bumps:
|
|
2067
|
+
|
|
2068
|
+
| Commit type | Bump |
|
|
2069
|
+
| ---------------------- | ------ |
|
|
2070
|
+
| \`fix: \u2026\` | patch |
|
|
2071
|
+
| \`feat: \u2026\` | minor |
|
|
2072
|
+
| \`feat!: \u2026\` / \`BREAKING CHANGE\` | major |
|
|
2073
|
+
|
|
2074
|
+
Commit messages are enforced by commitlint (\`commitlint.config.mjs\`) via a husky
|
|
2075
|
+
\`commit-msg\` hook, so the history stays releasable. Scope a commit to a project
|
|
2076
|
+
with \`fix(my-lib): \u2026\`.
|
|
2077
|
+
|
|
2078
|
+
## Local commands
|
|
2079
|
+
|
|
2080
|
+
\`\`\`sh
|
|
2081
|
+
npm run release # interactive: version + changelog + (optional) publish
|
|
2082
|
+
npm run release:version # bump versions + write changelogs from commits
|
|
2083
|
+
npm run release:publish # publish what changed to the configured registry
|
|
2084
|
+
npx nx release --dry-run # preview everything, change nothing
|
|
2085
|
+
\`\`\`
|
|
2086
|
+
|
|
2087
|
+
## What gets published
|
|
2088
|
+
|
|
2089
|
+
\`build\` emits \`dist/\` and runs \`tools/generate-dist-package.mjs\`, which writes a
|
|
2090
|
+
correct \`dist/package.json\`: it resolves real dependency versions from the **root**
|
|
2091
|
+
package.json (all deps live there) and from internal workspace packages. This is
|
|
2092
|
+
why published packages declare their dependencies even though project
|
|
2093
|
+
\`package.json\` files keep \`dependencies: {}\`. Publishing runs \`npm publish ./dist\`.
|
|
2094
|
+
|
|
2095
|
+
## First release
|
|
2096
|
+
|
|
2097
|
+
For a project that has never been released, set its starting version once:
|
|
2098
|
+
|
|
2099
|
+
\`\`\`sh
|
|
2100
|
+
npx nx release version 1.0.0 --projects=my-lib --first-release
|
|
2101
|
+
\`\`\`
|
|
2102
|
+
|
|
2103
|
+
## CI
|
|
2104
|
+
|
|
2105
|
+
On \`${vars.defaultBase}\` (non-PR builds), CI runs \`nx release version --yes\`
|
|
2106
|
+
then publishes affected publishable projects to ${registryLabel}. See the publish
|
|
2107
|
+
step (\`04-publish-libs\`, or the GitHub Actions \`publish\` job).
|
|
2108
|
+
`;
|
|
2109
|
+
}
|
|
2110
|
+
function codeWorkspace(vars) {
|
|
2111
|
+
return toJson({
|
|
2112
|
+
folders: [{ path: ".", name: vars.displayName }],
|
|
2113
|
+
settings: {
|
|
2114
|
+
"eslint.useFlatConfig": true,
|
|
2115
|
+
"eslint.validate": ["javascript", "typescript", "typescriptreact", "json", "jsonc", "json5", "yaml", "markdown"],
|
|
2116
|
+
"files.exclude": {
|
|
2117
|
+
"**/.nx": true,
|
|
2118
|
+
"**/node_modules": true,
|
|
2119
|
+
"**/coverage": true,
|
|
2120
|
+
"**/.azurite": true,
|
|
2121
|
+
tmp: true
|
|
2122
|
+
},
|
|
2123
|
+
"typescript.tsdk": "node_modules/typescript/lib",
|
|
2124
|
+
"jest.runMode": "on-demand"
|
|
2125
|
+
},
|
|
2126
|
+
extensions: {
|
|
2127
|
+
recommendations: [
|
|
2128
|
+
"dbaeumer.vscode-eslint",
|
|
2129
|
+
"orta.vscode-jest",
|
|
2130
|
+
"ms-azuretools.vscode-azurefunctions",
|
|
2131
|
+
"ms-edgedevtools.vscode-edge-devtools"
|
|
2132
|
+
]
|
|
2133
|
+
},
|
|
2134
|
+
// NOTE: launch/tasks are TOP-LEVEL workspace keys (NOT under settings) so VSCode surfaces them.
|
|
2135
|
+
launch: {
|
|
2136
|
+
version: "0.2.0",
|
|
2137
|
+
configurations: [
|
|
2138
|
+
// --- breakpoint-capable debug configs ---
|
|
2139
|
+
{
|
|
2140
|
+
name: "Debug Jest (current file)",
|
|
2141
|
+
type: "node",
|
|
2142
|
+
request: "launch",
|
|
2143
|
+
program: "${workspaceFolder}/node_modules/jest/bin/jest.js",
|
|
2144
|
+
args: ["--runInBand", "--watchAll=false", "--runTestsByPath", "${relativeFile}"],
|
|
2145
|
+
cwd: "${workspaceFolder}",
|
|
2146
|
+
console: "integratedTerminal",
|
|
2147
|
+
internalConsoleOptions: "neverOpen",
|
|
2148
|
+
disableOptimisticBPs: true,
|
|
2149
|
+
resolveSourceMapLocations: null,
|
|
2150
|
+
sourceMaps: true
|
|
2151
|
+
},
|
|
2152
|
+
{
|
|
2153
|
+
name: "Debug Jest (all)",
|
|
2154
|
+
type: "node",
|
|
2155
|
+
request: "launch",
|
|
2156
|
+
program: "${workspaceFolder}/node_modules/jest/bin/jest.js",
|
|
2157
|
+
args: ["--runInBand", "--watchAll=false"],
|
|
2158
|
+
cwd: "${workspaceFolder}",
|
|
2159
|
+
console: "integratedTerminal",
|
|
2160
|
+
internalConsoleOptions: "neverOpen",
|
|
2161
|
+
disableOptimisticBPs: true,
|
|
2162
|
+
resolveSourceMapLocations: null,
|
|
2163
|
+
sourceMaps: true
|
|
2164
|
+
},
|
|
2165
|
+
{
|
|
2166
|
+
// Function App: run `func start` (inspects via local.settings.json), then attach.
|
|
2167
|
+
// Node app: run `node --inspect=9229 dist/index.js` (after build), then attach;
|
|
2168
|
+
// or run `npm run dev -w <app>` in a JavaScript Debug Terminal for source-level tsx.
|
|
2169
|
+
name: "Debug Function/Node App (attach :9229)",
|
|
2170
|
+
type: "node",
|
|
2171
|
+
request: "attach",
|
|
2172
|
+
port: 9229,
|
|
2173
|
+
restart: true,
|
|
2174
|
+
sourceMaps: true,
|
|
2175
|
+
resolveSourceMapLocations: null,
|
|
2176
|
+
outFiles: ["${workspaceFolder}/apps/*/dist/**/*.js"],
|
|
2177
|
+
skipFiles: ["<node_internals>/**"]
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
// Start the dev server first (`npm run dev -w <app>`), then launch the browser.
|
|
2181
|
+
// Or use the JavaScript Debug Terminal: run `npm run dev -w <app>` there.
|
|
2182
|
+
name: "Debug React/Vue/Svelte (Edge)",
|
|
2183
|
+
type: "msedge",
|
|
2184
|
+
request: "launch",
|
|
2185
|
+
url: "http://localhost:5173",
|
|
2186
|
+
webRoot: "${workspaceFolder}",
|
|
2187
|
+
sourceMaps: true,
|
|
2188
|
+
resolveSourceMapLocations: null
|
|
2189
|
+
},
|
|
2190
|
+
{
|
|
2191
|
+
// Next.js dev server runs on :3000. For server-side breakpoints, run
|
|
2192
|
+
// `npm run dev -w <app>` in a JavaScript Debug Terminal instead.
|
|
2193
|
+
name: "Debug Next.js (Edge)",
|
|
2194
|
+
type: "msedge",
|
|
2195
|
+
request: "launch",
|
|
2196
|
+
url: "http://localhost:3000",
|
|
2197
|
+
webRoot: "${workspaceFolder}",
|
|
2198
|
+
sourceMaps: true,
|
|
2199
|
+
resolveSourceMapLocations: null
|
|
2200
|
+
},
|
|
2201
|
+
// --- convenience run configs (no breakpoints; quick npm scripts) ---
|
|
2202
|
+
{ name: "Run: build (all)", type: "node-terminal", request: "launch", command: "npm run build" },
|
|
2203
|
+
{ name: "Run: build (affected)", type: "node-terminal", request: "launch", command: "npm run build:affected" },
|
|
2204
|
+
{ name: "Run: test (all)", type: "node-terminal", request: "launch", command: "npm run test" },
|
|
2205
|
+
{ name: "Run: lint (all)", type: "node-terminal", request: "launch", command: "npm run lint" },
|
|
2206
|
+
{ name: "Run: docs (all)", type: "node-terminal", request: "launch", command: "npm run doc" },
|
|
2207
|
+
{ name: "Run: graph", type: "node-terminal", request: "launch", command: "npm run graph" }
|
|
2208
|
+
]
|
|
2209
|
+
},
|
|
2210
|
+
tasks: {
|
|
2211
|
+
version: "2.0.0",
|
|
2212
|
+
tasks: [
|
|
2213
|
+
{ label: "build all", type: "shell", command: "npm run build", problemMatcher: ["$tsc"] },
|
|
2214
|
+
{ label: "test all", type: "shell", command: "npm run test", problemMatcher: [] },
|
|
2215
|
+
{ label: "lint all", type: "shell", command: "npm run lint", problemMatcher: [] }
|
|
2216
|
+
]
|
|
2217
|
+
}
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
function pipelineFiles(vars) {
|
|
2221
|
+
const files = Array.from(listAssetFiles("build-templates"), (relativePath) => ({
|
|
2222
|
+
path: `.build-templates/${relativePath}`,
|
|
2223
|
+
content: readAsset(`build-templates/${relativePath}`),
|
|
2224
|
+
ownership: "tool-owned"
|
|
2225
|
+
}));
|
|
2226
|
+
if (vars.ci === "azure" || vars.ci === "both") {
|
|
2227
|
+
files.push({ path: "azure-pipelines.yml", content: readAsset("azure-pipelines.yml"), ownership: "tool-owned" });
|
|
2228
|
+
}
|
|
2229
|
+
if (vars.ci === "github" || vars.ci === "both") {
|
|
2230
|
+
files.push({ path: ".github/workflows/ci.yml", content: readAsset("github/workflows/ci.yml"), ownership: "tool-owned" });
|
|
2231
|
+
}
|
|
2232
|
+
return files;
|
|
2233
|
+
}
|
|
2234
|
+
function monorepoFiles(vars) {
|
|
2235
|
+
const toolOwned = (path, content) => ({ path, content, ownership: "tool-owned" });
|
|
2236
|
+
const scaffold = (path, content) => ({ path, content, ownership: "scaffold" });
|
|
2237
|
+
return [
|
|
2238
|
+
scaffold("package.json", packageJson(vars)),
|
|
2239
|
+
toolOwned("nx.json", nxJson(vars)),
|
|
2240
|
+
toolOwned("tsconfig.base.json", tsconfigBase()),
|
|
2241
|
+
toolOwned("tsconfig.jest.json", tsconfigJest()),
|
|
2242
|
+
toolOwned("jest.config.mjs", jestConfigMjs2),
|
|
2243
|
+
toolOwned("jest.preset.mjs", jestPresetMjs),
|
|
2244
|
+
toolOwned("jest.setup.mjs", jestSetupMjs),
|
|
2245
|
+
toolOwned("jest.clear.mjs", jestClearMjs),
|
|
2246
|
+
toolOwned("eslint.config.mjs", readAsset("eslint.config.mjs")),
|
|
2247
|
+
toolOwned("typedoc.json", typedocJson()),
|
|
2248
|
+
scaffold(".npmrc", npmrc(vars)),
|
|
2249
|
+
toolOwned(".editorconfig", editorconfig),
|
|
2250
|
+
scaffold(".gitignore", gitignore),
|
|
2251
|
+
toolOwned("commitlint.config.mjs", commitlintConfigMjs),
|
|
2252
|
+
scaffold(".husky/commit-msg", huskyCommitMessage),
|
|
2253
|
+
scaffold("README.md", readme(vars)),
|
|
2254
|
+
scaffold("docs/nx-release.md", nxReleaseDocument(vars)),
|
|
2255
|
+
toolOwned(`${vars.displayName}.code-workspace`, codeWorkspace(vars)),
|
|
2256
|
+
...pipelineFiles(vars),
|
|
2257
|
+
// Keep apps/ and libs/ present even before any project is added.
|
|
2258
|
+
scaffold("apps/.gitkeep", ""),
|
|
2259
|
+
scaffold("libs/.gitkeep", "")
|
|
2260
|
+
];
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// src/commands/doctor.ts
|
|
2264
|
+
async function runDoctor(options) {
|
|
2265
|
+
const repoRoot = process.cwd();
|
|
2266
|
+
if (!isManagedRepo(repoRoot)) {
|
|
2267
|
+
logger.error("No .monecromanci.json found here. Run `doctor` from a MoNecromanCI monorepo root.");
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
const config = loadConfig(repoRoot);
|
|
2271
|
+
if (!config) {
|
|
2272
|
+
logger.error("Could not read .monecromanci.json.");
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
const vars = {
|
|
2276
|
+
workspaceName: config.workspaceName,
|
|
2277
|
+
displayName: config.displayName,
|
|
2278
|
+
scope: config.scope,
|
|
2279
|
+
defaultBase: config.defaultBase,
|
|
2280
|
+
nodeVersion: config.nodeVersion,
|
|
2281
|
+
ci: config.ci,
|
|
2282
|
+
registry: config.registry
|
|
2283
|
+
};
|
|
2284
|
+
const specs = [...monorepoFiles(vars)];
|
|
2285
|
+
for (const project of discoverProjects(repoRoot, config)) {
|
|
2286
|
+
specs.push(...projectFiles(project.kind, project));
|
|
2287
|
+
}
|
|
2288
|
+
const report = syncToolOwned(repoRoot, specs, options.apply);
|
|
2289
|
+
for (const path of report.missing) {
|
|
2290
|
+
logger.warn(`missing: ${path}`);
|
|
2291
|
+
}
|
|
2292
|
+
for (const path of report.drift) {
|
|
2293
|
+
logger.warn(`drift: ${path}`);
|
|
2294
|
+
}
|
|
2295
|
+
for (const path of report.fixed) {
|
|
2296
|
+
logger.success(`fixed: ${path}`);
|
|
2297
|
+
}
|
|
2298
|
+
const issues = report.missing.length + report.drift.length;
|
|
2299
|
+
if (issues === 0) {
|
|
2300
|
+
logger.success(`Everything is in sync (${report.ok.length} tool-owned files checked).`);
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
if (!options.apply) {
|
|
2304
|
+
logger.info(`${issues} issue(s) found. Re-run with --fix to repair (scaffold files are left untouched).`);
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
saveConfig(repoRoot, { ...config, templateVersion: TEMPLATE_VERSION });
|
|
2308
|
+
logger.success(`Repaired ${report.fixed.length} file(s); stamped template version ${TEMPLATE_VERSION}.`);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// src/generators/createMonorepo.ts
|
|
2312
|
+
var import_node_path8 = require("path");
|
|
2313
|
+
async function runNew(options) {
|
|
2314
|
+
const yes = options.yes ?? false;
|
|
2315
|
+
const ask = async (message, fallback, provided) => provided ?? (yes ? fallback : await promptText(message, fallback));
|
|
2316
|
+
const askChoice = async (message, choices, fallback, provided) => provided ?? (yes ? fallback : await (0, import_prompts2.select)({ message, choices }));
|
|
2317
|
+
const displayName = await ask("Monorepo name", "My Monorepo", options.name);
|
|
2318
|
+
const workspaceName = toSlug(displayName);
|
|
2319
|
+
const ci = await askChoice("CI provider", [
|
|
2320
|
+
{ name: "Azure DevOps Pipelines", value: "azure" },
|
|
2321
|
+
{ name: "GitHub Actions", value: "github" },
|
|
2322
|
+
{ name: "Both", value: "both" }
|
|
2323
|
+
], "azure", options.ci);
|
|
2324
|
+
const registry = await resolveRegistry(ci, options, ask, askChoice);
|
|
2325
|
+
const defaultScope = registry.kind === "github-packages" ? `@${registry.owner}` : "@auto";
|
|
2326
|
+
const scopeInput = await ask("npm scope", defaultScope, options.scope);
|
|
2327
|
+
const scope = scopeInput.startsWith("@") ? scopeInput : `@${scopeInput}`;
|
|
2328
|
+
const defaultBase = await ask("Default git branch", DEFAULT_BASE, options.base);
|
|
2329
|
+
const targetDirectory = (0, import_node_path8.resolve)(process.cwd(), workspaceName);
|
|
2330
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path8.join)(targetDirectory, "package.json")) && !yes) {
|
|
2331
|
+
const overwrite = await (0, import_prompts2.confirm)({ message: `${targetDirectory} already contains a package.json. Continue and overwrite tool-owned files?`, default: false });
|
|
2332
|
+
if (!overwrite) {
|
|
2333
|
+
logger.warn("Aborted.");
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
const vars = {
|
|
2338
|
+
workspaceName,
|
|
2339
|
+
displayName,
|
|
2340
|
+
scope,
|
|
2341
|
+
defaultBase,
|
|
2342
|
+
nodeVersion: DEFAULT_NODE_VERSION,
|
|
2343
|
+
ci,
|
|
2344
|
+
registry
|
|
2345
|
+
};
|
|
2346
|
+
logger.step(`Creating monorepo in ${targetDirectory}`);
|
|
2347
|
+
reportApply(applyFiles(targetDirectory, monorepoFiles(vars)));
|
|
2348
|
+
saveConfig(targetDirectory, configFromVars(vars));
|
|
2349
|
+
const libName = await resolveInitialLib(options.lib, yes);
|
|
2350
|
+
if (libName) {
|
|
2351
|
+
generateProject(targetDirectory, "internal-lib", libName, configFromVars(vars));
|
|
2352
|
+
}
|
|
2353
|
+
logger.success("Done. Next steps:");
|
|
2354
|
+
logger.info(` cd ${workspaceName}`);
|
|
2355
|
+
logger.info(" npm install");
|
|
2356
|
+
logger.info(` code "${displayName}.code-workspace"`);
|
|
2357
|
+
}
|
|
2358
|
+
async function resolveRegistry(ci, options, ask, askChoice) {
|
|
2359
|
+
const fallbackKind = ci === "github" ? "github-packages" : "azure-artifacts";
|
|
2360
|
+
const kind = await askChoice("Package registry", [
|
|
2361
|
+
{ name: "Azure Artifacts", value: "azure-artifacts" },
|
|
2362
|
+
{ name: "GitHub Packages", value: "github-packages" },
|
|
2363
|
+
{ name: "Public npm", value: "npm" }
|
|
2364
|
+
], fallbackKind, options.registry);
|
|
2365
|
+
if (kind === "azure-artifacts") {
|
|
2366
|
+
const organization = await ask("Azure DevOps organization", "my-org", options.organization);
|
|
2367
|
+
const project = await ask("Azure DevOps project", "Automation", options.project);
|
|
2368
|
+
const artifactsFeed = await ask("Azure Artifacts feed", "AUTO", options.feed);
|
|
2369
|
+
return { kind, organization, project, artifactsFeed };
|
|
2370
|
+
}
|
|
2371
|
+
if (kind === "github-packages") {
|
|
2372
|
+
const owner = await ask("GitHub owner (org or user)", "my-org", options.owner);
|
|
2373
|
+
return { kind, owner };
|
|
2374
|
+
}
|
|
2375
|
+
return { kind: "npm" };
|
|
2376
|
+
}
|
|
2377
|
+
async function resolveInitialLib(provided, shouldAcceptDefaults) {
|
|
2378
|
+
if (provided !== void 0) {
|
|
2379
|
+
return provided === "" ? void 0 : toSlug(provided);
|
|
2380
|
+
}
|
|
2381
|
+
if (shouldAcceptDefaults) {
|
|
2382
|
+
return "helpers";
|
|
2383
|
+
}
|
|
2384
|
+
const addLib = await (0, import_prompts2.confirm)({ message: "Add an initial internal library now?", default: true });
|
|
2385
|
+
return addLib ? toSlug(await promptText("Library name", "helpers")) : void 0;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// src/commands/update.ts
|
|
2389
|
+
async function runUpdate() {
|
|
2390
|
+
await runDoctor({ apply: true });
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// src/util/exec.ts
|
|
2394
|
+
var import_node_child_process = require("child_process");
|
|
2395
|
+
function runShell(command, arguments_, cwd) {
|
|
2396
|
+
const line = [command, ...arguments_].join(" ");
|
|
2397
|
+
const result = (0, import_node_child_process.spawnSync)(line, { stdio: "inherit", shell: true, cwd });
|
|
2398
|
+
return result.status ?? 1;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// src/commands/validate.ts
|
|
2402
|
+
async function runValidate(options) {
|
|
2403
|
+
const repoRoot = process.cwd();
|
|
2404
|
+
if (!isManagedRepo(repoRoot)) {
|
|
2405
|
+
logger.error("No .monecromanci.json found here. Run `validate` from a MoNecromanCI monorepo root.");
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
const arguments_ = ["nx", options.all ? "run-many" : "affected", "-t", "lint", "test", "build"];
|
|
2409
|
+
logger.info(`Running: npx ${arguments_.join(" ")}`);
|
|
2410
|
+
const status = runShell("npx", arguments_, repoRoot);
|
|
2411
|
+
if (status === 0) {
|
|
2412
|
+
logger.success("Validation passed.");
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
logger.error(`Validation failed (exit ${status}).`);
|
|
2416
|
+
process.exitCode = status;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
// src/cli.ts
|
|
2420
|
+
function readVersion() {
|
|
2421
|
+
try {
|
|
2422
|
+
const package_ = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path9.join)(__dirname, "..", "package.json"), "utf8"));
|
|
2423
|
+
return package_.version ?? "0.0.0";
|
|
2424
|
+
} catch {
|
|
2425
|
+
return "0.0.0";
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
var program = new import_commander.Command();
|
|
2429
|
+
program.name("monecromanci").description("MoNecromanCI \u2014 summon, conjure, raise and validate NX monorepos").version(readVersion());
|
|
2430
|
+
program.command("new").alias("summon").argument("[name]", "monorepo name").option("-y, --yes", "non-interactive: accept provided values and defaults").option("--scope <scope>", "npm scope, e.g. @auto").option("--ci <provider>", "CI provider: azure | github | both").option("--registry <kind>", "registry: azure-artifacts | github-packages | npm").option("--owner <owner>", "GitHub owner for the github-packages registry").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--feed <feed>", "Azure Artifacts feed").option("--base <branch>", "default git branch").option("--lib <name>", "initial internal library name (empty string to skip)").description("Scaffold a brand-new canonical NX monorepo").action(async (name, options) => {
|
|
2431
|
+
await runNew({
|
|
2432
|
+
name,
|
|
2433
|
+
yes: options.yes,
|
|
2434
|
+
scope: options.scope,
|
|
2435
|
+
ci: options.ci,
|
|
2436
|
+
registry: options.registry,
|
|
2437
|
+
owner: options.owner,
|
|
2438
|
+
organization: options.org,
|
|
2439
|
+
project: options.project,
|
|
2440
|
+
feed: options.feed,
|
|
2441
|
+
base: options.base,
|
|
2442
|
+
lib: options.lib
|
|
2443
|
+
});
|
|
2444
|
+
});
|
|
2445
|
+
program.command("add").alias("conjure").argument("[type]", "function-app | react-app | internal-lib | publishable-lib | cli-tool").argument("[name]", "project name").description("Add a new project to the current monorepo").action(async (type, name) => {
|
|
2446
|
+
await runAdd({ type, name });
|
|
2447
|
+
});
|
|
2448
|
+
program.command("doctor").aliases(["fix", "raise"]).option("--fix", "apply fixes instead of only reporting").description("Detect and repair configuration drift in the current monorepo").action(async (options) => {
|
|
2449
|
+
await runDoctor({ apply: options.fix ?? false });
|
|
2450
|
+
});
|
|
2451
|
+
program.command("update").alias("ascend").description("Re-sync tool-owned files to the latest templates and apply migrations").action(async () => {
|
|
2452
|
+
await runUpdate();
|
|
2453
|
+
});
|
|
2454
|
+
program.command("validate").alias("ritual").option("--all", "run every project (nx run-many) instead of only affected").description("Run lint/test/build locally (nx affected) before pushing to CI").action(async (options) => {
|
|
2455
|
+
await runValidate({ all: options.all ?? false });
|
|
2456
|
+
});
|
|
2457
|
+
async function main() {
|
|
2458
|
+
try {
|
|
2459
|
+
await program.parseAsync(process.argv);
|
|
2460
|
+
} catch (error) {
|
|
2461
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
2462
|
+
process.exitCode = 1;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
main();
|
|
2466
|
+
//# sourceMappingURL=cli.js.map
|