create-next-pro-cli 0.1.31 → 0.1.32
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 +8 -6
- package/create-next-pro-completion.sh +4 -3
- package/create-next-pro-completion.zsh +3 -3
- package/dist/bin.bun.js +495 -118
- package/dist/bin.node.js +558 -135
- package/dist/bin.node.js.map +1 -1
- package/package.json +6 -5
- package/templates/Page/page-ui.tsx +2 -2
- package/templates/Page/page-ui.user.tsx +17 -0
- package/templates/Projects/default/.agents/skills/create-next-pro-addcomponent/SKILL.md +7 -7
- package/templates/Projects/default/.agents/skills/create-next-pro-addpage/SKILL.md +14 -9
- package/templates/Projects/default/.agents/skills/create-next-pro-rmpage/SKILL.md +7 -6
- package/templates/Projects/default/.github/workflows/quality.yml +2 -1
- package/templates/Projects/default/AGENTS.md +12 -4
- package/templates/Projects/default/README.md +6 -0
- package/templates/Projects/default/bun.lock +114 -106
- package/templates/Projects/default/package.json +7 -5
- package/templates/Projects/default/pnpm-workspace.yaml +2 -1
- package/templates/Projects/default/scripts/audit-policy.ts +376 -0
- package/templates/Projects/default/scripts/audit.ts +72 -3
- package/templates/Projects/default/scripts/package-manager.ts +37 -0
- package/templates/Projects/default/tests/unit/audit-policy.test.ts +152 -0
package/dist/bin.node.js
CHANGED
|
@@ -6,14 +6,124 @@ import path10 from "path";
|
|
|
6
6
|
// src/cli/completion.ts
|
|
7
7
|
import path2 from "path";
|
|
8
8
|
|
|
9
|
+
// src/core/contracts.ts
|
|
10
|
+
var CliError = class extends Error {
|
|
11
|
+
exitCode;
|
|
12
|
+
code;
|
|
13
|
+
hint;
|
|
14
|
+
scope;
|
|
15
|
+
path;
|
|
16
|
+
constructor(message, options = {}) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "CliError";
|
|
19
|
+
if (typeof options === "number") {
|
|
20
|
+
this.exitCode = options;
|
|
21
|
+
this.code = "FILESYSTEM_ERROR";
|
|
22
|
+
} else {
|
|
23
|
+
this.exitCode = options.exitCode ?? 1;
|
|
24
|
+
this.code = options.code ?? "FILESYSTEM_ERROR";
|
|
25
|
+
this.hint = options.hint;
|
|
26
|
+
this.scope = options.scope;
|
|
27
|
+
this.path = options.path;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/core/page-area.ts
|
|
33
|
+
var PAGE_AREAS = ["public", "user"];
|
|
34
|
+
function isPageArea(value) {
|
|
35
|
+
return PAGE_AREAS.includes(value);
|
|
36
|
+
}
|
|
37
|
+
function parseAreaOption(args) {
|
|
38
|
+
const remaining = args.length > 0 ? [args[0]] : [];
|
|
39
|
+
let area;
|
|
40
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
41
|
+
const argument = args[index];
|
|
42
|
+
if (argument.startsWith("--area=")) {
|
|
43
|
+
throw new CliError(
|
|
44
|
+
"The --area option must use a separate public or user value.",
|
|
45
|
+
{
|
|
46
|
+
code: "INVALID_ARGUMENT",
|
|
47
|
+
hint: "Use --area public or --area user."
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (argument !== "--area") {
|
|
52
|
+
remaining.push(argument);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (area) {
|
|
56
|
+
throw new CliError("The --area option can only be provided once.", {
|
|
57
|
+
code: "INVALID_ARGUMENT"
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const value = args[index + 1];
|
|
61
|
+
if (!value || value.startsWith("-")) {
|
|
62
|
+
throw new CliError("The --area option requires public or user.", {
|
|
63
|
+
code: "INVALID_ARGUMENT",
|
|
64
|
+
hint: "Use --area public or --area user."
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (!isPageArea(value)) {
|
|
68
|
+
throw new CliError(`Unsupported page area: ${value}.`, {
|
|
69
|
+
code: "INVALID_ARGUMENT",
|
|
70
|
+
hint: "Use --area public or --area user."
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
area = value;
|
|
74
|
+
index += 1;
|
|
75
|
+
}
|
|
76
|
+
return { area, args: remaining };
|
|
77
|
+
}
|
|
78
|
+
function requirePageArea(area, command) {
|
|
79
|
+
if (area) return area;
|
|
80
|
+
throw new CliError(`The ${command} command requires --area public or user.`, {
|
|
81
|
+
code: "INVALID_ARGUMENT",
|
|
82
|
+
hint: `Run ${command} with --area public or --area user.`
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function areaRouteGroup(area) {
|
|
86
|
+
return `(${area})`;
|
|
87
|
+
}
|
|
88
|
+
|
|
9
89
|
// src/core/page-catalog.ts
|
|
10
90
|
import path from "path";
|
|
11
91
|
function isRouteGroup(segment) {
|
|
12
92
|
return segment.startsWith("(") && segment.endsWith(")");
|
|
13
93
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
94
|
+
function logicalSegments(relative) {
|
|
95
|
+
return relative.filter((segment) => !isRouteGroup(segment));
|
|
96
|
+
}
|
|
97
|
+
function issueForRoute(projectRoot, directory, relative) {
|
|
98
|
+
const segments = logicalSegments(relative);
|
|
99
|
+
if (segments.length === 0 || segments.some(
|
|
100
|
+
(segment) => segment.startsWith("_") || segment.startsWith("[")
|
|
101
|
+
)) {
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
const first = relative[0];
|
|
105
|
+
if (!first || !isRouteGroup(first)) {
|
|
106
|
+
return {
|
|
107
|
+
logicalName: segments.join("."),
|
|
108
|
+
reason: "ungrouped",
|
|
109
|
+
routeDirectories: [path.relative(projectRoot, directory)]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const groupName = first.slice(1, -1);
|
|
113
|
+
if (!isPageArea(groupName) || relative.slice(1).some(isRouteGroup)) {
|
|
114
|
+
return {
|
|
115
|
+
logicalName: segments.join("."),
|
|
116
|
+
reason: "unsupported-route-group",
|
|
117
|
+
routeDirectories: [path.relative(projectRoot, directory)]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
async function discoverPageCatalog(projectRoot, fs) {
|
|
123
|
+
const localizedRoot = path.join(projectRoot, "src", "app", "[locale]");
|
|
124
|
+
const appRoot = fs.exists(localizedRoot) ? localizedRoot : path.join(projectRoot, "src", "app");
|
|
125
|
+
const rawCandidates = [];
|
|
126
|
+
const issues = [];
|
|
17
127
|
async function visit(directory, relative = []) {
|
|
18
128
|
let entries;
|
|
19
129
|
try {
|
|
@@ -22,26 +132,32 @@ async function discoverPages(projectRoot, fs) {
|
|
|
22
132
|
return;
|
|
23
133
|
}
|
|
24
134
|
if (entries.some((entry) => entry.isFile && entry.name === "page.tsx")) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
routeSegments
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
135
|
+
const issue = issueForRoute(projectRoot, directory, relative);
|
|
136
|
+
if (issue) {
|
|
137
|
+
issues.push(issue);
|
|
138
|
+
} else if (relative.length > 1) {
|
|
139
|
+
const area = relative[0].slice(1, -1);
|
|
140
|
+
const routeSegments = relative.slice(1);
|
|
141
|
+
if (routeSegments.length > 0 && !routeSegments.some(
|
|
142
|
+
(segment) => segment.startsWith("_") || segment.startsWith("[")
|
|
143
|
+
)) {
|
|
144
|
+
const logicalName = routeSegments.join(".");
|
|
145
|
+
rawCandidates.push({
|
|
146
|
+
id: `${area}:${logicalName}`,
|
|
147
|
+
area,
|
|
148
|
+
logicalName,
|
|
149
|
+
routeSegments,
|
|
150
|
+
routeDirectory: directory,
|
|
151
|
+
uiDirectory: path.join(projectRoot, "src", "ui", ...routeSegments),
|
|
152
|
+
messageFile: path.join(
|
|
153
|
+
projectRoot,
|
|
154
|
+
"messages",
|
|
155
|
+
"{locale}",
|
|
156
|
+
`${routeSegments[0]}.json`
|
|
157
|
+
),
|
|
158
|
+
messageKey: routeSegments.length > 1 ? routeSegments.at(-1) : void 0
|
|
159
|
+
});
|
|
160
|
+
}
|
|
45
161
|
}
|
|
46
162
|
}
|
|
47
163
|
for (const entry of entries) {
|
|
@@ -54,9 +170,59 @@ async function discoverPages(projectRoot, fs) {
|
|
|
54
170
|
}
|
|
55
171
|
}
|
|
56
172
|
await visit(appRoot);
|
|
57
|
-
|
|
173
|
+
const candidatesByName = /* @__PURE__ */ new Map();
|
|
174
|
+
for (const candidate of rawCandidates) {
|
|
175
|
+
const entries = candidatesByName.get(candidate.logicalName) ?? [];
|
|
176
|
+
entries.push(candidate);
|
|
177
|
+
candidatesByName.set(candidate.logicalName, entries);
|
|
178
|
+
}
|
|
179
|
+
for (const [logicalName, candidates2] of candidatesByName) {
|
|
180
|
+
if (candidates2.length < 2) continue;
|
|
181
|
+
issues.push({
|
|
182
|
+
logicalName,
|
|
183
|
+
reason: "duplicate-logical-route",
|
|
184
|
+
routeDirectories: candidates2.map(
|
|
185
|
+
(candidate) => path.relative(projectRoot, candidate.routeDirectory)
|
|
186
|
+
)
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const invalidNames = new Set(issues.map((issue) => issue.logicalName));
|
|
190
|
+
const candidates = rawCandidates.filter((candidate) => !invalidNames.has(candidate.logicalName)).sort(
|
|
191
|
+
(left, right) => left.area.localeCompare(right.area) || left.logicalName.localeCompare(right.logicalName)
|
|
192
|
+
);
|
|
193
|
+
issues.sort(
|
|
58
194
|
(left, right) => left.logicalName.localeCompare(right.logicalName)
|
|
59
195
|
);
|
|
196
|
+
return { candidates, issues };
|
|
197
|
+
}
|
|
198
|
+
function routeIssue(catalog, logicalName) {
|
|
199
|
+
return catalog.issues.find((issue) => issue.logicalName === logicalName);
|
|
200
|
+
}
|
|
201
|
+
function assertConsistentLogicalRoute(catalog, logicalName) {
|
|
202
|
+
const issue = routeIssue(catalog, logicalName);
|
|
203
|
+
if (!issue) return;
|
|
204
|
+
throw new CliError(`Page route "${logicalName}" is inconsistent.`, {
|
|
205
|
+
code: "INCONSISTENT_ROUTE",
|
|
206
|
+
scope: "project",
|
|
207
|
+
path: issue.routeDirectories.join(", "),
|
|
208
|
+
hint: "Move the route into exactly one of the (public) or (user) areas before retrying."
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function resolvePageCandidate(catalog, logicalName, area) {
|
|
212
|
+
assertConsistentLogicalRoute(catalog, logicalName);
|
|
213
|
+
const candidate = catalog.candidates.find(
|
|
214
|
+
(entry) => entry.logicalName === logicalName && entry.area === area
|
|
215
|
+
);
|
|
216
|
+
if (candidate) return candidate;
|
|
217
|
+
const otherArea = catalog.candidates.find(
|
|
218
|
+
(entry) => entry.logicalName === logicalName
|
|
219
|
+
);
|
|
220
|
+
throw new CliError(`Page not found in the ${area} area: ${logicalName}.`, {
|
|
221
|
+
code: "TARGET_NOT_FOUND",
|
|
222
|
+
scope: "project",
|
|
223
|
+
path: logicalName.replaceAll(".", "/"),
|
|
224
|
+
hint: otherArea ? `The page exists in the ${otherArea.area} area.` : void 0
|
|
225
|
+
});
|
|
60
226
|
}
|
|
61
227
|
|
|
62
228
|
// src/cli/completion.ts
|
|
@@ -75,6 +241,7 @@ var PUBLIC_COMMANDS = [
|
|
|
75
241
|
];
|
|
76
242
|
var OPTIONS = {
|
|
77
243
|
addpage: [
|
|
244
|
+
"--area",
|
|
78
245
|
"--layout",
|
|
79
246
|
"--page",
|
|
80
247
|
"--loading",
|
|
@@ -94,22 +261,57 @@ async function directories(root, context) {
|
|
|
94
261
|
return [];
|
|
95
262
|
}
|
|
96
263
|
}
|
|
97
|
-
|
|
98
|
-
|
|
264
|
+
function selectedArea(args) {
|
|
265
|
+
const index = args.lastIndexOf("--area");
|
|
266
|
+
if (index < 0) return void 0;
|
|
267
|
+
const value = args[index + 1];
|
|
268
|
+
return value && isPageArea(value) ? value : void 0;
|
|
269
|
+
}
|
|
270
|
+
function uniqueSorted(candidates) {
|
|
271
|
+
return [...new Set(candidates)].sort(
|
|
272
|
+
(left, right) => left.localeCompare(right)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
async function completionCandidates(words, context) {
|
|
276
|
+
if (words.length === 0) return [...PUBLIC_COMMANDS];
|
|
277
|
+
const [command, ...args] = words;
|
|
278
|
+
const previous = args.at(-1);
|
|
279
|
+
if (previous === "--area") return [...PAGE_AREAS];
|
|
280
|
+
const area = selectedArea(args);
|
|
281
|
+
const hasArea = args.includes("--area");
|
|
99
282
|
if (command === "rmpage") {
|
|
100
|
-
|
|
101
|
-
|
|
283
|
+
if (!area) return hasArea ? [] : ["--area"];
|
|
284
|
+
const catalog = await discoverPageCatalog(context.cwd, context.fs);
|
|
285
|
+
return uniqueSorted(
|
|
286
|
+
catalog.candidates.filter((candidate) => candidate.area === area).map((candidate) => candidate.logicalName)
|
|
102
287
|
);
|
|
103
288
|
}
|
|
104
|
-
if (command === "addcomponent"
|
|
105
|
-
|
|
106
|
-
|
|
289
|
+
if (command === "addcomponent") {
|
|
290
|
+
const pageOptionIndex = args.findIndex(
|
|
291
|
+
(argument) => argument === "--page" || argument === "-P"
|
|
292
|
+
);
|
|
293
|
+
if (previous === "--page" || previous === "-P") {
|
|
294
|
+
const catalog = await discoverPageCatalog(context.cwd, context.fs);
|
|
295
|
+
return uniqueSorted(
|
|
296
|
+
catalog.candidates.filter((candidate) => !area || candidate.area === area).map((candidate) => candidate.logicalName)
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return uniqueSorted([
|
|
300
|
+
...OPTIONS.addcomponent ?? [],
|
|
301
|
+
...pageOptionIndex >= 0 && !hasArea ? ["--area"] : []
|
|
302
|
+
]);
|
|
303
|
+
}
|
|
304
|
+
if (command === "addpage") {
|
|
305
|
+
return uniqueSorted([
|
|
306
|
+
...(OPTIONS.addpage ?? []).filter(
|
|
307
|
+
(candidate) => candidate !== "--area" || !hasArea
|
|
308
|
+
),
|
|
107
309
|
...await directories(path2.join(context.cwd, "src", "ui"), context)
|
|
108
|
-
];
|
|
310
|
+
]);
|
|
109
311
|
}
|
|
110
312
|
if (command === "addlanguage")
|
|
111
313
|
return ["de", "en", "es", "fr", "it", "ja", "pt"];
|
|
112
|
-
return OPTIONS[command] ?? [];
|
|
314
|
+
return uniqueSorted(OPTIONS[command] ?? []);
|
|
113
315
|
}
|
|
114
316
|
|
|
115
317
|
// src/cli/onboarding.ts
|
|
@@ -117,31 +319,6 @@ import path4 from "path";
|
|
|
117
319
|
|
|
118
320
|
// src/core/operations.ts
|
|
119
321
|
import path3 from "path";
|
|
120
|
-
|
|
121
|
-
// src/core/contracts.ts
|
|
122
|
-
var CliError = class extends Error {
|
|
123
|
-
exitCode;
|
|
124
|
-
code;
|
|
125
|
-
hint;
|
|
126
|
-
scope;
|
|
127
|
-
path;
|
|
128
|
-
constructor(message, options = {}) {
|
|
129
|
-
super(message);
|
|
130
|
-
this.name = "CliError";
|
|
131
|
-
if (typeof options === "number") {
|
|
132
|
-
this.exitCode = options;
|
|
133
|
-
this.code = "FILESYSTEM_ERROR";
|
|
134
|
-
} else {
|
|
135
|
-
this.exitCode = options.exitCode ?? 1;
|
|
136
|
-
this.code = options.code ?? "FILESYSTEM_ERROR";
|
|
137
|
-
this.hint = options.hint;
|
|
138
|
-
this.scope = options.scope;
|
|
139
|
-
this.path = options.path;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
// src/core/operations.ts
|
|
145
322
|
var OperationJournal = class {
|
|
146
323
|
#events = [];
|
|
147
324
|
record(event) {
|
|
@@ -755,18 +932,64 @@ var addApi = async (args, context) => {
|
|
|
755
932
|
|
|
756
933
|
// src/lib/addComponent.ts
|
|
757
934
|
import { join as join3 } from "path";
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
(
|
|
935
|
+
function formatGeneratedTranslations(content) {
|
|
936
|
+
return content.replace(
|
|
937
|
+
/^(\s*)(<(?:h2|p)\b[^>]*>)\{t\("([^"]+)"\)\}(<\/(?:h2|p)>)$/gm,
|
|
938
|
+
(line, indent, opening, key, closing) => line.length <= 80 ? line : `${indent}${opening}
|
|
939
|
+
${indent} {t("${key}")}
|
|
940
|
+
${indent}${closing}`
|
|
762
941
|
);
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
942
|
+
}
|
|
943
|
+
function parseAddComponentArguments(args) {
|
|
944
|
+
const parsedArea = parseAreaOption(args);
|
|
945
|
+
let componentName;
|
|
946
|
+
let pageScope;
|
|
947
|
+
for (let index = 1; index < parsedArea.args.length; index += 1) {
|
|
948
|
+
const argument = parsedArea.args[index];
|
|
949
|
+
if (argument === "-P" || argument === "--page") {
|
|
950
|
+
if (pageScope) {
|
|
951
|
+
throw new CliError("The --page option can only be provided once.", {
|
|
952
|
+
code: "INVALID_ARGUMENT"
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
const value = parsedArea.args[index + 1];
|
|
956
|
+
if (!value || value.startsWith("-")) {
|
|
957
|
+
throw new CliError("The --page option requires a page name.", {
|
|
958
|
+
code: "INVALID_ARGUMENT"
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
pageScope = value;
|
|
962
|
+
index += 1;
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (argument.startsWith("-")) {
|
|
966
|
+
throw new CliError(`Unknown addcomponent option: ${argument}.`, {
|
|
967
|
+
code: "INVALID_ARGUMENT"
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
if (componentName) {
|
|
971
|
+
throw new CliError(`Unexpected addcomponent argument: ${argument}.`, {
|
|
972
|
+
code: "INVALID_ARGUMENT"
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
componentName = argument;
|
|
976
|
+
}
|
|
977
|
+
if (pageScope && !parsedArea.area) {
|
|
978
|
+
requirePageArea(parsedArea.area, "addcomponent --page");
|
|
768
979
|
}
|
|
769
|
-
if (!
|
|
980
|
+
if (!pageScope && parsedArea.area) {
|
|
981
|
+
throw new CliError(
|
|
982
|
+
"The --area option is only valid with addcomponent --page.",
|
|
983
|
+
{ code: "INVALID_ARGUMENT" }
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
return { area: parsedArea.area, componentName, pageScope };
|
|
987
|
+
}
|
|
988
|
+
var addComponent = async (args, context) => {
|
|
989
|
+
const parsed = parseAddComponentArguments(args);
|
|
990
|
+
let { componentName } = parsed;
|
|
991
|
+
const { area, pageScope } = parsed;
|
|
992
|
+
if (!componentName) {
|
|
770
993
|
if (context.outputMode === "json") {
|
|
771
994
|
throw new CliError("Component name is required in JSON mode.", {
|
|
772
995
|
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
@@ -810,6 +1033,10 @@ var addComponent = async (args, context) => {
|
|
|
810
1033
|
hint: "Run this command from the generated project root."
|
|
811
1034
|
});
|
|
812
1035
|
}
|
|
1036
|
+
if (pageScope) {
|
|
1037
|
+
const catalog = await discoverPageCatalog(context.cwd, context.fs);
|
|
1038
|
+
resolvePageCandidate(catalog, pageScope, area);
|
|
1039
|
+
}
|
|
813
1040
|
const componentNameUpper = capitalize(toIdentifier(componentName));
|
|
814
1041
|
const templateRoot = join3(context.packageRoot, "templates", "Component");
|
|
815
1042
|
const componentTemplate = join3(templateRoot, "Component.tsx");
|
|
@@ -825,7 +1052,9 @@ var addComponent = async (args, context) => {
|
|
|
825
1052
|
await assertSafeTarget(context.cwd, targetDirectory, context.fs);
|
|
826
1053
|
const componentFile = join3(targetDirectory, `${componentNameUpper}.tsx`);
|
|
827
1054
|
const translationNamespace = pageScope ?? "_global_ui";
|
|
828
|
-
const componentContent = (
|
|
1055
|
+
const componentContent = formatGeneratedTranslations(
|
|
1056
|
+
(await context.fs.readText(componentTemplate)).replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationNamespace)
|
|
1057
|
+
);
|
|
829
1058
|
const preparedMessages = [];
|
|
830
1059
|
if (config.useI18n) {
|
|
831
1060
|
const messagesRoot = join3(context.cwd, "messages");
|
|
@@ -836,6 +1065,7 @@ var addComponent = async (args, context) => {
|
|
|
836
1065
|
path: "messages"
|
|
837
1066
|
});
|
|
838
1067
|
}
|
|
1068
|
+
await assertSafeTarget(context.cwd, messagesRoot, context.fs);
|
|
839
1069
|
const templateMessages = JSON.parse(
|
|
840
1070
|
await context.fs.readText(messagesTemplate)
|
|
841
1071
|
);
|
|
@@ -850,6 +1080,7 @@ var addComponent = async (args, context) => {
|
|
|
850
1080
|
for (const locale of locales) {
|
|
851
1081
|
const messageFile = pageScope ? pageSegments[0] : "_global_ui";
|
|
852
1082
|
const target = join3(messagesRoot, locale, `${messageFile}.json`);
|
|
1083
|
+
await assertSafeTarget(context.cwd, target, context.fs);
|
|
853
1084
|
let data = {};
|
|
854
1085
|
if (context.fs.exists(target)) {
|
|
855
1086
|
try {
|
|
@@ -896,17 +1127,20 @@ var addComponent = async (args, context) => {
|
|
|
896
1127
|
const gateway = new MutationGateway(context, context.cwd);
|
|
897
1128
|
await gateway.mkdir(targetDirectory, {
|
|
898
1129
|
role: "component-directory",
|
|
899
|
-
resource: "directory"
|
|
1130
|
+
resource: "directory",
|
|
1131
|
+
detail: area ? { area } : void 0
|
|
900
1132
|
});
|
|
901
1133
|
await gateway.write(componentFile, componentContent, {
|
|
902
1134
|
role: "ui-component",
|
|
903
|
-
preserveExisting: true
|
|
1135
|
+
preserveExisting: true,
|
|
1136
|
+
detail: area ? { area } : void 0
|
|
904
1137
|
});
|
|
905
1138
|
for (const item of preparedMessages) {
|
|
906
1139
|
if (item.exists) {
|
|
907
1140
|
gateway.unchanged(item.target, {
|
|
908
1141
|
role: "translation-messages",
|
|
909
1142
|
detail: {
|
|
1143
|
+
...area ? { area } : {},
|
|
910
1144
|
locale: item.locale,
|
|
911
1145
|
key: `${translationNamespace}.${componentNameUpper}`
|
|
912
1146
|
}
|
|
@@ -915,6 +1149,7 @@ var addComponent = async (args, context) => {
|
|
|
915
1149
|
await gateway.write(item.target, item.content, {
|
|
916
1150
|
role: "translation-messages",
|
|
917
1151
|
detail: {
|
|
1152
|
+
...area ? { area } : {},
|
|
918
1153
|
locale: item.locale,
|
|
919
1154
|
key: `${translationNamespace}.${componentNameUpper}`
|
|
920
1155
|
}
|
|
@@ -924,8 +1159,9 @@ var addComponent = async (args, context) => {
|
|
|
924
1159
|
const mutated = context.operations.snapshot().some((event) => event.action === "created" || event.action === "updated");
|
|
925
1160
|
return commandResult(context, {
|
|
926
1161
|
command: "addcomponent",
|
|
927
|
-
summary: mutated ? `Added component "${componentNameUpper}" ${pageScope ? `to page "${pageScope}"` : "globally"}.` : `Component "${componentNameUpper}" already exists and was preserved.`,
|
|
1162
|
+
summary: mutated ? `Added component "${componentNameUpper}" ${pageScope ? `to ${area} page "${pageScope}"` : "globally"}.` : pageScope ? `Component "${componentNameUpper}" already exists on ${area} page "${pageScope}" and was preserved.` : `Component "${componentNameUpper}" already exists and was preserved.`,
|
|
928
1163
|
projectRoot: context.cwd,
|
|
1164
|
+
data: pageScope ? { area, componentName: componentNameUpper, page: pageScope } : { componentName: componentNameUpper },
|
|
929
1165
|
nextSteps: mutated ? [
|
|
930
1166
|
{
|
|
931
1167
|
kind: "review",
|
|
@@ -1394,7 +1630,6 @@ var SHORT_FLAGS = {
|
|
|
1394
1630
|
};
|
|
1395
1631
|
function registerMessagesFile(content, locale, fileName) {
|
|
1396
1632
|
const importPath = `./${locale}/${fileName}.json`;
|
|
1397
|
-
if (content.includes(importPath)) return content;
|
|
1398
1633
|
const declarationIndex = content.indexOf("const messages =");
|
|
1399
1634
|
const registryMatch = content.match(/const messages = \{([\s\S]*?)\n\};/);
|
|
1400
1635
|
if (declarationIndex < 0 || !registryMatch) {
|
|
@@ -1408,19 +1643,30 @@ function registerMessagesFile(content, locale, fileName) {
|
|
|
1408
1643
|
);
|
|
1409
1644
|
}
|
|
1410
1645
|
const identifier = toIdentifier(fileName);
|
|
1646
|
+
const property = identifier === fileName ? ` ${identifier},` : ` ${JSON.stringify(fileName)}: ${identifier},`;
|
|
1647
|
+
if (content.includes(importPath)) {
|
|
1648
|
+
if (registryMatch[1].split(/\r?\n/).includes(property)) return content;
|
|
1649
|
+
const legacyProperty = new RegExp(`^\\s*${identifier},\\s*$`, "m");
|
|
1650
|
+
if (legacyProperty.test(registryMatch[1])) {
|
|
1651
|
+
return content.replace(legacyProperty, property);
|
|
1652
|
+
}
|
|
1653
|
+
const nextRegistry2 = registryMatch[0].replace(
|
|
1654
|
+
/\n\};$/,
|
|
1655
|
+
`
|
|
1656
|
+
${property}
|
|
1657
|
+
};`
|
|
1658
|
+
);
|
|
1659
|
+
return content.replace(registryMatch[0], nextRegistry2);
|
|
1660
|
+
}
|
|
1411
1661
|
const importStatement = `import ${identifier} from "${importPath}";
|
|
1412
1662
|
`;
|
|
1413
|
-
const nextRegistry = registryMatch[0].replace(
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
${identifier},
|
|
1417
|
-
};`
|
|
1418
|
-
);
|
|
1663
|
+
const nextRegistry = registryMatch[0].replace(/\n\};$/, `
|
|
1664
|
+
${property}
|
|
1665
|
+
};`);
|
|
1419
1666
|
return content.slice(0, declarationIndex) + importStatement + content.slice(declarationIndex).replace(registryMatch[0], nextRegistry);
|
|
1420
1667
|
}
|
|
1421
|
-
function selectedFlags(
|
|
1668
|
+
function selectedFlags(optionArguments) {
|
|
1422
1669
|
const selected = /* @__PURE__ */ new Set();
|
|
1423
|
-
const optionArguments = args.slice(2).filter((argument) => argument.startsWith("-"));
|
|
1424
1670
|
if (optionArguments.length === 0)
|
|
1425
1671
|
return /* @__PURE__ */ new Set(["layout", "page", "loading"]);
|
|
1426
1672
|
for (const argument of optionArguments) {
|
|
@@ -1446,23 +1692,61 @@ function selectedFlags(args) {
|
|
|
1446
1692
|
}
|
|
1447
1693
|
return selected;
|
|
1448
1694
|
}
|
|
1695
|
+
function parseAddPageArguments(args) {
|
|
1696
|
+
const parsedArea = parseAreaOption(args);
|
|
1697
|
+
let logicalName;
|
|
1698
|
+
const optionArguments = [];
|
|
1699
|
+
for (const argument of parsedArea.args.slice(1)) {
|
|
1700
|
+
if (argument.startsWith("-")) {
|
|
1701
|
+
optionArguments.push(argument);
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
if (logicalName) {
|
|
1705
|
+
throw new CliError(`Unexpected addpage argument: ${argument}.`, {
|
|
1706
|
+
code: "INVALID_ARGUMENT"
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
logicalName = argument;
|
|
1710
|
+
}
|
|
1711
|
+
return {
|
|
1712
|
+
area: parsedArea.area,
|
|
1713
|
+
logicalName,
|
|
1714
|
+
flags: selectedFlags(optionArguments)
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1449
1717
|
var addPage = async (args, context) => {
|
|
1450
|
-
|
|
1451
|
-
|
|
1718
|
+
const parsed = parseAddPageArguments(args);
|
|
1719
|
+
let { area, logicalName } = parsed;
|
|
1720
|
+
if (!logicalName) {
|
|
1452
1721
|
if (context.outputMode === "json") {
|
|
1453
1722
|
throw new CliError("Page name is required in JSON mode.", {
|
|
1454
1723
|
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
1455
1724
|
hint: "Pass a simple or Parent.Child page name after addpage."
|
|
1456
1725
|
});
|
|
1457
1726
|
}
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1727
|
+
const questions = [
|
|
1728
|
+
{
|
|
1729
|
+
type: "text",
|
|
1730
|
+
name: "pageName",
|
|
1731
|
+
message: "Page name to add:",
|
|
1732
|
+
validate: (name) => name ? true : "Page name is required"
|
|
1733
|
+
},
|
|
1734
|
+
...!area ? [
|
|
1735
|
+
{
|
|
1736
|
+
type: "select",
|
|
1737
|
+
name: "area",
|
|
1738
|
+
message: "Page area:",
|
|
1739
|
+
choices: [
|
|
1740
|
+
{ title: "public", value: "public" },
|
|
1741
|
+
{ title: "user", value: "user" }
|
|
1742
|
+
]
|
|
1743
|
+
}
|
|
1744
|
+
] : []
|
|
1745
|
+
];
|
|
1746
|
+
const response = await context.prompt(questions);
|
|
1464
1747
|
logicalName = String(response.pageName ?? "");
|
|
1465
|
-
|
|
1748
|
+
area = area ?? response.area;
|
|
1749
|
+
if (!logicalName || !area) {
|
|
1466
1750
|
context.operations.record({
|
|
1467
1751
|
action: "cancelled",
|
|
1468
1752
|
resource: "command",
|
|
@@ -1477,14 +1761,17 @@ var addPage = async (args, context) => {
|
|
|
1477
1761
|
status: "cancelled"
|
|
1478
1762
|
});
|
|
1479
1763
|
}
|
|
1764
|
+
} else {
|
|
1765
|
+
area = requirePageArea(area, "addpage");
|
|
1480
1766
|
}
|
|
1767
|
+
area = requirePageArea(area, "addpage");
|
|
1481
1768
|
const pageSegments = parseLogicalName(logicalName, "page name");
|
|
1482
1769
|
if (pageSegments.length > 2) {
|
|
1483
1770
|
throw new CliError("Nested pages support exactly Parent.Child.", {
|
|
1484
1771
|
code: "INVALID_ARGUMENT"
|
|
1485
1772
|
});
|
|
1486
1773
|
}
|
|
1487
|
-
const flags =
|
|
1774
|
+
const flags = parsed.flags;
|
|
1488
1775
|
const config = await loadConfig(context);
|
|
1489
1776
|
if (!config) {
|
|
1490
1777
|
throw new CliError("Configuration file cnp.config.json was not found.", {
|
|
@@ -1507,16 +1794,64 @@ var addPage = async (args, context) => {
|
|
|
1507
1794
|
path: useI18n ? "src/app/[locale]" : "src/app"
|
|
1508
1795
|
});
|
|
1509
1796
|
}
|
|
1797
|
+
const areaRoot = join6(appRoot, areaRouteGroup(area));
|
|
1798
|
+
if (!context.fs.exists(areaRoot)) {
|
|
1799
|
+
throw new CliError(`The ${area} page area was not found.`, {
|
|
1800
|
+
code: "CONFIG_NOT_FOUND",
|
|
1801
|
+
scope: "project",
|
|
1802
|
+
path: join6(
|
|
1803
|
+
useI18n ? "src/app/[locale]" : "src/app",
|
|
1804
|
+
areaRouteGroup(area)
|
|
1805
|
+
)
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1510
1808
|
const [parentName, childName] = pageSegments.length === 2 ? pageSegments : [void 0, void 0];
|
|
1511
1809
|
const leafName = childName ?? pageSegments[0];
|
|
1512
1810
|
const pageIdentifier = capitalize(toIdentifier(leafName));
|
|
1513
1811
|
const jsonFileName = parentName ?? leafName;
|
|
1514
1812
|
const uiDirectory = join6(context.cwd, "src", "ui", ...pageSegments);
|
|
1515
|
-
const routeDirectory = join6(
|
|
1813
|
+
const routeDirectory = join6(areaRoot, ...pageSegments);
|
|
1814
|
+
const catalog = await discoverPageCatalog(context.cwd, context.fs);
|
|
1815
|
+
assertConsistentLogicalRoute(catalog, logicalName);
|
|
1816
|
+
const existingOtherArea = catalog.candidates.find(
|
|
1817
|
+
(candidate) => candidate.logicalName === logicalName && candidate.area !== area
|
|
1818
|
+
);
|
|
1819
|
+
const otherArea = area === "public" ? "user" : "public";
|
|
1820
|
+
const otherAreaDirectory = join6(
|
|
1821
|
+
appRoot,
|
|
1822
|
+
areaRouteGroup(otherArea),
|
|
1823
|
+
...pageSegments
|
|
1824
|
+
);
|
|
1825
|
+
if (existingOtherArea || context.fs.exists(otherAreaDirectory)) {
|
|
1826
|
+
throw new CliError(
|
|
1827
|
+
`Page "${logicalName}" already belongs to the ${otherArea} area.`,
|
|
1828
|
+
{
|
|
1829
|
+
code: "TARGET_EXISTS",
|
|
1830
|
+
scope: "project",
|
|
1831
|
+
path: join6(
|
|
1832
|
+
useI18n ? "src/app/[locale]" : "src/app",
|
|
1833
|
+
areaRouteGroup(otherArea),
|
|
1834
|
+
...pageSegments
|
|
1835
|
+
)
|
|
1836
|
+
}
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
const legacyDirectory = join6(appRoot, ...pageSegments);
|
|
1840
|
+
if (context.fs.exists(legacyDirectory)) {
|
|
1841
|
+
throw new CliError(`Page route "${logicalName}" is ungrouped.`, {
|
|
1842
|
+
code: "INCONSISTENT_ROUTE",
|
|
1843
|
+
scope: "project",
|
|
1844
|
+
path: join6(useI18n ? "src/app/[locale]" : "src/app", ...pageSegments),
|
|
1845
|
+
hint: "Move the route into exactly one of the (public) or (user) areas before retrying."
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1516
1848
|
await assertSafeTarget(context.cwd, uiDirectory, context.fs);
|
|
1517
1849
|
await assertSafeTarget(context.cwd, routeDirectory, context.fs);
|
|
1518
1850
|
const templateRoot = join6(context.packageRoot, "templates", "Page");
|
|
1519
|
-
const uiTemplate = join6(
|
|
1851
|
+
const uiTemplate = join6(
|
|
1852
|
+
templateRoot,
|
|
1853
|
+
area === "user" ? "page-ui.user.tsx" : "page-ui.tsx"
|
|
1854
|
+
);
|
|
1520
1855
|
const routeTemplates = [...flags].map((flag) => ({
|
|
1521
1856
|
flag,
|
|
1522
1857
|
source: join6(templateRoot, toFileName(flag))
|
|
@@ -1551,6 +1886,7 @@ var addPage = async (args, context) => {
|
|
|
1551
1886
|
path: "messages"
|
|
1552
1887
|
});
|
|
1553
1888
|
}
|
|
1889
|
+
await assertSafeTarget(context.cwd, messagesRoot, context.fs);
|
|
1554
1890
|
const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
|
|
1555
1891
|
if (locales.length === 0) {
|
|
1556
1892
|
throw new CliError("No locale directories were found.", {
|
|
@@ -1562,6 +1898,8 @@ var addPage = async (args, context) => {
|
|
|
1562
1898
|
for (const locale of locales) {
|
|
1563
1899
|
const target = join6(messagesRoot, locale, `${jsonFileName}.json`);
|
|
1564
1900
|
const aggregator = join6(messagesRoot, `${locale}.ts`);
|
|
1901
|
+
await assertSafeTarget(context.cwd, target, context.fs);
|
|
1902
|
+
await assertSafeTarget(context.cwd, aggregator, context.fs);
|
|
1565
1903
|
if (!context.fs.exists(aggregator)) {
|
|
1566
1904
|
throw new CliError(
|
|
1567
1905
|
`Locale aggregator messages/${locale}.ts was not found.`,
|
|
@@ -1612,7 +1950,8 @@ var addPage = async (args, context) => {
|
|
|
1612
1950
|
const gateway = new MutationGateway(context, context.cwd);
|
|
1613
1951
|
await gateway.mkdir(uiDirectory, {
|
|
1614
1952
|
role: "page-ui-directory",
|
|
1615
|
-
resource: "directory"
|
|
1953
|
+
resource: "directory",
|
|
1954
|
+
detail: { area }
|
|
1616
1955
|
});
|
|
1617
1956
|
const uiFile = join6(uiDirectory, "page-ui.tsx");
|
|
1618
1957
|
const uiContent = (await context.fs.readText(uiTemplate)).replace(
|
|
@@ -1621,11 +1960,13 @@ var addPage = async (args, context) => {
|
|
|
1621
1960
|
).replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, pageIdentifier).replace(/Template/g, pageIdentifier);
|
|
1622
1961
|
await gateway.write(uiFile, uiContent, {
|
|
1623
1962
|
role: "page-ui",
|
|
1624
|
-
preserveExisting: true
|
|
1963
|
+
preserveExisting: true,
|
|
1964
|
+
detail: { area }
|
|
1625
1965
|
});
|
|
1626
1966
|
await gateway.mkdir(routeDirectory, {
|
|
1627
1967
|
role: "page-route-directory",
|
|
1628
|
-
resource: "directory"
|
|
1968
|
+
resource: "directory",
|
|
1969
|
+
detail: { area }
|
|
1629
1970
|
});
|
|
1630
1971
|
for (const template of routeTemplates) {
|
|
1631
1972
|
const filename = toFileName(template.flag);
|
|
@@ -1634,7 +1975,8 @@ var addPage = async (args, context) => {
|
|
|
1634
1975
|
const content = (await context.fs.readText(template.source)).replace("@/ui/template/", `@/ui/${uiImportPath}/`).replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, pageIdentifier).replace(/Template/g, pageIdentifier);
|
|
1635
1976
|
await gateway.write(target, content, {
|
|
1636
1977
|
role: `page-${template.flag}`,
|
|
1637
|
-
preserveExisting: true
|
|
1978
|
+
preserveExisting: true,
|
|
1979
|
+
detail: { area }
|
|
1638
1980
|
});
|
|
1639
1981
|
}
|
|
1640
1982
|
if (useI18n) {
|
|
@@ -1642,23 +1984,32 @@ var addPage = async (args, context) => {
|
|
|
1642
1984
|
if (item.messageExists) {
|
|
1643
1985
|
gateway.unchanged(item.target, {
|
|
1644
1986
|
role: "translation-messages",
|
|
1645
|
-
detail: {
|
|
1987
|
+
detail: {
|
|
1988
|
+
area,
|
|
1989
|
+
locale: item.locale,
|
|
1990
|
+
namespace: translationNamespace
|
|
1991
|
+
}
|
|
1646
1992
|
});
|
|
1647
1993
|
} else {
|
|
1648
1994
|
await gateway.write(item.target, item.targetContent, {
|
|
1649
1995
|
role: "translation-messages",
|
|
1650
|
-
detail: {
|
|
1996
|
+
detail: {
|
|
1997
|
+
area,
|
|
1998
|
+
locale: item.locale,
|
|
1999
|
+
namespace: translationNamespace
|
|
2000
|
+
}
|
|
1651
2001
|
});
|
|
1652
2002
|
}
|
|
1653
2003
|
await gateway.write(item.aggregator, item.aggregatorContent, {
|
|
1654
|
-
role: "locale-aggregator"
|
|
2004
|
+
role: "locale-aggregator",
|
|
2005
|
+
detail: { area }
|
|
1655
2006
|
});
|
|
1656
2007
|
}
|
|
1657
2008
|
} else {
|
|
1658
2009
|
gateway.skipped(join6(context.cwd, "messages"), {
|
|
1659
2010
|
role: "translation-messages",
|
|
1660
2011
|
resource: "directory",
|
|
1661
|
-
detail: { reason: "Internationalization is disabled." }
|
|
2012
|
+
detail: { area, reason: "Internationalization is disabled." }
|
|
1662
2013
|
});
|
|
1663
2014
|
}
|
|
1664
2015
|
const mutated = context.operations.snapshot().some(
|
|
@@ -1677,8 +2028,9 @@ var addPage = async (args, context) => {
|
|
|
1677
2028
|
];
|
|
1678
2029
|
return commandResult(context, {
|
|
1679
2030
|
command: "addpage",
|
|
1680
|
-
summary: mutated ? `Added page "${logicalName}" and its missing resources.` :
|
|
2031
|
+
summary: mutated ? `Added ${area} page "${logicalName}" and its missing resources.` : `${area[0].toUpperCase()}${area.slice(1)} page "${logicalName}" already exists and was preserved.`,
|
|
1681
2032
|
projectRoot: context.cwd,
|
|
2033
|
+
data: { area, logicalName },
|
|
1682
2034
|
nextSteps: mutated ? [
|
|
1683
2035
|
{
|
|
1684
2036
|
kind: "review",
|
|
@@ -1841,13 +2193,15 @@ function escapeRegExp(value) {
|
|
|
1841
2193
|
function unregisterMessagesFile(content, locale, fileName) {
|
|
1842
2194
|
const identifier = toIdentifier(fileName);
|
|
1843
2195
|
const escapedIdentifier = escapeRegExp(identifier);
|
|
2196
|
+
const escapedFileName = escapeRegExp(fileName);
|
|
1844
2197
|
const escapedPath = escapeRegExp(`./${locale}/${fileName}.json`);
|
|
1845
2198
|
const importPattern = new RegExp(
|
|
1846
2199
|
`^import\\s+${escapedIdentifier}\\s+from\\s+["']${escapedPath}["'];?\\r?\\n?`,
|
|
1847
2200
|
"m"
|
|
1848
2201
|
);
|
|
2202
|
+
const propertyExpression = identifier === fileName ? escapedIdentifier : `(?:["']${escapedFileName}["']\\s*:\\s*)?${escapedIdentifier}`;
|
|
1849
2203
|
const propertyPattern = new RegExp(
|
|
1850
|
-
`^\\s*${
|
|
2204
|
+
`^\\s*${propertyExpression},\\s*\\r?\\n?`,
|
|
1851
2205
|
"m"
|
|
1852
2206
|
);
|
|
1853
2207
|
const hasImport = importPattern.test(content);
|
|
@@ -1868,24 +2222,63 @@ function unregisterMessagesFile(content, locale, fileName) {
|
|
|
1868
2222
|
changed: true
|
|
1869
2223
|
};
|
|
1870
2224
|
}
|
|
2225
|
+
function parseRmPageArguments(args) {
|
|
2226
|
+
const parsedArea = parseAreaOption(args);
|
|
2227
|
+
let logicalName;
|
|
2228
|
+
for (const argument of parsedArea.args.slice(1)) {
|
|
2229
|
+
if (argument.startsWith("-")) {
|
|
2230
|
+
throw new CliError(`Unknown rmpage option: ${argument}.`, {
|
|
2231
|
+
code: "INVALID_ARGUMENT"
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
if (logicalName) {
|
|
2235
|
+
throw new CliError(`Unexpected rmpage argument: ${argument}.`, {
|
|
2236
|
+
code: "INVALID_ARGUMENT"
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
logicalName = argument;
|
|
2240
|
+
}
|
|
2241
|
+
return { area: parsedArea.area, logicalName };
|
|
2242
|
+
}
|
|
1871
2243
|
var rmPage = async (args, context) => {
|
|
1872
|
-
const
|
|
1873
|
-
|
|
1874
|
-
|
|
2244
|
+
const parsed = parseRmPageArguments(args);
|
|
2245
|
+
const catalog = await discoverPageCatalog(context.cwd, context.fs);
|
|
2246
|
+
let { area, logicalName } = parsed;
|
|
2247
|
+
let candidate;
|
|
2248
|
+
if (!logicalName) {
|
|
1875
2249
|
if (context.outputMode === "json") {
|
|
1876
2250
|
throw new CliError("Page name is required in JSON mode.", {
|
|
1877
2251
|
code: "INTERACTIVE_INPUT_REQUIRED",
|
|
1878
2252
|
hint: "Pass a page name returned by completion after rmpage."
|
|
1879
2253
|
});
|
|
1880
2254
|
}
|
|
2255
|
+
const candidates = area ? catalog.candidates.filter((entry) => entry.area === area) : catalog.candidates;
|
|
2256
|
+
if (candidates.length === 0) {
|
|
2257
|
+
const issue = catalog.issues[0];
|
|
2258
|
+
if (issue) {
|
|
2259
|
+
throw new CliError(
|
|
2260
|
+
`Page route "${issue.logicalName}" is inconsistent.`,
|
|
2261
|
+
{
|
|
2262
|
+
code: "INCONSISTENT_ROUTE",
|
|
2263
|
+
scope: "project",
|
|
2264
|
+
path: issue.routeDirectories.join(", "),
|
|
2265
|
+
hint: "Move the route into exactly one of the (public) or (user) areas before retrying."
|
|
2266
|
+
}
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2269
|
+
throw new CliError(
|
|
2270
|
+
area ? `No pages were found in the ${area} area.` : "No removable pages were found.",
|
|
2271
|
+
{ code: "TARGET_NOT_FOUND", scope: "project", path: "." }
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
1881
2274
|
const selected = await context.prompt([
|
|
1882
2275
|
{
|
|
1883
2276
|
type: "autocomplete",
|
|
1884
2277
|
name: "page",
|
|
1885
2278
|
message: "Page to remove:",
|
|
1886
2279
|
choices: candidates.map((candidate2) => ({
|
|
1887
|
-
title: candidate2.logicalName.replaceAll(".", " > ")
|
|
1888
|
-
value: candidate2.
|
|
2280
|
+
title: `${candidate2.area[0].toUpperCase()}${candidate2.area.slice(1)} > ${candidate2.logicalName.replaceAll(".", " > ")}`,
|
|
2281
|
+
value: candidate2.id
|
|
1889
2282
|
}))
|
|
1890
2283
|
},
|
|
1891
2284
|
{
|
|
@@ -1895,38 +2288,51 @@ var rmPage = async (args, context) => {
|
|
|
1895
2288
|
initial: false
|
|
1896
2289
|
}
|
|
1897
2290
|
]);
|
|
2291
|
+
const selectedId = String(selected.page ?? "");
|
|
2292
|
+
const selectedCandidate = candidates.find(
|
|
2293
|
+
(entry) => entry.id === selectedId
|
|
2294
|
+
);
|
|
1898
2295
|
if (!selected.confirm) {
|
|
1899
2296
|
context.operations.record({
|
|
1900
2297
|
action: "cancelled",
|
|
1901
2298
|
resource: "command",
|
|
1902
2299
|
role: "page-removal",
|
|
1903
2300
|
scope: "project",
|
|
1904
|
-
path: "."
|
|
2301
|
+
path: ".",
|
|
2302
|
+
detail: selectedCandidate ? { area: selectedCandidate.area } : area ? { area } : void 0
|
|
1905
2303
|
});
|
|
1906
2304
|
return commandResult(context, {
|
|
1907
2305
|
command: "rmpage",
|
|
1908
|
-
summary: "Page deletion was cancelled.",
|
|
2306
|
+
summary: selectedCandidate ? `Deletion of ${selectedCandidate.area} page "${selectedCandidate.logicalName}" was cancelled.` : area ? `Page deletion in the ${area} area was cancelled.` : "Page deletion was cancelled.",
|
|
1909
2307
|
projectRoot: context.cwd,
|
|
1910
|
-
status: "cancelled"
|
|
2308
|
+
status: "cancelled",
|
|
2309
|
+
data: selectedCandidate ? {
|
|
2310
|
+
area: selectedCandidate.area,
|
|
2311
|
+
logicalName: selectedCandidate.logicalName
|
|
2312
|
+
} : area ? { area } : void 0
|
|
1911
2313
|
});
|
|
1912
2314
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2315
|
+
if (!selectedCandidate) {
|
|
2316
|
+
throw new CliError("The selected page is not in the page catalog.", {
|
|
2317
|
+
code: "INVALID_ARGUMENT"
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
candidate = selectedCandidate;
|
|
2321
|
+
logicalName = candidate.logicalName;
|
|
2322
|
+
area = candidate.area;
|
|
2323
|
+
} else {
|
|
2324
|
+
area = requirePageArea(area, "rmpage");
|
|
2325
|
+
parseLogicalName(logicalName, "page name");
|
|
2326
|
+
candidate = resolvePageCandidate(catalog, logicalName, area);
|
|
1925
2327
|
}
|
|
2328
|
+
area = requirePageArea(area, "rmpage");
|
|
1926
2329
|
const messagesRoot = resolveInside(context.cwd, "messages");
|
|
2330
|
+
await assertSafeTarget(context.cwd, candidate.uiDirectory, context.fs);
|
|
2331
|
+
await assertSafeTarget(context.cwd, candidate.routeDirectory, context.fs);
|
|
1927
2332
|
const preparedMessages = [];
|
|
1928
2333
|
const preparedAggregators = [];
|
|
1929
2334
|
if (context.fs.exists(messagesRoot)) {
|
|
2335
|
+
await assertSafeTarget(context.cwd, messagesRoot, context.fs);
|
|
1930
2336
|
const locales = (await context.fs.list(messagesRoot)).filter((entry) => entry.isDirectory).map((entry) => entry.name).sort();
|
|
1931
2337
|
for (const locale of locales) {
|
|
1932
2338
|
const target = resolveInside(
|
|
@@ -1935,12 +2341,14 @@ var rmPage = async (args, context) => {
|
|
|
1935
2341
|
locale,
|
|
1936
2342
|
`${candidate.routeSegments[0]}.json`
|
|
1937
2343
|
);
|
|
2344
|
+
await assertSafeTarget(context.cwd, target, context.fs);
|
|
1938
2345
|
if (!candidate.messageKey) {
|
|
1939
2346
|
const aggregator = resolveInside(
|
|
1940
2347
|
context.cwd,
|
|
1941
2348
|
"messages",
|
|
1942
2349
|
`${locale}.ts`
|
|
1943
2350
|
);
|
|
2351
|
+
await assertSafeTarget(context.cwd, aggregator, context.fs);
|
|
1944
2352
|
if (!context.fs.exists(aggregator)) {
|
|
1945
2353
|
throw new CliError(
|
|
1946
2354
|
`Locale aggregator messages/${locale}.ts was not found.`,
|
|
@@ -1993,33 +2401,41 @@ var rmPage = async (args, context) => {
|
|
|
1993
2401
|
const gateway = new MutationGateway(context, context.cwd);
|
|
1994
2402
|
for (const prepared of preparedMessages) {
|
|
1995
2403
|
if (!candidate.messageKey) {
|
|
1996
|
-
await gateway.remove(prepared.target, {
|
|
2404
|
+
await gateway.remove(prepared.target, {
|
|
2405
|
+
role: "translation-messages",
|
|
2406
|
+
detail: { area }
|
|
2407
|
+
});
|
|
1997
2408
|
} else if (prepared.keyPresent) {
|
|
1998
2409
|
await gateway.write(prepared.target, prepared.content, {
|
|
1999
2410
|
role: "translation-messages",
|
|
2000
|
-
detail: { removedKey: candidate.messageKey }
|
|
2411
|
+
detail: { area, removedKey: candidate.messageKey }
|
|
2001
2412
|
});
|
|
2002
2413
|
} else {
|
|
2003
2414
|
gateway.unchanged(prepared.target, {
|
|
2004
2415
|
role: "translation-messages",
|
|
2005
|
-
detail: { missingKey: candidate.messageKey }
|
|
2416
|
+
detail: { area, missingKey: candidate.messageKey }
|
|
2006
2417
|
});
|
|
2007
2418
|
}
|
|
2008
2419
|
}
|
|
2009
2420
|
for (const prepared of preparedAggregators) {
|
|
2010
2421
|
if (prepared.changed) {
|
|
2011
2422
|
await gateway.write(prepared.target, prepared.content, {
|
|
2012
|
-
role: "locale-aggregator"
|
|
2423
|
+
role: "locale-aggregator",
|
|
2424
|
+
detail: { area }
|
|
2013
2425
|
});
|
|
2014
2426
|
} else {
|
|
2015
|
-
gateway.unchanged(prepared.target, {
|
|
2427
|
+
gateway.unchanged(prepared.target, {
|
|
2428
|
+
role: "locale-aggregator",
|
|
2429
|
+
detail: { area }
|
|
2430
|
+
});
|
|
2016
2431
|
}
|
|
2017
2432
|
}
|
|
2018
2433
|
await gateway.remove(
|
|
2019
2434
|
candidate.uiDirectory,
|
|
2020
2435
|
{
|
|
2021
2436
|
role: "page-ui",
|
|
2022
|
-
resource: "directory"
|
|
2437
|
+
resource: "directory",
|
|
2438
|
+
detail: { area }
|
|
2023
2439
|
},
|
|
2024
2440
|
{ recursive: true, force: false }
|
|
2025
2441
|
);
|
|
@@ -2027,15 +2443,17 @@ var rmPage = async (args, context) => {
|
|
|
2027
2443
|
candidate.routeDirectory,
|
|
2028
2444
|
{
|
|
2029
2445
|
role: "page-route",
|
|
2030
|
-
resource: "directory"
|
|
2446
|
+
resource: "directory",
|
|
2447
|
+
detail: { area }
|
|
2031
2448
|
},
|
|
2032
2449
|
{ recursive: true, force: false }
|
|
2033
2450
|
);
|
|
2034
2451
|
const mutated = context.operations.snapshot().some((event) => event.action === "deleted" || event.action === "updated");
|
|
2035
2452
|
return commandResult(context, {
|
|
2036
2453
|
command: "rmpage",
|
|
2037
|
-
summary: mutated ? `Deleted page "${logicalName}" and its associated resources.` : `No resources remained for page "${logicalName}".`,
|
|
2454
|
+
summary: mutated ? `Deleted ${area} page "${logicalName}" and its associated resources.` : `No resources remained for ${area} page "${logicalName}".`,
|
|
2038
2455
|
projectRoot: context.cwd,
|
|
2456
|
+
data: { area, logicalName },
|
|
2039
2457
|
nextSteps: mutated ? [
|
|
2040
2458
|
{
|
|
2041
2459
|
kind: "run-checks",
|
|
@@ -2145,6 +2563,7 @@ async function validateScaffoldTemplate(root, fs) {
|
|
|
2145
2563
|
"bun.lock",
|
|
2146
2564
|
"pnpm-workspace.yaml",
|
|
2147
2565
|
"vitest.config.ts",
|
|
2566
|
+
path8.join("scripts", "audit-policy.ts"),
|
|
2148
2567
|
path8.join("scripts", "audit.ts"),
|
|
2149
2568
|
path8.join("scripts", "package-manager.ts"),
|
|
2150
2569
|
path8.join("tests", "consumer", "validate-template.ts"),
|
|
@@ -2581,18 +3000,19 @@ var HELP_TEXT = `create-next-pro
|
|
|
2581
3000
|
|
|
2582
3001
|
Usage:
|
|
2583
3002
|
create-next-pro <project-name> [--force] [--json]
|
|
2584
|
-
create-next-pro addpage [options] [--json]
|
|
2585
|
-
create-next-pro addcomponent [
|
|
3003
|
+
create-next-pro addpage [name] --area <public|user> [options] [--json]
|
|
3004
|
+
create-next-pro addcomponent [name] [--page <page> --area <public|user>] [--json]
|
|
2586
3005
|
create-next-pro addlib [name] [--json]
|
|
2587
3006
|
create-next-pro addapi [name] [--json]
|
|
2588
3007
|
create-next-pro addlanguage [locale] [--json]
|
|
2589
3008
|
create-next-pro addtext <path> [text] [--json]
|
|
2590
|
-
create-next-pro rmpage [
|
|
3009
|
+
create-next-pro rmpage [name] [--area <public|user>] [--json]
|
|
2591
3010
|
|
|
2592
3011
|
Options:
|
|
2593
3012
|
--help Show this help message
|
|
2594
3013
|
--version Show the CLI version
|
|
2595
3014
|
--json Emit one deterministic JSON document
|
|
3015
|
+
--area Select the public or user area for page operations
|
|
2596
3016
|
--reconfigure Run the configuration assistant again
|
|
2597
3017
|
`;
|
|
2598
3018
|
function parseOptions(args) {
|
|
@@ -2680,7 +3100,10 @@ async function executeCli(context) {
|
|
|
2680
3100
|
}
|
|
2681
3101
|
async function runCompletion(context, args) {
|
|
2682
3102
|
try {
|
|
2683
|
-
for (const candidate of await completionCandidates(
|
|
3103
|
+
for (const candidate of await completionCandidates(
|
|
3104
|
+
args.slice(1),
|
|
3105
|
+
context
|
|
3106
|
+
)) {
|
|
2684
3107
|
context.terminal.log(candidate);
|
|
2685
3108
|
}
|
|
2686
3109
|
return 0;
|