@aventara/cli 0.1.0-pilot.1 → 0.1.0-pilot.3
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 +12 -5
- package/dist/apply/app-module.anchor.d.ts +5 -11
- package/dist/apply/app-module.anchor.js +0 -23
- package/dist/apply/conflict.confirmer.d.ts +0 -9
- package/dist/apply/conflict.confirmer.js +0 -15
- package/dist/apply/e2e-spec.anchor.js +0 -17
- package/dist/apply/main-cors.anchor.d.ts +50 -0
- package/dist/apply/main-cors.anchor.js +60 -0
- package/dist/apply/manifest.merger.d.ts +17 -13
- package/dist/apply/manifest.merger.js +8 -18
- package/dist/aventara.bin.js +0 -10
- package/dist/catalog/adapter.catalog.generated.d.ts +1 -0
- package/dist/catalog/adapter.catalog.generated.js +1 -4
- package/dist/catalog/catalog-entry.interface.d.ts +21 -13
- package/dist/catalog/catalog.matcher.d.ts +7 -13
- package/dist/catalog/catalog.matcher.js +0 -3
- package/dist/catalog/range.reader.d.ts +4 -10
- package/dist/catalog/range.reader.js +0 -14
- package/dist/cli.d.ts +2 -3
- package/dist/cli.js +5 -8
- package/dist/command/command.parser.d.ts +11 -9
- package/dist/command/command.parser.js +24 -18
- package/dist/node-version.guard.js +0 -12
- package/dist/plan/project.planner.d.ts +7 -13
- package/dist/plan/project.planner.js +44 -35
- package/dist/project/package-manager.detector.d.ts +0 -7
- package/dist/project/package-manager.detector.js +0 -7
- package/dist/project/project.inspector.d.ts +3 -9
- package/dist/project/project.inspector.js +0 -10
- package/dist/project/service.detector.d.ts +5 -10
- package/dist/project/service.detector.js +0 -2
- package/dist/project/source.scanner.js +0 -13
- package/dist/run/command.runner.d.ts +4 -4
- package/dist/run/init.orchestrator.d.ts +4 -6
- package/dist/run/init.orchestrator.js +3 -16
- package/dist/run/new.orchestrator.d.ts +8 -9
- package/dist/run/new.orchestrator.js +10 -18
- package/dist/run/scaffold.committer.d.ts +28 -0
- package/dist/run/scaffold.committer.js +53 -0
- package/dist/templates/docs.links.d.ts +8 -0
- package/dist/templates/docs.links.js +2 -0
- package/dist/templates/prisma7/prisma7.templates.js +0 -27
- package/dist/templates/template.registry.d.ts +13 -17
- package/dist/templates/template.registry.js +0 -5
- package/dist/wizard/answer.resolver.d.ts +5 -7
- package/dist/wizard/answer.resolver.js +0 -10
- package/dist/wizard/readline.prompter.d.ts +5 -7
- package/dist/wizard/readline.prompter.js +0 -10
- package/dist/wizard/wizard.questions.d.ts +28 -22
- package/dist/wizard/wizard.questions.js +21 -49
- package/package.json +2 -2
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reading a declared version range for the two facts the CLI needs from one:
|
|
3
|
-
* its lowest version (D1, D9: what `init` pins when it installs the ORM — the
|
|
4
|
-
* adapter's measured floor) and its lowest major (a project's declared Nest and
|
|
5
|
-
* TypeScript ranges, checked before anything is written).
|
|
6
|
-
*/
|
|
7
1
|
const VERSION = /(\d+)(?:\.(\d+))?(?:\.(\d+))?/;
|
|
8
|
-
/** `^7.10.0` → `7.10.0`; `>=7.10.0 <8` → `7.10.0`. */
|
|
9
2
|
export function lowestVersionOf(range) {
|
|
10
3
|
const found = VERSION.exec(range);
|
|
11
4
|
if (found === null) {
|
|
@@ -13,19 +6,12 @@ export function lowestVersionOf(range) {
|
|
|
13
6
|
}
|
|
14
7
|
return `${found[1]}.${found[2] ?? 0}.${found[3] ?? 0}`;
|
|
15
8
|
}
|
|
16
|
-
/** `^12.0.1` → 12; `~6.0.2` → 6; a range with no number (`latest`, `*`) → `undefined`. */
|
|
17
9
|
export function lowestMajorOf(range) {
|
|
18
10
|
const found = VERSION.exec(range);
|
|
19
11
|
return found === null ? undefined : Number(found[1]);
|
|
20
12
|
}
|
|
21
13
|
const RELEASE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
22
14
|
const COMPARATOR = /^(\^|>=|>|<=|<|=)?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/;
|
|
23
|
-
/**
|
|
24
|
-
* Whether an installed `version` is in a declared `range` (R7: the adapter's
|
|
25
|
-
* declaration decides support). Reads the comparator forms an adapter's peer
|
|
26
|
-
* range uses; a pre-release never satisfies (semver's rule for a range whose
|
|
27
|
-
* comparators carry none). Any other form is refused rather than guessed.
|
|
28
|
-
*/
|
|
29
15
|
export function satisfiesRange(version, range) {
|
|
30
16
|
const release = RELEASE.exec(version);
|
|
31
17
|
if (release === null) {
|
package/dist/cli.d.ts
CHANGED
|
@@ -2,9 +2,8 @@ import type { AdapterCatalog } from "./catalog/catalog-entry.interface.js";
|
|
|
2
2
|
import { type CommandRunner } from "./run/command.runner.js";
|
|
3
3
|
import { type Prompter } from "./wizard/answer.resolver.js";
|
|
4
4
|
/**
|
|
5
|
-
* `aventara` — the bin
|
|
6
|
-
*
|
|
7
|
-
* stack.
|
|
5
|
+
* `aventara` — the bin: `new` and `init`. A refusal is one sentence on stderr and
|
|
6
|
+
* exit 1, never a stack; anything else is a defect and keeps its stack.
|
|
8
7
|
*
|
|
9
8
|
* `init` initializes the project in the working directory
|
|
10
9
|
* (`run/init.orchestrator.ts`); `new` creates one with the pinned `nest new` and
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { ConflictsNotConfirmedError } from "./apply/conflict.confirmer.js";
|
|
3
3
|
import { ADAPTER_CATALOG } from "./catalog/adapter.catalog.generated.js";
|
|
4
|
-
import { CliCommandError, commandUsage, parseCliCommand,
|
|
4
|
+
import { CliCommandError, commandUsage, parseCliCommand, usage, } from "./command/command.parser.js";
|
|
5
5
|
import { ProjectRefusedError } from "./project/project.inspector.js";
|
|
6
6
|
import { runCommand } from "./run/command.runner.js";
|
|
7
7
|
import { InstallFailedError, runInit } from "./run/init.orchestrator.js";
|
|
@@ -27,7 +27,10 @@ export async function runCli(argv, io) {
|
|
|
27
27
|
try {
|
|
28
28
|
const command = parseCliCommand(argv);
|
|
29
29
|
if (command.command === "help") {
|
|
30
|
-
|
|
30
|
+
const catalog = io.catalog ?? ADAPTER_CATALOG;
|
|
31
|
+
io.stdout(command.topic === undefined
|
|
32
|
+
? usage(catalog)
|
|
33
|
+
: commandUsage(command.topic, catalog));
|
|
31
34
|
return 0;
|
|
32
35
|
}
|
|
33
36
|
if (command.command === "version") {
|
|
@@ -66,12 +69,6 @@ export async function runCli(argv, io) {
|
|
|
66
69
|
return 1;
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Runs `aventara` over this process — its arguments, its terminal, its exit
|
|
71
|
-
* code. Called by the bin's entry (`aventara.bin.ts`) once the Node guard has
|
|
72
|
-
* admitted this Node; importing this module runs nothing, which lets `runCli`
|
|
73
|
-
* be tested in process.
|
|
74
|
-
*/
|
|
75
72
|
export async function runFromProcess() {
|
|
76
73
|
const prompter = createReadlinePrompter({
|
|
77
74
|
input: process.stdin,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { AdapterCatalog } from "../catalog/catalog-entry.interface.js";
|
|
1
2
|
import { type QuestionId } from "../wizard/wizard.questions.js";
|
|
2
3
|
/**
|
|
3
|
-
* The `aventara` command line
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The `aventara` command line: `new <name>` and `init`, each question's flag from
|
|
5
|
+
* the wizard's table, and the options that are not questions. Unknown, misplaced
|
|
6
|
+
* or repeated arguments are refused in one sentence; a flag's VALUE is the
|
|
7
|
+
* question's to judge, because what is valid (`--db`'s providers) depends on the
|
|
8
|
+
* catalog and on earlier answers.
|
|
8
9
|
*/
|
|
9
10
|
export type ScaffoldCommandName = "new" | "init";
|
|
10
11
|
export type ScaffoldCommand = {
|
|
@@ -12,9 +13,9 @@ export type ScaffoldCommand = {
|
|
|
12
13
|
/** Raw answers by question, from flags and `new`'s positional `<name>`. */
|
|
13
14
|
readonly given: Readonly<Partial<Record<QuestionId, string>>>;
|
|
14
15
|
readonly skipInstall: boolean;
|
|
15
|
-
/** `new` only: passed through to `nest new
|
|
16
|
+
/** `new` only: passed through to `nest new`. */
|
|
16
17
|
readonly skipGit: boolean;
|
|
17
|
-
/** Accepts each unanswered question's default and confirms overwrites
|
|
18
|
+
/** Accepts each unanswered question's default and confirms overwrites. */
|
|
18
19
|
readonly yes: boolean;
|
|
19
20
|
};
|
|
20
21
|
export type CliCommand =
|
|
@@ -30,7 +31,8 @@ export declare class CliCommandError extends Error {
|
|
|
30
31
|
readonly name = "CliCommandError";
|
|
31
32
|
}
|
|
32
33
|
/** `aventara <command> --help` (pilot.1): that command's usage alone. */
|
|
33
|
-
export declare function commandUsage(command: ScaffoldCommandName): string;
|
|
34
|
-
|
|
34
|
+
export declare function commandUsage(command: ScaffoldCommandName, catalog: AdapterCatalog): string;
|
|
35
|
+
/** `aventara --help`: both commands, each flag with the values `catalog` offers. */
|
|
36
|
+
export declare function usage(catalog: AdapterCatalog): string;
|
|
35
37
|
/** @throws CliCommandError when `argv` is not a command this CLI has. */
|
|
36
38
|
export declare function parseCliCommand(argv: readonly string[]): CliCommand;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { INIT_FLAG_QUESTIONS, NEW_QUESTIONS, } from "../wizard/wizard.questions.js";
|
|
2
|
-
/** The command line cannot be understood. A refusal: one sentence, exit 1. */
|
|
1
|
+
import { INIT_FLAG_QUESTIONS, NEW_QUESTIONS, ormAliasesSentence, } from "../wizard/wizard.questions.js";
|
|
3
2
|
export class CliCommandError extends Error {
|
|
4
3
|
name = "CliCommandError";
|
|
5
4
|
}
|
|
6
|
-
/** The options that are not wizard questions. */
|
|
7
5
|
const OPTIONS = [
|
|
8
6
|
{
|
|
9
7
|
flag: "--skip-install",
|
|
@@ -30,12 +28,18 @@ const QUESTIONS = {
|
|
|
30
28
|
init: INIT_FLAG_QUESTIONS,
|
|
31
29
|
};
|
|
32
30
|
const HELP = "run `aventara --help` for usage";
|
|
33
|
-
function
|
|
31
|
+
function flagUsage(question, catalog) {
|
|
32
|
+
const accepted = question.accepts?.(catalog);
|
|
33
|
+
return `${question.flag} ${accepted === undefined
|
|
34
|
+
? (question.placeholder ?? "<value>")
|
|
35
|
+
: `<${accepted.join("|")}>`}`;
|
|
36
|
+
}
|
|
37
|
+
function usageLines(command, catalog) {
|
|
34
38
|
const rows = [
|
|
35
39
|
...QUESTIONS[command]
|
|
36
40
|
.filter((question) => question.flag.startsWith("--"))
|
|
37
41
|
.map((question) => [
|
|
38
|
-
|
|
42
|
+
flagUsage(question, catalog),
|
|
39
43
|
`${question.label}; asked when omitted`,
|
|
40
44
|
]),
|
|
41
45
|
...OPTIONS.filter((option) => option.commands.includes(command)).map((option) => [
|
|
@@ -48,7 +52,6 @@ function usageLines(command) {
|
|
|
48
52
|
.map(([left, right]) => ` ${left.padEnd(width)} ${right}`)
|
|
49
53
|
.join("\n");
|
|
50
54
|
}
|
|
51
|
-
/** What each command does, in one line: the top-level usage and its own. */
|
|
52
55
|
const SUMMARY = {
|
|
53
56
|
new: "Create a NestJS project with Aventara (nest new, then init).",
|
|
54
57
|
init: "Add Aventara to the NestJS 12 project in this directory.",
|
|
@@ -57,35 +60,38 @@ const SYNOPSIS = {
|
|
|
57
60
|
new: "aventara new <name> [options]",
|
|
58
61
|
init: "aventara init [options]",
|
|
59
62
|
};
|
|
60
|
-
|
|
63
|
+
function asked(catalog) {
|
|
64
|
+
return `${ormAliasesSentence(catalog)}.
|
|
65
|
+
On a terminal every unanswered question is asked; anywhere else, pass its flag
|
|
61
66
|
or --yes, or the run stops before writing anything.
|
|
62
67
|
`;
|
|
63
|
-
|
|
64
|
-
export function commandUsage(command) {
|
|
68
|
+
}
|
|
69
|
+
export function commandUsage(command, catalog) {
|
|
65
70
|
return `Usage: ${SYNOPSIS[command]}
|
|
66
71
|
|
|
67
72
|
${SUMMARY[command]}
|
|
68
73
|
|
|
69
74
|
Options:
|
|
70
|
-
${usageLines(command)}
|
|
75
|
+
${usageLines(command, catalog)}
|
|
71
76
|
|
|
72
|
-
${
|
|
77
|
+
${asked(catalog)}`;
|
|
73
78
|
}
|
|
74
|
-
export
|
|
79
|
+
export function usage(catalog) {
|
|
80
|
+
return `aventara — scaffold an Aventara server on NestJS.
|
|
75
81
|
|
|
76
82
|
Usage:
|
|
77
83
|
${SYNOPSIS.new} ${SUMMARY.new}
|
|
78
|
-
${usageLines("new")}
|
|
84
|
+
${usageLines("new", catalog)}
|
|
79
85
|
|
|
80
86
|
${SYNOPSIS.init} ${SUMMARY.init}
|
|
81
|
-
${usageLines("init")}
|
|
87
|
+
${usageLines("init", catalog)}
|
|
82
88
|
|
|
83
89
|
aventara --help Print this and exit 0.
|
|
84
90
|
aventara <command> --help Print that command's usage and exit 0.
|
|
85
|
-
aventara --version
|
|
91
|
+
aventara --version, -v Print this CLI's version and exit 0.
|
|
86
92
|
|
|
87
|
-
${
|
|
88
|
-
|
|
93
|
+
${asked(catalog)}`;
|
|
94
|
+
}
|
|
89
95
|
export function parseCliCommand(argv) {
|
|
90
96
|
const [command, ...rest] = argv;
|
|
91
97
|
if (command === undefined) {
|
|
@@ -94,7 +100,7 @@ export function parseCliCommand(argv) {
|
|
|
94
100
|
if (command === "--help" || command === "-h") {
|
|
95
101
|
return { command: "help" };
|
|
96
102
|
}
|
|
97
|
-
if (command === "--version") {
|
|
103
|
+
if (command === "--version" || command === "-v") {
|
|
98
104
|
return { command: "version" };
|
|
99
105
|
}
|
|
100
106
|
if (command !== "new" && command !== "init") {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// biome-ignore lint/style/useNodejsImportProtocol: Node 14.0–14.13.0 resolves no "node:" specifier in an ES module, and this module runs on the Nodes the packages do not support.
|
|
2
1
|
import { readFileSync } from "fs";
|
|
3
2
|
function versionOf(text) {
|
|
4
3
|
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(text);
|
|
@@ -16,11 +15,6 @@ function compareVersions(left, right) {
|
|
|
16
15
|
}
|
|
17
16
|
return 0;
|
|
18
17
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Whether `range` admits `version`. Reads `^x.y.z` (the same major, from the
|
|
21
|
-
* floor; majors ≥ 1) and `>=x.y.z`, joined by `||`; anything else throws rather
|
|
22
|
-
* than admit or refuse by guess.
|
|
23
|
-
*/
|
|
24
18
|
function admits(range, version) {
|
|
25
19
|
return range.split("||").some((part) => {
|
|
26
20
|
const comparator = /^\s*(\^|>=)(\d+\.\d+\.\d+)\s*$/.exec(part);
|
|
@@ -32,17 +26,11 @@ function admits(range, version) {
|
|
|
32
26
|
(comparator[1] === ">=" || version[0] === floor[0]));
|
|
33
27
|
});
|
|
34
28
|
}
|
|
35
|
-
/** The sentence a bin answers on `version`, or `undefined` when `range` admits it. */
|
|
36
29
|
export function nodeVersionRefusal(bin, packageName, range, version) {
|
|
37
30
|
return admits(range, versionOf(version))
|
|
38
31
|
? undefined
|
|
39
32
|
: `${bin}: Node ${version} is not supported; ${packageName} needs Node ${range}.`;
|
|
40
33
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Reads the manifest at `manifestUrl`; on a Node its `engines.node` does not
|
|
43
|
-
* admit, writes the sentence to stderr, sets exit code 1 and answers `true` —
|
|
44
|
-
* the bin then loads nothing else.
|
|
45
|
-
*/
|
|
46
34
|
export function refuseUnsupportedNode(bin, manifestUrl) {
|
|
47
35
|
const manifest = JSON.parse(readFileSync(manifestUrl, "utf8"));
|
|
48
36
|
const range = manifest.engines === undefined ? undefined : manifest.engines.node;
|
|
@@ -3,18 +3,12 @@ import { type ProjectInspection } from "../project/project.inspector.js";
|
|
|
3
3
|
import { type OrmScaffold } from "../templates/template.registry.js";
|
|
4
4
|
import type { PackageManager } from "../wizard/wizard.questions.js";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* What is Nest's (its default start scripts, `AventaraModule`'s import) and what
|
|
13
|
-
* is Aventara's (its packages at this CLI's version, D1; the `aventara:prepare`
|
|
14
|
-
* and `postinstall` scripts, Q16-22, D2) is decided here; what is the ORM's
|
|
15
|
-
* comes from its templates (D7).
|
|
6
|
+
* Pure apart from reading the files it would replace.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The name of the script that regenerates the ORM client and the discovery
|
|
10
|
+
* artifact.
|
|
16
11
|
*/
|
|
17
|
-
/** The name of the script that regenerates the ORM client and the discovery artifact (Q16-22). */
|
|
18
12
|
export declare const PREPARE_SCRIPT = "aventara:prepare";
|
|
19
13
|
export type PlannedWrite = {
|
|
20
14
|
readonly path: string;
|
|
@@ -39,12 +33,12 @@ export type PlanInput = {
|
|
|
39
33
|
readonly entry: CatalogEntry;
|
|
40
34
|
readonly scaffold: OrmScaffold;
|
|
41
35
|
readonly packageManager: PackageManager;
|
|
42
|
-
/** This CLI's version: every Aventara package is pinned to it
|
|
36
|
+
/** This CLI's version: every Aventara package is pinned to it. */
|
|
43
37
|
readonly aventaraVersion: string;
|
|
44
38
|
readonly skipInstall: boolean;
|
|
45
39
|
/** The current text of a project file, or `undefined` when absent. */
|
|
46
40
|
readonly read: (file: string) => string | undefined;
|
|
47
|
-
/** The anchored edit is not attempted; the wiring is printed
|
|
41
|
+
/** The anchored edit is not attempted; the wiring is printed. */
|
|
48
42
|
readonly wiringByHand?: string;
|
|
49
43
|
/** Said before the write, beside the planner's own. */
|
|
50
44
|
readonly warnings?: readonly string[];
|
|
@@ -1,23 +1,10 @@
|
|
|
1
1
|
import { spliceAppModule, wiringInstructions, } from "../apply/app-module.anchor.js";
|
|
2
2
|
import { E2E_SPEC, spliceContractTest } from "../apply/e2e-spec.anchor.js";
|
|
3
|
-
import {
|
|
3
|
+
import { CORS_ORIGINS_VARIABLE, MAIN_FILE, mainCorsInstructions, SCAFFOLD_CORS_ORIGINS, spliceMainCors, } from "../apply/main-cors.anchor.js";
|
|
4
|
+
import { appendEnvEntryIfAbsent, mergeEnvFile, mergeGitignore, mergePackageManifest, mergePnpmWorkspace, } from "../apply/manifest.merger.js";
|
|
4
5
|
import { APP_MODULE, } from "../project/project.inspector.js";
|
|
5
6
|
import { SCAFFOLD_ENTRYPOINT, } from "../templates/template.registry.js";
|
|
6
|
-
/**
|
|
7
|
-
* §4.1's `plan`: everything `aventara init` will write, decided before anything
|
|
8
|
-
* is written — the files, the anchored `app.module.ts` edit (or the printed
|
|
9
|
-
* instructions), the merged manifests — and every **conflict**, computed both
|
|
10
|
-
* ways so the confirmation can choose. Pure apart from reading the files it
|
|
11
|
-
* would replace.
|
|
12
|
-
*
|
|
13
|
-
* What is Nest's (its default start scripts, `AventaraModule`'s import) and what
|
|
14
|
-
* is Aventara's (its packages at this CLI's version, D1; the `aventara:prepare`
|
|
15
|
-
* and `postinstall` scripts, Q16-22, D2) is decided here; what is the ORM's
|
|
16
|
-
* comes from its templates (D7).
|
|
17
|
-
*/
|
|
18
|
-
/** The name of the script that regenerates the ORM client and the discovery artifact (Q16-22). */
|
|
19
7
|
export const PREPARE_SCRIPT = "aventara:prepare";
|
|
20
|
-
/** D3: `nest new`'s start scripts (B1), and what each becomes so `.env` reaches the server. */
|
|
21
8
|
const START_SCRIPTS = {
|
|
22
9
|
start: { from: "nest start", to: "nest start --env-file .env" },
|
|
23
10
|
"start:dev": {
|
|
@@ -33,11 +20,6 @@ const START_SCRIPTS = {
|
|
|
33
20
|
to: "node --env-file-if-exists=.env dist/main",
|
|
34
21
|
},
|
|
35
22
|
};
|
|
36
|
-
/**
|
|
37
|
-
* F-853: `nest new`'s e2e script per module kind (B1, B28), and the same run with
|
|
38
|
-
* the `.env` the start scripts load — the e2e spec boots `AppModule`, whose
|
|
39
|
-
* service needs `DATABASE_URL`, and neither vitest nor jest loads `.env`.
|
|
40
|
-
*/
|
|
41
23
|
const E2E_SCRIPTS = {
|
|
42
24
|
esm: {
|
|
43
25
|
from: "vitest run --config ./vitest.config.e2e.ts",
|
|
@@ -61,12 +43,6 @@ function writeOf(path, before, after) {
|
|
|
61
43
|
},
|
|
62
44
|
];
|
|
63
45
|
}
|
|
64
|
-
/**
|
|
65
|
-
* pilot.1 — the frontend's first command, as the developer can run it: the
|
|
66
|
-
* package manager's own one-off runner (`npx` or `pnpm dlx`), and — while this
|
|
67
|
-
* CLI is a prerelease — its dist-tag, so the client comes from the same release
|
|
68
|
-
* (`0.1.0-pilot.1` → `@aventara/client@pilot`). A release has no tag to name.
|
|
69
|
-
*/
|
|
70
46
|
export function frontendInitCommand(packageManager, cliVersion) {
|
|
71
47
|
const tag = /^\d+\.\d+\.\d+-([0-9A-Za-z-]+)/.exec(cliVersion)?.[1];
|
|
72
48
|
const runner = packageManager === "pnpm" ? "pnpm dlx" : "npx";
|
|
@@ -87,7 +63,6 @@ export function planInit(input) {
|
|
|
87
63
|
keeping.push(...writeOf(path, before, kept.text));
|
|
88
64
|
replacing.push(...writeOf(path, before, replaced.text));
|
|
89
65
|
};
|
|
90
|
-
// The ORM's new files: identical content is no change (P3), other content a conflict.
|
|
91
66
|
for (const file of scaffold.files) {
|
|
92
67
|
const before = input.read(file.path);
|
|
93
68
|
if (before === undefined) {
|
|
@@ -111,7 +86,6 @@ export function planInit(input) {
|
|
|
111
86
|
});
|
|
112
87
|
}
|
|
113
88
|
}
|
|
114
|
-
// The one edit to developer code (R2).
|
|
115
89
|
const wiring = {
|
|
116
90
|
imports: [
|
|
117
91
|
`import { AventaraModule } from '${NEST_HOST}';`,
|
|
@@ -132,8 +106,6 @@ export function planInit(input) {
|
|
|
132
106
|
if (edit.kind === "edited") {
|
|
133
107
|
keeping.push(...writeOf(APP_MODULE, appModule, edit.text));
|
|
134
108
|
replacing.push(...writeOf(APP_MODULE, appModule, edit.text));
|
|
135
|
-
// pilot.1: Nest's own e2e spec also asks for the contract — only when
|
|
136
|
-
// AventaraModule was wired in, so the test proves what it claims.
|
|
137
109
|
const spec = input.read(E2E_SPEC);
|
|
138
110
|
const tested = spec === undefined
|
|
139
111
|
? undefined
|
|
@@ -147,6 +119,31 @@ export function planInit(input) {
|
|
|
147
119
|
instructions = wiringInstructions(wiring);
|
|
148
120
|
warnings.push(`${APP_MODULE} was not edited: ${edit.reason}; add AventaraModule by hand, as printed below`);
|
|
149
121
|
}
|
|
122
|
+
const main = input.read(MAIN_FILE);
|
|
123
|
+
const cors = main === undefined
|
|
124
|
+
? { kind: "not-anchored", reason: `there is no ${MAIN_FILE}` }
|
|
125
|
+
: spliceMainCors(main);
|
|
126
|
+
if (cors.kind === "edited") {
|
|
127
|
+
keeping.push(...writeOf(MAIN_FILE, main, cors.text));
|
|
128
|
+
replacing.push(...writeOf(MAIN_FILE, main, cors.text));
|
|
129
|
+
}
|
|
130
|
+
else if (cors.kind === "configured") {
|
|
131
|
+
warnings.push(`${MAIN_FILE} already configures CORS, so it was left as it is; a browser frontend on another origin is answered only if that configuration allows it`);
|
|
132
|
+
}
|
|
133
|
+
else if (cors.kind === "not-anchored") {
|
|
134
|
+
instructions = [instructions, mainCorsInstructions()]
|
|
135
|
+
.filter((text) => text !== undefined)
|
|
136
|
+
.join("\n");
|
|
137
|
+
warnings.push(`${MAIN_FILE} was not edited: ${cors.reason}; add CORS by hand, as printed below`);
|
|
138
|
+
}
|
|
139
|
+
const corsWanted = cors.kind !== "configured";
|
|
140
|
+
const scaffoldWritesEnv = Object.keys(scaffold.env).length > 0;
|
|
141
|
+
const env = input.read(".env");
|
|
142
|
+
const corsStep = corsWanted && !scaffoldWritesEnv && env === undefined
|
|
143
|
+
? [
|
|
144
|
+
`Let a browser frontend call the server: add ${CORS_ORIGINS_VARIABLE}="${SCAFFOLD_CORS_ORIGINS}" to .env (unset, CORS stays off)`,
|
|
145
|
+
]
|
|
146
|
+
: [];
|
|
150
147
|
const versions = {
|
|
151
148
|
[CORE]: input.aventaraVersion,
|
|
152
149
|
[NEST_HOST]: input.aventaraVersion,
|
|
@@ -157,7 +154,6 @@ export function planInit(input) {
|
|
|
157
154
|
devDependencies: scaffold.devDependencies,
|
|
158
155
|
scripts: {
|
|
159
156
|
[PREPARE_SCRIPT]: scaffold.prepare,
|
|
160
|
-
// D2: the generated trees are ignored and rebuilt on every install.
|
|
161
157
|
postinstall: `${packageManager} run ${PREPARE_SCRIPT}`,
|
|
162
158
|
},
|
|
163
159
|
rewrites: inspection.usesNestConfig
|
|
@@ -167,12 +163,24 @@ export function planInit(input) {
|
|
|
167
163
|
"test:e2e": E2E_SCRIPTS[inspection.moduleKind],
|
|
168
164
|
},
|
|
169
165
|
}, replace));
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
166
|
+
if (scaffoldWritesEnv || (corsWanted && env !== undefined)) {
|
|
167
|
+
both(".env", env, (replace) => {
|
|
168
|
+
const merged = scaffoldWritesEnv
|
|
169
|
+
? mergeEnvFile(env, scaffold.env, replace)
|
|
170
|
+
: { text: env ?? "", conflicts: [] };
|
|
171
|
+
return corsWanted
|
|
172
|
+
? {
|
|
173
|
+
text: appendEnvEntryIfAbsent(merged.text, CORS_ORIGINS_VARIABLE, SCAFFOLD_CORS_ORIGINS),
|
|
174
|
+
conflicts: merged.conflicts,
|
|
175
|
+
}
|
|
176
|
+
: merged;
|
|
177
|
+
});
|
|
173
178
|
}
|
|
174
179
|
const gitignore = input.read(".gitignore");
|
|
175
|
-
both(".gitignore", gitignore, () => mergeGitignore(gitignore,
|
|
180
|
+
both(".gitignore", gitignore, () => mergeGitignore(gitignore, [
|
|
181
|
+
...scaffold.gitignore,
|
|
182
|
+
...(scaffoldWritesEnv ? [".env"] : []),
|
|
183
|
+
]));
|
|
176
184
|
if (packageManager === "pnpm" && scaffold.nativeBuilds.length > 0) {
|
|
177
185
|
const workspace = input.read("pnpm-workspace.yaml");
|
|
178
186
|
both("pnpm-workspace.yaml", workspace, () => mergePnpmWorkspace(workspace, scaffold.nativeBuilds));
|
|
@@ -194,6 +202,7 @@ export function planInit(input) {
|
|
|
194
202
|
"The start scripts were left as they are: @nestjs/config loads .env, so make sure it does before Prisma connects.",
|
|
195
203
|
]
|
|
196
204
|
: []),
|
|
205
|
+
...corsStep,
|
|
197
206
|
`Start the server: ${run} run start:dev`,
|
|
198
207
|
`In your frontend: ${frontendInitCommand(packageManager, input.aventaraVersion)}`,
|
|
199
208
|
],
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import type { PackageManager } from "../wizard/wizard.questions.js";
|
|
2
|
-
/**
|
|
3
|
-
* Q16-5's default package manager: a lockfile says what a project already uses
|
|
4
|
-
* (`init`); otherwise the package manager that launched this CLI says what the
|
|
5
|
-
* developer uses — `npm_config_user_agent` is `npm/…` under `npx` and
|
|
6
|
-
* `npm exec`, `pnpm/…` under `pnpm dlx` and `pnpm exec`, and unset when the bin
|
|
7
|
-
* runs directly (B11); otherwise npm.
|
|
8
|
-
*/
|
|
9
2
|
export declare function detectPackageManager(signals: {
|
|
10
3
|
readonly lockfile?: PackageManager | undefined;
|
|
11
4
|
readonly userAgent?: string | undefined;
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Q16-5's default package manager: a lockfile says what a project already uses
|
|
3
|
-
* (`init`); otherwise the package manager that launched this CLI says what the
|
|
4
|
-
* developer uses — `npm_config_user_agent` is `npm/…` under `npx` and
|
|
5
|
-
* `npm exec`, `pnpm/…` under `pnpm dlx` and `pnpm exec`, and unset when the bin
|
|
6
|
-
* runs directly (B11); otherwise npm.
|
|
7
|
-
*/
|
|
8
1
|
export function detectPackageManager(signals) {
|
|
9
2
|
if (signals.lockfile !== undefined) {
|
|
10
3
|
return signals.lockfile;
|
|
@@ -2,17 +2,11 @@ import type { PackageManifest } from "../apply/manifest.merger.js";
|
|
|
2
2
|
import type { AdapterCatalog, CatalogEntry } from "../catalog/catalog-entry.interface.js";
|
|
3
3
|
import type { ModuleKind } from "../templates/template.registry.js";
|
|
4
4
|
import type { PackageManager } from "../wizard/wizard.questions.js";
|
|
5
|
-
/**
|
|
6
|
-
* §4.1's `inspect`: what `aventara init` needs to know about the project in the
|
|
7
|
-
* working directory, read before anything is asked or written — so every
|
|
8
|
-
* refusal here leaves the directory exactly as it was (it is a property of the
|
|
9
|
-
* order, not of a rollback).
|
|
10
|
-
*/
|
|
11
5
|
/** The project is not one `aventara init` serves. A refusal: one sentence, nothing written. */
|
|
12
6
|
export declare class ProjectRefusedError extends Error {
|
|
13
7
|
readonly name = "ProjectRefusedError";
|
|
14
8
|
}
|
|
15
|
-
/** The ORM a project already has, by the catalog's family packages
|
|
9
|
+
/** The ORM a project already has, by the catalog's family packages. */
|
|
16
10
|
export type OrmPresence = {
|
|
17
11
|
readonly kind: "absent";
|
|
18
12
|
} | {
|
|
@@ -30,9 +24,9 @@ export type ProjectInspection = {
|
|
|
30
24
|
/** The package manager the lockfile names, if exactly one does. */
|
|
31
25
|
readonly lockfile: PackageManager | undefined;
|
|
32
26
|
readonly orm: OrmPresence;
|
|
33
|
-
/** `@nestjs/config` loads `.env` itself, so the start scripts are left alone
|
|
27
|
+
/** `@nestjs/config` loads `.env` itself, so the start scripts are left alone. */
|
|
34
28
|
readonly usesNestConfig: boolean;
|
|
35
29
|
};
|
|
36
|
-
/** The file the anchored edit opens
|
|
30
|
+
/** The file the anchored edit opens. */
|
|
37
31
|
export declare const APP_MODULE = "src/app.module.ts";
|
|
38
32
|
export declare function inspectProject(directory: string, catalog: AdapterCatalog): ProjectInspection;
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { lowestMajorOf } from "../catalog/range.reader.js";
|
|
4
|
-
/**
|
|
5
|
-
* §4.1's `inspect`: what `aventara init` needs to know about the project in the
|
|
6
|
-
* working directory, read before anything is asked or written — so every
|
|
7
|
-
* refusal here leaves the directory exactly as it was (it is a property of the
|
|
8
|
-
* order, not of a rollback).
|
|
9
|
-
*/
|
|
10
|
-
/** The project is not one `aventara init` serves. A refusal: one sentence, nothing written. */
|
|
11
4
|
export class ProjectRefusedError extends Error {
|
|
12
5
|
name = "ProjectRefusedError";
|
|
13
6
|
}
|
|
14
|
-
/** Nest 12 only (Phase 11 Q16). */
|
|
15
7
|
const NEST_MAJOR = 12;
|
|
16
|
-
/** D4: TypeScript 7 has no classic compiler API. */
|
|
17
8
|
const FIRST_UNSUPPORTED_TYPESCRIPT = 7;
|
|
18
|
-
/** The file the anchored edit opens (R2). */
|
|
19
9
|
export const APP_MODULE = "src/app.module.ts";
|
|
20
10
|
const LOCKFILES = [
|
|
21
11
|
["package-lock.json", "npm"],
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* 1. **Candidates**: exported classes that `extends` the ORM's client class,
|
|
6
|
-
* where the file imports that class from a specifier the ORM's templates
|
|
7
|
-
* recognise (the generated client, or the ORM package).
|
|
8
|
-
* 2. **Compositions**: exported classes that hold a client instead of extending
|
|
9
|
-
* one (`client = new PrismaClient(…)`, Phase 11 S7's shape) — reported with
|
|
10
|
-
* the member, never wired.
|
|
2
|
+
* 1. **Candidates**: exported classes that `extends` the ORM's client class, where
|
|
3
|
+
* the file imports that class from a specifier the ORM's templates recognise
|
|
4
|
+
* (the generated client, or the ORM package).
|
|
11
5
|
* 3. **Modules**: each `@Module({…})`-decorated exported class with the
|
|
12
|
-
* identifiers its `providers` and `exports` list, and whether it is
|
|
6
|
+
* identifiers its `providers` and `exports` list, and whether it is
|
|
7
|
+
* `@Global()`.
|
|
13
8
|
*
|
|
14
9
|
* Which of them is *the* service is the caller's question (the wizard's).
|
|
15
10
|
*/
|
|
@@ -18,7 +18,6 @@ function sourceFiles(directory, root, skip) {
|
|
|
18
18
|
: [];
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
|
-
/** The identifiers the array under `key`, at `object`'s own level, names (`providers: [A, B]`). */
|
|
22
21
|
function arrayNames(text, code, object, key) {
|
|
23
22
|
const end = closing(text, code, object);
|
|
24
23
|
let depth = 0;
|
|
@@ -99,7 +98,6 @@ export function scanServices(directory, shape) {
|
|
|
99
98
|
}
|
|
100
99
|
return { candidates, compositions, modules };
|
|
101
100
|
}
|
|
102
|
-
/** The modules that make `service` injectable elsewhere: providing and exporting it, or global. */
|
|
103
101
|
export function modulesProviding(scan, service) {
|
|
104
102
|
return scan.modules.filter((module) => module.providers.includes(service.exportName) &&
|
|
105
103
|
(module.exports.includes(service.exportName) || module.global));
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A lexical view of TypeScript source — enough to find decorators, classes,
|
|
3
|
-
* imports and brackets **in code**, never inside a comment, a string or a
|
|
4
|
-
* template literal's text — with no compiler: `aventara new` runs before
|
|
5
|
-
* anything (TypeScript included) is installed in the project, and the CLI
|
|
6
|
-
* carries no TypeScript of its own. Shared by the anchored `app.module.ts` edit
|
|
7
|
-
* and the detection of a developer's ORM service.
|
|
8
|
-
*/
|
|
9
|
-
/** For each character: is it code (not inside a comment, string or template text)? */
|
|
10
1
|
export function codeMask(text) {
|
|
11
2
|
const code = new Array(text.length).fill(true);
|
|
12
|
-
/** Open template literals' brace depths: `${` re-enters code until its `}`. */
|
|
13
3
|
const templates = [];
|
|
14
4
|
let braces = 0;
|
|
15
5
|
let index = 0;
|
|
@@ -19,7 +9,6 @@ export function codeMask(text) {
|
|
|
19
9
|
}
|
|
20
10
|
};
|
|
21
11
|
const templateText = (from) => {
|
|
22
|
-
// Scans template text from `from` until "`" (closes) or "${" (re-enters code).
|
|
23
12
|
let at = from;
|
|
24
13
|
while (at < text.length) {
|
|
25
14
|
if (text[at] === "\\") {
|
|
@@ -88,7 +77,6 @@ export function codeMask(text) {
|
|
|
88
77
|
}
|
|
89
78
|
return code;
|
|
90
79
|
}
|
|
91
|
-
/** Every index where `token` starts in code. */
|
|
92
80
|
export function codeIndexes(text, code, token) {
|
|
93
81
|
const found = [];
|
|
94
82
|
for (const match of text.matchAll(token)) {
|
|
@@ -103,7 +91,6 @@ export const OPENERS = {
|
|
|
103
91
|
"[": "]",
|
|
104
92
|
"{": "}",
|
|
105
93
|
};
|
|
106
|
-
/** The index of the bracket closing the one at `open`, counting code brackets only. */
|
|
107
94
|
export function closing(text, code, open) {
|
|
108
95
|
const stack = [];
|
|
109
96
|
for (let at = open; at < text.length; at += 1) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The one external process seam
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The one external process seam: a command, its arguments, a working directory.
|
|
3
|
+
* Its stdout is always **piped**, never inherited — `nest new` asks a question it
|
|
4
|
+
* has no flag for when its stdout is a terminal — and both streams are collected,
|
|
5
|
+
* so a failure can be reported with what it said.
|
|
6
6
|
*/
|
|
7
7
|
export type CommandResult = {
|
|
8
8
|
readonly code: number;
|
|
@@ -3,11 +3,9 @@ import type { ScaffoldCommand } from "../command/command.parser.js";
|
|
|
3
3
|
import { type Prompter } from "../wizard/answer.resolver.js";
|
|
4
4
|
import type { CommandRunner } from "./command.runner.js";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* install) names the command, its exit code and what to re-run; nothing is
|
|
10
|
-
* rolled back.
|
|
6
|
+
* Every step before the write reads; so a refusal anywhere before it leaves the
|
|
7
|
+
* project exactly as it was. A failure after it (the install) names the command,
|
|
8
|
+
* its exit code and what to re-run; nothing is rolled back.
|
|
11
9
|
*/
|
|
12
10
|
export type InitIo = {
|
|
13
11
|
readonly directory: string;
|
|
@@ -17,7 +15,7 @@ export type InitIo = {
|
|
|
17
15
|
readonly prompter: Prompter;
|
|
18
16
|
readonly runner: CommandRunner;
|
|
19
17
|
readonly catalog: AdapterCatalog;
|
|
20
|
-
/** This CLI's own version
|
|
18
|
+
/** This CLI's own version. */
|
|
21
19
|
readonly aventaraVersion: string;
|
|
22
20
|
/** Next steps that come before init's own (`new`'s `cd <name>`). */
|
|
23
21
|
readonly firstSteps?: readonly string[];
|