create-next-pro-cli 0.1.25 → 0.1.27
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 +205 -292
- package/create-next-pro-completion.sh +8 -38
- package/create-next-pro-completion.zsh +13 -0
- package/dist/bin.bun.js +1127 -628
- package/dist/bin.node.js +1170 -617
- package/dist/bin.node.js.map +1 -1
- package/dist/create-next-pro +35 -6
- package/package.json +49 -27
- package/templates/Projects/default/.env.example +17 -0
- package/templates/Projects/default/.github/workflows/quality.yml +24 -0
- package/templates/Projects/default/.gitignore.template +47 -0
- package/templates/Projects/default/.prettierignore +3 -0
- package/templates/Projects/default/README.md +66 -21
- package/templates/Projects/default/bun.lock +1152 -0
- package/templates/Projects/default/eslint.config.mjs +27 -11
- package/templates/Projects/default/messages/en/_global_ui.json +23 -10
- package/templates/Projects/default/messages/en.ts +17 -2
- package/templates/Projects/default/messages/fr/_global_ui.json +23 -10
- package/templates/Projects/default/messages/fr.ts +17 -2
- package/templates/Projects/default/next.config.ts +43 -3
- package/templates/Projects/default/package.json +42 -24
- package/templates/Projects/default/playwright.config.ts +26 -0
- package/templates/Projects/default/pnpm-workspace.yaml +5 -0
- package/templates/Projects/default/public/{cnp-logo.svg → logo.svg} +1 -1
- package/templates/Projects/default/src/app/[locale]/(public)/layout.tsx +8 -1
- package/templates/Projects/default/src/app/[locale]/(public)/login/page.tsx +8 -0
- package/templates/Projects/default/src/app/[locale]/(public)/register/page.tsx +8 -0
- package/templates/Projects/default/src/app/[locale]/(user)/dashboard/error.tsx +25 -0
- package/templates/Projects/default/src/app/[locale]/(user)/dashboard/loading.tsx +5 -0
- package/templates/Projects/default/src/app/[locale]/(user)/{Dashboard → dashboard}/page.tsx +1 -1
- package/templates/Projects/default/src/app/[locale]/(user)/layout.tsx +24 -2
- package/templates/Projects/default/src/app/[locale]/(user)/settings/loading.tsx +5 -0
- package/templates/Projects/default/src/app/[locale]/(user)/{Settings → settings}/page.tsx +1 -2
- package/templates/Projects/default/src/app/[locale]/(user)/userInfo/loading.tsx +5 -0
- package/templates/Projects/default/src/app/[locale]/(user)/{UserInfo → userInfo}/page.tsx +1 -2
- package/templates/Projects/default/src/app/[locale]/layout.tsx +34 -10
- package/templates/Projects/default/src/app/[locale]/loading.tsx +0 -9
- package/templates/Projects/default/src/app/[locale]/not-found.tsx +6 -15
- package/templates/Projects/default/src/app/[locale]/page.tsx +10 -1
- package/templates/Projects/default/src/app/api/auth/[...nextauth]/route.ts +8 -60
- package/templates/Projects/default/src/app/not-found.tsx +1 -1
- package/templates/Projects/default/src/app/sitemap.ts +2 -2
- package/templates/Projects/default/src/app/styles/globals.css +166 -113
- package/templates/Projects/default/src/auth.ts +20 -0
- package/templates/Projects/default/src/config.ts +3 -3
- package/templates/Projects/default/src/env.ts +65 -0
- package/templates/Projects/default/src/lib/i18n/messages.ts +8 -0
- package/templates/Projects/default/src/lib/i18n/request.ts +2 -16
- package/templates/Projects/default/src/lib/security/csp.ts +16 -0
- package/templates/Projects/default/src/lib/utils.ts +2 -1
- package/templates/Projects/default/src/proxy.ts +13 -0
- package/templates/Projects/default/src/ui/_global/BackButton.tsx +4 -2
- package/templates/Projects/default/src/ui/_global/Button.tsx +3 -8
- package/templates/Projects/default/src/ui/_global/GlobalHeader.tsx +10 -28
- package/templates/Projects/default/src/ui/_global/GlobalMain.tsx +1 -1
- package/templates/Projects/default/src/ui/_global/LocaleSwitcher.tsx +9 -10
- package/templates/Projects/default/src/ui/_global/PublicNav.tsx +51 -17
- package/templates/Projects/default/src/ui/_global/ThemeToggle.tsx +45 -39
- package/templates/Projects/default/src/ui/_global/UserNav.tsx +6 -6
- package/templates/Projects/default/src/ui/_home/page-ui.tsx +5 -7
- package/templates/Projects/default/src/ui/{Dashboard → dashboard}/LogoutButton.tsx +9 -7
- package/templates/Projects/default/src/ui/{Dashboard → dashboard}/StatsCard.tsx +4 -4
- package/templates/Projects/default/src/ui/{Dashboard → dashboard}/page-ui.tsx +7 -8
- package/templates/Projects/default/src/ui/login/page-ui.tsx +36 -0
- package/templates/Projects/default/src/ui/register/page-ui.tsx +38 -0
- package/templates/Projects/default/src/ui/settings/page-ui.tsx +15 -0
- package/templates/Projects/default/src/ui/userInfo/page-ui.tsx +15 -0
- package/templates/Projects/default/tailwind.config.ts +81 -1
- package/templates/Projects/default/tests/consumer/validate-template.ts +66 -0
- package/templates/Projects/default/tests/e2e/template-remediation.playwright.ts +106 -0
- package/templates/Projects/default/tests/rendering/verify-rendering.ts +56 -0
- package/templates/Projects/default/tests/unit/csp.test.ts +19 -0
- package/templates/Projects/default/tests/unit/env.test.ts +76 -0
- package/templates/Projects/default/tsconfig.json +6 -6
- package/templates/Projects/default/messages/getMergedMessages.ts +0 -31
- package/templates/Projects/default/middleware.ts +0 -11
- package/templates/Projects/default/src/app/[locale]/(public)/Login/page.tsx +0 -6
- package/templates/Projects/default/src/app/[locale]/(public)/Register/page.tsx +0 -6
- package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/error.tsx +0 -38
- package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/loading.tsx +0 -10
- package/templates/Projects/default/src/app/[locale]/(user)/Settings/loading.tsx +0 -17
- package/templates/Projects/default/src/app/[locale]/(user)/UserInfo/loading.tsx +0 -17
- package/templates/Projects/default/src/app/api/auth/post-login/route.ts +0 -26
- package/templates/Projects/default/src/app/api/hello/route.ts +0 -5
- package/templates/Projects/default/src/app/layout.tsx +0 -11
- package/templates/Projects/default/src/app/page.tsx +0 -6
- package/templates/Projects/default/src/auth.config.ts +0 -0
- package/templates/Projects/default/src/lib/auth/disconnect.ts +0 -11
- package/templates/Projects/default/src/lib/auth/isConnected.ts +0 -18
- package/templates/Projects/default/src/lib/sample/example.ts +0 -3
- package/templates/Projects/default/src/lib/sample/index.ts +0 -3
- package/templates/Projects/default/src/ui/Login/page-ui.tsx +0 -22
- package/templates/Projects/default/src/ui/Register/page-ui.tsx +0 -26
- package/templates/Projects/default/src/ui/Settings/page-ui.tsx +0 -17
- package/templates/Projects/default/src/ui/UserInfo/page-ui.tsx +0 -17
- /package/templates/Projects/default/messages/en/{Dashboard.json → dashboard.json} +0 -0
- /package/templates/Projects/default/messages/en/{Login.json → login.json} +0 -0
- /package/templates/Projects/default/messages/en/{Register.json → register.json} +0 -0
- /package/templates/Projects/default/messages/en/{Settings.json → settings.json} +0 -0
- /package/templates/Projects/default/messages/en/{UserInfo.json → userInfo.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Dashboard.json → dashboard.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Login.json → login.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Register.json → register.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Settings.json → settings.json} +0 -0
- /package/templates/Projects/default/messages/fr/{UserInfo.json → userInfo.json} +0 -0
- /package/templates/Projects/default/public/{cnp-logo.png → logo.png} +0 -0
- /package/templates/Projects/default/src/ui/{Dashboard → dashboard}/WelcomeCard.tsx +0 -0
package/dist/bin.node.js
CHANGED
|
@@ -1,17 +1,241 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import path8 from "path";
|
|
5
|
+
|
|
6
|
+
// src/cli/onboarding.ts
|
|
6
7
|
import path from "path";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
function configDirectory(context) {
|
|
9
|
+
return context.env.XDG_CONFIG_HOME ? path.join(context.env.XDG_CONFIG_HOME, "create-next-pro") : path.join(context.homeDir, ".config", "create-next-pro");
|
|
10
|
+
}
|
|
11
|
+
function configFile(context) {
|
|
12
|
+
return path.join(configDirectory(context), "config.json");
|
|
13
|
+
}
|
|
14
|
+
async function readConfig(context) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(
|
|
17
|
+
await context.fs.readText(configFile(context))
|
|
18
|
+
);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function ensureLineInRc(context, target, line) {
|
|
24
|
+
try {
|
|
25
|
+
const current = context.fs.exists(target) ? await context.fs.readText(target) : "";
|
|
26
|
+
if (!current.includes(line))
|
|
27
|
+
await context.fs.appendText(target, `
|
|
28
|
+
${line}
|
|
29
|
+
`);
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function installCompletion(context, shell) {
|
|
34
|
+
const directory = configDirectory(context);
|
|
35
|
+
const source = path.join(
|
|
36
|
+
context.packageRoot,
|
|
37
|
+
shell === "zsh" ? "create-next-pro-completion.zsh" : "create-next-pro-completion.sh"
|
|
38
|
+
);
|
|
39
|
+
const target = path.join(
|
|
40
|
+
directory,
|
|
41
|
+
`completion.${shell === "zsh" ? "zsh" : "sh"}`
|
|
42
|
+
);
|
|
43
|
+
await context.fs.mkdir(directory);
|
|
44
|
+
await context.fs.copyFile(source, target);
|
|
45
|
+
const rcFile = path.join(
|
|
46
|
+
context.homeDir,
|
|
47
|
+
shell === "zsh" ? ".zshrc" : ".bashrc"
|
|
48
|
+
);
|
|
49
|
+
await ensureLineInRc(context, rcFile, `source "${target}"`);
|
|
50
|
+
}
|
|
51
|
+
async function onboarding(context, version) {
|
|
52
|
+
context.terminal.log(`\u{1F680} Welcome to create-next-pro v${version}
|
|
53
|
+
`);
|
|
54
|
+
const response = await context.prompt(
|
|
55
|
+
[
|
|
56
|
+
{
|
|
57
|
+
type: "select",
|
|
58
|
+
name: "shell",
|
|
59
|
+
message: "Which shell do you use?",
|
|
60
|
+
choices: [
|
|
61
|
+
{ title: "zsh", value: "zsh" },
|
|
62
|
+
{ title: "bash", value: "bash" }
|
|
63
|
+
],
|
|
64
|
+
initial: context.env.SHELL?.includes("zsh") ? 0 : 1
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: "toggle",
|
|
68
|
+
name: "completion",
|
|
69
|
+
message: "Install autocompletion?",
|
|
70
|
+
initial: true,
|
|
71
|
+
active: "Yes",
|
|
72
|
+
inactive: "No"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
{ onCancel: () => false }
|
|
76
|
+
);
|
|
77
|
+
if (response.shell !== "bash" && response.shell !== "zsh") {
|
|
78
|
+
throw new Error("Configuration cancelled.");
|
|
79
|
+
}
|
|
80
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
81
|
+
const config = {
|
|
82
|
+
version: 1,
|
|
83
|
+
shell: response.shell,
|
|
84
|
+
completionInstalled: Boolean(response.completion),
|
|
85
|
+
createdAt: now,
|
|
86
|
+
updatedAt: now
|
|
87
|
+
};
|
|
88
|
+
if (config.completionInstalled)
|
|
89
|
+
await installCompletion(context, config.shell);
|
|
90
|
+
await context.fs.mkdir(configDirectory(context));
|
|
91
|
+
await context.fs.writeText(
|
|
92
|
+
configFile(context),
|
|
93
|
+
JSON.stringify(config, null, 2)
|
|
94
|
+
);
|
|
95
|
+
context.terminal.log("\n\u2705 Configuration saved.");
|
|
96
|
+
context.terminal.log("you can now use the CLI ! ex : ");
|
|
97
|
+
context.terminal.log(" Without prompt (will change in future) :");
|
|
98
|
+
context.terminal.log(" create-next-pro my-next-project");
|
|
99
|
+
context.terminal.log(" With prompt :");
|
|
100
|
+
context.terminal.log(" create-next-pro");
|
|
101
|
+
context.terminal.log(
|
|
102
|
+
"For more information, visit: https://github.com/Rising-Corporation/create-next-pro-cli"
|
|
103
|
+
);
|
|
104
|
+
context.terminal.log("Happy coding! \u{1F389}");
|
|
105
|
+
return config;
|
|
106
|
+
}
|
|
10
107
|
|
|
11
|
-
// src/
|
|
108
|
+
// src/cli/completion.ts
|
|
109
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
110
|
+
import path3 from "path";
|
|
111
|
+
|
|
112
|
+
// src/core/page-catalog.ts
|
|
113
|
+
import { readdir } from "fs/promises";
|
|
114
|
+
import path2 from "path";
|
|
115
|
+
function isRouteGroup(segment) {
|
|
116
|
+
return segment.startsWith("(") && segment.endsWith(")");
|
|
117
|
+
}
|
|
118
|
+
async function discoverPages(projectRoot) {
|
|
119
|
+
const appRoot = path2.join(projectRoot, "src", "app", "[locale]");
|
|
120
|
+
const candidates = [];
|
|
121
|
+
async function visit(directory, relative = []) {
|
|
122
|
+
let entries;
|
|
123
|
+
try {
|
|
124
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
125
|
+
} catch {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (entries.some((entry) => entry.isFile() && entry.name === "page.tsx")) {
|
|
129
|
+
const routeSegments = relative.filter(
|
|
130
|
+
(segment) => !isRouteGroup(segment)
|
|
131
|
+
);
|
|
132
|
+
if (routeSegments.length > 0 && !routeSegments.some(
|
|
133
|
+
(segment) => segment.startsWith("_") || segment.startsWith("[")
|
|
134
|
+
)) {
|
|
135
|
+
const logicalName = routeSegments.join(".");
|
|
136
|
+
candidates.push({
|
|
137
|
+
logicalName,
|
|
138
|
+
routeSegments,
|
|
139
|
+
routeDirectory: directory,
|
|
140
|
+
uiDirectory: path2.join(projectRoot, "src", "ui", ...routeSegments),
|
|
141
|
+
messageFile: path2.join(
|
|
142
|
+
projectRoot,
|
|
143
|
+
"messages",
|
|
144
|
+
"{locale}",
|
|
145
|
+
`${routeSegments[0]}.json`
|
|
146
|
+
),
|
|
147
|
+
messageKey: routeSegments.length > 1 ? routeSegments.at(-1) : void 0
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
153
|
+
await visit(path2.join(directory, entry.name), [
|
|
154
|
+
...relative,
|
|
155
|
+
entry.name
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
await visit(appRoot);
|
|
161
|
+
return candidates.sort(
|
|
162
|
+
(left, right) => left.logicalName.localeCompare(right.logicalName)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/cli/completion.ts
|
|
167
|
+
var PUBLIC_COMMANDS = [
|
|
168
|
+
"addpage",
|
|
169
|
+
"addcomponent",
|
|
170
|
+
"addlib",
|
|
171
|
+
"addapi",
|
|
172
|
+
"addlanguage",
|
|
173
|
+
"addtext",
|
|
174
|
+
"rmpage",
|
|
175
|
+
"--help",
|
|
176
|
+
"--version",
|
|
177
|
+
"--reconfigure"
|
|
178
|
+
];
|
|
179
|
+
var OPTIONS = {
|
|
180
|
+
addpage: [
|
|
181
|
+
"--layout",
|
|
182
|
+
"--page",
|
|
183
|
+
"--loading",
|
|
184
|
+
"--not-found",
|
|
185
|
+
"--error",
|
|
186
|
+
"--global-error",
|
|
187
|
+
"--route",
|
|
188
|
+
"--template",
|
|
189
|
+
"--default"
|
|
190
|
+
],
|
|
191
|
+
addcomponent: ["--page", "-P"]
|
|
192
|
+
};
|
|
193
|
+
async function directories(root) {
|
|
194
|
+
try {
|
|
195
|
+
return (await readdir2(root, { withFileTypes: true })).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name).sort();
|
|
196
|
+
} catch {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function completionCandidates(command, context) {
|
|
201
|
+
if (!command) return [...PUBLIC_COMMANDS];
|
|
202
|
+
if (command === "rmpage") {
|
|
203
|
+
return (await discoverPages(context.cwd)).map(
|
|
204
|
+
(candidate) => candidate.logicalName
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (command === "addcomponent" || command === "addpage") {
|
|
208
|
+
return [
|
|
209
|
+
...OPTIONS[command] ?? [],
|
|
210
|
+
...await directories(path3.join(context.cwd, "src", "ui"))
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
if (command === "addlanguage")
|
|
214
|
+
return ["de", "en", "es", "fr", "it", "ja", "pt"];
|
|
215
|
+
return OPTIONS[command] ?? [];
|
|
216
|
+
}
|
|
217
|
+
async function printCompletions(args, context) {
|
|
218
|
+
for (const candidate of await completionCandidates(args[1], context)) {
|
|
219
|
+
context.terminal.log(candidate);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/core/contracts.ts
|
|
224
|
+
var success = () => ({ exitCode: 0 });
|
|
225
|
+
var CliError = class extends Error {
|
|
226
|
+
constructor(message, exitCode = 1) {
|
|
227
|
+
super(message);
|
|
228
|
+
this.exitCode = exitCode;
|
|
229
|
+
this.name = "CliError";
|
|
230
|
+
}
|
|
231
|
+
exitCode;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// src/lib/addApi.ts
|
|
12
235
|
import { join as join2 } from "path";
|
|
13
|
-
import { mkdir, readFile as
|
|
14
|
-
import
|
|
236
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
237
|
+
import prompts2 from "prompts";
|
|
238
|
+
import { existsSync as existsSync3 } from "fs";
|
|
15
239
|
|
|
16
240
|
// src/lib/utils.ts
|
|
17
241
|
import { readFile } from "fs/promises";
|
|
@@ -20,8 +244,12 @@ import { join } from "path";
|
|
|
20
244
|
function capitalize(str) {
|
|
21
245
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
22
246
|
}
|
|
23
|
-
|
|
24
|
-
const
|
|
247
|
+
function configuredAliasPrefix(config) {
|
|
248
|
+
const alias = config.importAlias ?? "@/*";
|
|
249
|
+
return alias.endsWith("/*") ? alias.slice(0, -2) : "@";
|
|
250
|
+
}
|
|
251
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
252
|
+
const configPath = join(cwd, "cnp.config.json");
|
|
25
253
|
if (!existsSync(configPath)) return null;
|
|
26
254
|
try {
|
|
27
255
|
const raw = await readFile(configPath, "utf-8");
|
|
@@ -55,21 +283,212 @@ function toFileName(key) {
|
|
|
55
283
|
}
|
|
56
284
|
}
|
|
57
285
|
|
|
286
|
+
// src/runtime/node-context.ts
|
|
287
|
+
import { existsSync as existsSync2 } from "fs";
|
|
288
|
+
import {
|
|
289
|
+
appendFile,
|
|
290
|
+
copyFile,
|
|
291
|
+
mkdir,
|
|
292
|
+
readFile as readFile2,
|
|
293
|
+
writeFile
|
|
294
|
+
} from "fs/promises";
|
|
295
|
+
import os from "os";
|
|
296
|
+
import path4 from "path";
|
|
297
|
+
import { fileURLToPath } from "url";
|
|
298
|
+
import prompts from "prompts";
|
|
299
|
+
function findPackageRoot(start) {
|
|
300
|
+
let current = start;
|
|
301
|
+
while (true) {
|
|
302
|
+
const packagePath = path4.join(current, "package.json");
|
|
303
|
+
if (existsSync2(packagePath)) return current;
|
|
304
|
+
const parent = path4.dirname(current);
|
|
305
|
+
if (parent === current) {
|
|
306
|
+
throw new Error(`Unable to locate package.json from ${start}`);
|
|
307
|
+
}
|
|
308
|
+
current = parent;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function resolvePackageRoot(metaUrl = import.meta.url) {
|
|
312
|
+
return findPackageRoot(path4.dirname(fileURLToPath(metaUrl)));
|
|
313
|
+
}
|
|
314
|
+
function createNodeContext(overrides = {}) {
|
|
315
|
+
return {
|
|
316
|
+
argv: process.argv.slice(2),
|
|
317
|
+
cwd: process.cwd(),
|
|
318
|
+
env: process.env,
|
|
319
|
+
homeDir: os.homedir(),
|
|
320
|
+
packageRoot: resolvePackageRoot(),
|
|
321
|
+
terminal: console,
|
|
322
|
+
prompt: prompts,
|
|
323
|
+
fs: {
|
|
324
|
+
exists: existsSync2,
|
|
325
|
+
readText: (target) => readFile2(target, "utf8"),
|
|
326
|
+
writeText: async (target, content) => {
|
|
327
|
+
await writeFile(target, content);
|
|
328
|
+
},
|
|
329
|
+
mkdir: async (target) => {
|
|
330
|
+
await mkdir(target, { recursive: true });
|
|
331
|
+
},
|
|
332
|
+
copyFile: async (source, target) => {
|
|
333
|
+
await copyFile(source, target);
|
|
334
|
+
},
|
|
335
|
+
appendText: async (target, content) => {
|
|
336
|
+
await appendFile(target, content);
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
...overrides
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/core/project-paths.ts
|
|
344
|
+
import path5 from "path";
|
|
345
|
+
import { lstat } from "fs/promises";
|
|
346
|
+
var SAFE_SEGMENT = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
347
|
+
function hasControlCharacters(value) {
|
|
348
|
+
return [...value].some((character) => {
|
|
349
|
+
const code = character.charCodeAt(0);
|
|
350
|
+
return code <= 31 || code === 127;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function parseLogicalName(value, label = "name") {
|
|
354
|
+
if (!value || hasControlCharacters(value)) {
|
|
355
|
+
throw new CliError(
|
|
356
|
+
`Invalid ${label}: a non-empty printable value is required.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
if (path5.isAbsolute(value) || value.includes("/") || value.includes("\\")) {
|
|
360
|
+
throw new CliError(
|
|
361
|
+
`Invalid ${label}: paths and separators are not allowed.`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const segments = value.split(".");
|
|
365
|
+
if (segments.some((segment) => !SAFE_SEGMENT.test(segment))) {
|
|
366
|
+
throw new CliError(
|
|
367
|
+
`Invalid ${label}: use dot-separated alphanumeric segments beginning with a letter.`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return segments;
|
|
371
|
+
}
|
|
372
|
+
function validateProjectName(value) {
|
|
373
|
+
if (!value || hasControlCharacters(value) || path5.isAbsolute(value) || value === "." || value === ".." || value.includes("/") || value.includes("\\") || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
|
|
374
|
+
throw new CliError(
|
|
375
|
+
"Invalid project name: use letters, numbers, dots, dashes or underscores without path separators."
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return value;
|
|
379
|
+
}
|
|
380
|
+
function resolveInside(root, ...segments) {
|
|
381
|
+
const absoluteRoot = path5.resolve(root);
|
|
382
|
+
const target = path5.resolve(absoluteRoot, ...segments);
|
|
383
|
+
if (target !== absoluteRoot && !target.startsWith(`${absoluteRoot}${path5.sep}`)) {
|
|
384
|
+
throw new CliError(
|
|
385
|
+
`Refusing to access a path outside the project: ${target}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return target;
|
|
389
|
+
}
|
|
390
|
+
async function assertSafeTarget(root, target) {
|
|
391
|
+
const safeTarget = resolveInside(root, path5.relative(root, target));
|
|
392
|
+
const relativeSegments = path5.relative(path5.resolve(root), safeTarget).split(path5.sep);
|
|
393
|
+
let current = path5.resolve(root);
|
|
394
|
+
for (const segment of relativeSegments) {
|
|
395
|
+
if (!segment) continue;
|
|
396
|
+
current = path5.join(current, segment);
|
|
397
|
+
try {
|
|
398
|
+
if ((await lstat(current)).isSymbolicLink()) {
|
|
399
|
+
throw new CliError(
|
|
400
|
+
`Symbolic links are forbidden in project paths: ${current}`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (error instanceof CliError) throw error;
|
|
405
|
+
if (error.code !== "ENOENT") throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return safeTarget;
|
|
409
|
+
}
|
|
410
|
+
function normalizeImportAlias(value) {
|
|
411
|
+
if (!/^[A-Za-z@~][A-Za-z0-9@~_-]*\/\*$/.test(value)) {
|
|
412
|
+
throw new CliError(
|
|
413
|
+
'Invalid import alias: expected a prefix followed by "/*" (for example "@/*" or "@core/*").'
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/lib/addApi.ts
|
|
420
|
+
async function addApi(args, cwd = process.cwd()) {
|
|
421
|
+
let apiName = args[1];
|
|
422
|
+
if (!apiName || apiName.startsWith("-")) {
|
|
423
|
+
const response = await prompts2.prompt({
|
|
424
|
+
type: "text",
|
|
425
|
+
name: "apiName",
|
|
426
|
+
message: "\u{1F50C} API route name to add:",
|
|
427
|
+
validate: (name) => name ? true : "API route name is required"
|
|
428
|
+
});
|
|
429
|
+
apiName = response.apiName;
|
|
430
|
+
}
|
|
431
|
+
const apiSegments = parseLogicalName(apiName, "API route name");
|
|
432
|
+
const config = await loadConfig(cwd);
|
|
433
|
+
if (!config) {
|
|
434
|
+
console.error(
|
|
435
|
+
"\u274C Configuration file cnp.config.json not found. Run this command from the project root."
|
|
436
|
+
);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const apiDir = join2(cwd, "src", "app", "api", ...apiSegments);
|
|
440
|
+
await assertSafeTarget(cwd, apiDir);
|
|
441
|
+
if (!existsSync3(apiDir)) {
|
|
442
|
+
await mkdir2(apiDir, { recursive: true });
|
|
443
|
+
}
|
|
444
|
+
const templateDir = join2(
|
|
445
|
+
resolvePackageRoot(import.meta.url),
|
|
446
|
+
"templates",
|
|
447
|
+
"Api"
|
|
448
|
+
);
|
|
449
|
+
const routeTemplate = join2(templateDir, "route.ts");
|
|
450
|
+
const routePath = join2(apiDir, "route.ts");
|
|
451
|
+
if (!existsSync3(routePath)) {
|
|
452
|
+
if (existsSync3(routeTemplate)) {
|
|
453
|
+
let content = await readFile3(routeTemplate, "utf-8");
|
|
454
|
+
content = content.replace(/template/g, apiName);
|
|
455
|
+
await writeFile2(routePath, content);
|
|
456
|
+
} else {
|
|
457
|
+
await writeFile2(
|
|
458
|
+
routePath,
|
|
459
|
+
`import { NextResponse } from "next/server";
|
|
460
|
+
|
|
461
|
+
export async function GET() {
|
|
462
|
+
return NextResponse.json({ message: "Hello from ${apiName}" });
|
|
463
|
+
}
|
|
464
|
+
`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
console.log(`\u{1F4C4} File created: ${routePath}`);
|
|
468
|
+
} else {
|
|
469
|
+
console.log(`\u2139\uFE0F File already exists: ${routePath}`);
|
|
470
|
+
}
|
|
471
|
+
console.log(`\u2705 API route "${apiName}" added.`);
|
|
472
|
+
}
|
|
473
|
+
|
|
58
474
|
// src/lib/addComponent.ts
|
|
59
|
-
import {
|
|
60
|
-
|
|
475
|
+
import { join as join3 } from "path";
|
|
476
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3, readdir as readdir3 } from "fs/promises";
|
|
477
|
+
import prompts3 from "prompts";
|
|
478
|
+
import { existsSync as existsSync4, statSync } from "fs";
|
|
479
|
+
async function addComponent(args, cwd = process.cwd()) {
|
|
61
480
|
let componentName = args[1];
|
|
62
481
|
let pageScope = null;
|
|
63
|
-
|
|
482
|
+
const pageIndex = args.findIndex((arg) => arg === "-P" || arg === "--page");
|
|
64
483
|
if (pageIndex !== -1 && args[pageIndex + 1]) {
|
|
65
484
|
pageScope = args[pageIndex + 1];
|
|
66
485
|
}
|
|
67
486
|
let nestedPath = null;
|
|
68
487
|
if (pageScope && pageScope.includes(".")) {
|
|
69
|
-
nestedPath =
|
|
488
|
+
nestedPath = join3(...pageScope.split("."));
|
|
70
489
|
}
|
|
71
490
|
if (!componentName || componentName.startsWith("-")) {
|
|
72
|
-
const response = await
|
|
491
|
+
const response = await prompts3.prompt({
|
|
73
492
|
type: "text",
|
|
74
493
|
name: "componentName",
|
|
75
494
|
message: "\u{1F9E9} Component name to add:",
|
|
@@ -77,7 +496,9 @@ async function addComponent(args) {
|
|
|
77
496
|
});
|
|
78
497
|
componentName = response.componentName;
|
|
79
498
|
}
|
|
80
|
-
|
|
499
|
+
parseLogicalName(componentName, "component name");
|
|
500
|
+
if (pageScope) parseLogicalName(pageScope, "page name");
|
|
501
|
+
const config = await loadConfig(cwd);
|
|
81
502
|
if (!config) {
|
|
82
503
|
console.error(
|
|
83
504
|
"\u274C Configuration file cnp.config.json not found. Run this command from the project root."
|
|
@@ -86,15 +507,15 @@ async function addComponent(args) {
|
|
|
86
507
|
}
|
|
87
508
|
const useI18n = !!config.useI18n;
|
|
88
509
|
const componentNameUpper = capitalize(componentName);
|
|
89
|
-
const templatePath =
|
|
90
|
-
|
|
510
|
+
const templatePath = join3(
|
|
511
|
+
resolvePackageRoot(import.meta.url),
|
|
91
512
|
"templates",
|
|
92
513
|
"Component"
|
|
93
514
|
);
|
|
94
515
|
let messagesPath = null;
|
|
95
516
|
if (useI18n) {
|
|
96
|
-
messagesPath =
|
|
97
|
-
if (!
|
|
517
|
+
messagesPath = join3(cwd, "messages");
|
|
518
|
+
if (!existsSync4(messagesPath)) {
|
|
98
519
|
console.error(
|
|
99
520
|
"\u274C Messages directory missing. Ensure i18n was configured."
|
|
100
521
|
);
|
|
@@ -105,25 +526,26 @@ async function addComponent(args) {
|
|
|
105
526
|
let translationKey;
|
|
106
527
|
if (pageScope) {
|
|
107
528
|
if (nestedPath) {
|
|
108
|
-
componentTargetPath =
|
|
529
|
+
componentTargetPath = join3(cwd, "src", "ui", nestedPath);
|
|
109
530
|
translationKey = pageScope;
|
|
110
531
|
} else {
|
|
111
|
-
componentTargetPath =
|
|
532
|
+
componentTargetPath = join3(cwd, "src", "ui", pageScope);
|
|
112
533
|
translationKey = pageScope;
|
|
113
534
|
}
|
|
114
535
|
} else {
|
|
115
|
-
componentTargetPath =
|
|
536
|
+
componentTargetPath = join3(cwd, "src", "ui", "_global");
|
|
116
537
|
translationKey = "_global_ui";
|
|
117
538
|
}
|
|
118
|
-
|
|
119
|
-
|
|
539
|
+
await assertSafeTarget(cwd, componentTargetPath);
|
|
540
|
+
if (!existsSync4(componentTargetPath)) {
|
|
541
|
+
await mkdir3(componentTargetPath, { recursive: true });
|
|
120
542
|
}
|
|
121
|
-
const componentFile =
|
|
122
|
-
const templateComponentPath =
|
|
123
|
-
if (
|
|
124
|
-
let content = await
|
|
543
|
+
const componentFile = join3(componentTargetPath, `${componentNameUpper}.tsx`);
|
|
544
|
+
const templateComponentPath = join3(templatePath, "Component.tsx");
|
|
545
|
+
if (existsSync4(templateComponentPath)) {
|
|
546
|
+
let content = await readFile4(templateComponentPath, "utf-8");
|
|
125
547
|
content = content.replace(/Component/g, componentNameUpper).replace(/componentPage/g, translationKey);
|
|
126
|
-
await
|
|
548
|
+
await writeFile3(componentFile, content);
|
|
127
549
|
console.log(`\u{1F4C4} File created: ${componentFile}`);
|
|
128
550
|
} else {
|
|
129
551
|
console.error(
|
|
@@ -132,32 +554,40 @@ async function addComponent(args) {
|
|
|
132
554
|
);
|
|
133
555
|
}
|
|
134
556
|
if (useI18n && messagesPath) {
|
|
135
|
-
const entries = await
|
|
557
|
+
const entries = await readdir3(messagesPath, { withFileTypes: true });
|
|
136
558
|
const langDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
137
|
-
const jsonTemplate =
|
|
138
|
-
if (!
|
|
559
|
+
const jsonTemplate = join3(templatePath, "component.json");
|
|
560
|
+
if (!existsSync4(jsonTemplate)) {
|
|
139
561
|
console.error("\u274C Template component.json not found:", jsonTemplate);
|
|
140
562
|
return;
|
|
141
563
|
}
|
|
142
|
-
const jsonContent = await
|
|
564
|
+
const jsonContent = await readFile4(jsonTemplate, "utf-8");
|
|
143
565
|
const parsed = JSON.parse(jsonContent);
|
|
144
566
|
for (const locale of langDirs) {
|
|
145
|
-
const localeDir =
|
|
146
|
-
if (!
|
|
567
|
+
const localeDir = join3(messagesPath, locale);
|
|
568
|
+
if (!existsSync4(localeDir) || !statSync(localeDir).isDirectory())
|
|
147
569
|
continue;
|
|
148
570
|
let jsonTarget;
|
|
149
571
|
if (pageScope) {
|
|
150
|
-
|
|
572
|
+
const [messageFile] = pageScope.split(".");
|
|
573
|
+
jsonTarget = join3(messagesPath, locale, `${messageFile}.json`);
|
|
151
574
|
} else {
|
|
152
|
-
jsonTarget =
|
|
575
|
+
jsonTarget = join3(messagesPath, locale, `_global_ui.json`);
|
|
153
576
|
}
|
|
154
577
|
let current = {};
|
|
155
|
-
if (
|
|
156
|
-
const jsonFile = await
|
|
578
|
+
if (existsSync4(jsonTarget)) {
|
|
579
|
+
const jsonFile = await readFile4(jsonTarget, "utf-8");
|
|
157
580
|
current = JSON.parse(jsonFile);
|
|
158
581
|
}
|
|
159
|
-
|
|
160
|
-
|
|
582
|
+
if (pageScope?.includes(".")) {
|
|
583
|
+
const child = pageScope.split(".")[1];
|
|
584
|
+
const childMessages = current[child] && typeof current[child] === "object" ? current[child] : {};
|
|
585
|
+
childMessages[componentNameUpper] = parsed;
|
|
586
|
+
current[child] = childMessages;
|
|
587
|
+
} else {
|
|
588
|
+
current[componentNameUpper] = parsed;
|
|
589
|
+
}
|
|
590
|
+
await writeFile3(jsonTarget, JSON.stringify(current, null, 2));
|
|
161
591
|
console.log(`\u{1F4C4} File updated: ${jsonTarget}`);
|
|
162
592
|
}
|
|
163
593
|
} else {
|
|
@@ -168,15 +598,270 @@ async function addComponent(args) {
|
|
|
168
598
|
);
|
|
169
599
|
}
|
|
170
600
|
|
|
601
|
+
// src/lib/addLanguage.ts
|
|
602
|
+
import { join as join4 } from "path";
|
|
603
|
+
import { existsSync as existsSync5 } from "fs";
|
|
604
|
+
import { cp, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
605
|
+
import prompts4 from "prompts";
|
|
606
|
+
function generateLocales() {
|
|
607
|
+
const dn = new Intl.DisplayNames(["en"], { type: "language" });
|
|
608
|
+
const locales = [];
|
|
609
|
+
for (let i = 0; i < 26; i++) {
|
|
610
|
+
for (let j = 0; j < 26; j++) {
|
|
611
|
+
const code = String.fromCharCode(97 + i) + String.fromCharCode(97 + j);
|
|
612
|
+
try {
|
|
613
|
+
const name = dn.of(code);
|
|
614
|
+
if (name && name.toLowerCase() !== code) {
|
|
615
|
+
locales.push(code);
|
|
616
|
+
}
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return locales.sort();
|
|
622
|
+
}
|
|
623
|
+
async function addLanguage(args, cwd = process.cwd()) {
|
|
624
|
+
const config = await loadConfig(cwd);
|
|
625
|
+
if (!config?.useI18n) {
|
|
626
|
+
console.error("\u274C i18n is not enabled in this project.");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const messagesPath = join4(cwd, "messages");
|
|
630
|
+
if (!existsSync5(messagesPath)) {
|
|
631
|
+
console.error("\u274C Messages directory missing. Ensure i18n was configured.");
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const available = generateLocales();
|
|
635
|
+
let locale = args[1];
|
|
636
|
+
if (!locale || !available.includes(locale)) {
|
|
637
|
+
const response = await prompts4({
|
|
638
|
+
type: "autocomplete",
|
|
639
|
+
name: "locale",
|
|
640
|
+
message: "\u{1F310} Locale to add:",
|
|
641
|
+
choices: available.map((l) => ({ title: l, value: l }))
|
|
642
|
+
});
|
|
643
|
+
locale = response.locale;
|
|
644
|
+
}
|
|
645
|
+
if (!locale) return;
|
|
646
|
+
parseLogicalName(locale, "locale");
|
|
647
|
+
await assertSafeTarget(cwd, messagesPath);
|
|
648
|
+
if (existsSync5(join4(messagesPath, locale))) {
|
|
649
|
+
throw new Error(`Locale ${locale} already exists.`);
|
|
650
|
+
}
|
|
651
|
+
const routingFile = join4(cwd, "src", "lib", "i18n", "routing.ts");
|
|
652
|
+
const messagesRegistryFile = join4(cwd, "src", "lib", "i18n", "messages.ts");
|
|
653
|
+
if (!existsSync5(routingFile)) {
|
|
654
|
+
throw new Error("routing.ts not found. Are you in project root?");
|
|
655
|
+
}
|
|
656
|
+
if (!existsSync5(messagesRegistryFile)) {
|
|
657
|
+
throw new Error("messages.ts not found. Are you in project root?");
|
|
658
|
+
}
|
|
659
|
+
const routingContent = await readFile5(routingFile, "utf-8");
|
|
660
|
+
const defaultMatch = routingContent.match(/defaultLocale:\s*"([^"]+)"/);
|
|
661
|
+
const defaultLocale = defaultMatch ? defaultMatch[1] : null;
|
|
662
|
+
if (!defaultLocale || !existsSync5(join4(messagesPath, defaultLocale))) {
|
|
663
|
+
throw new Error("Default locale not found.");
|
|
664
|
+
}
|
|
665
|
+
const defaultAggregatorFile = join4(messagesPath, `${defaultLocale}.ts`);
|
|
666
|
+
if (!existsSync5(defaultAggregatorFile)) {
|
|
667
|
+
throw new Error(`Default locale aggregator not found: ${defaultLocale}.ts`);
|
|
668
|
+
}
|
|
669
|
+
const localesMatch = routingContent.match(/locales:\s*\[([^\]]*)\]/);
|
|
670
|
+
if (!localesMatch) {
|
|
671
|
+
throw new Error("Unable to locate routing locales.");
|
|
672
|
+
}
|
|
673
|
+
const localesArr = localesMatch[1].split(",").map((s) => s.trim().replace(/["']/g, "")).filter(Boolean);
|
|
674
|
+
if (localesArr.includes(locale)) {
|
|
675
|
+
throw new Error(`Locale ${locale} already exists in routing.`);
|
|
676
|
+
}
|
|
677
|
+
const newLocales = `locales: [${[...localesArr, locale].map((item) => `"${item}"`).join(", ")}]`;
|
|
678
|
+
const nextRoutingContent = routingContent.replace(
|
|
679
|
+
/locales:\s*\[[^\]]*\]/,
|
|
680
|
+
newLocales
|
|
681
|
+
);
|
|
682
|
+
const defaultAggregatorContent = await readFile5(
|
|
683
|
+
defaultAggregatorFile,
|
|
684
|
+
"utf-8"
|
|
685
|
+
);
|
|
686
|
+
const defaultImportPrefix = `./${defaultLocale}/`;
|
|
687
|
+
if (!defaultAggregatorContent.includes(defaultImportPrefix)) {
|
|
688
|
+
throw new Error(
|
|
689
|
+
`Default locale aggregator does not import from ${defaultImportPrefix}`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
const nextAggregatorContent = defaultAggregatorContent.replaceAll(
|
|
693
|
+
defaultImportPrefix,
|
|
694
|
+
`./${locale}/`
|
|
695
|
+
);
|
|
696
|
+
const messagesRegistryContent = await readFile5(messagesRegistryFile, "utf-8");
|
|
697
|
+
const registryMatch = messagesRegistryContent.match(
|
|
698
|
+
/const messages = \{([^}]*)\} as const;/
|
|
699
|
+
);
|
|
700
|
+
if (!registryMatch) {
|
|
701
|
+
throw new Error("Unable to locate the typed messages registry.");
|
|
702
|
+
}
|
|
703
|
+
const registeredLocales = registryMatch[1].split(",").map((item) => item.trim()).filter(Boolean);
|
|
704
|
+
if (registeredLocales.includes(locale)) {
|
|
705
|
+
throw new Error(`Locale ${locale} already exists in messages registry.`);
|
|
706
|
+
}
|
|
707
|
+
const registryDeclaration = `const messages = { ${[
|
|
708
|
+
...registeredLocales,
|
|
709
|
+
locale
|
|
710
|
+
].join(", ")} } as const;`;
|
|
711
|
+
const declarationIndex = messagesRegistryContent.indexOf("const messages =");
|
|
712
|
+
const nextMessagesRegistryContent = messagesRegistryContent.slice(0, declarationIndex) + `import ${locale} from "../../../messages/${locale}";
|
|
713
|
+
|
|
714
|
+
` + messagesRegistryContent.slice(declarationIndex).replace(/const messages = \{[^}]*\} as const;/, registryDeclaration);
|
|
715
|
+
await cp(join4(messagesPath, defaultLocale), join4(messagesPath, locale), {
|
|
716
|
+
recursive: true
|
|
717
|
+
});
|
|
718
|
+
console.log(`\u{1F4C4} Directory created: ${join4(messagesPath, locale)}`);
|
|
719
|
+
await writeFile4(join4(messagesPath, `${locale}.ts`), nextAggregatorContent);
|
|
720
|
+
console.log(`\u{1F4C4} File created: ${join4(messagesPath, `${locale}.ts`)}`);
|
|
721
|
+
await writeFile4(messagesRegistryFile, nextMessagesRegistryContent);
|
|
722
|
+
console.log(`\u{1F4C4} File updated: ${messagesRegistryFile}`);
|
|
723
|
+
await writeFile4(routingFile, nextRoutingContent);
|
|
724
|
+
console.log(`\u{1F4C4} File updated: ${routingFile}`);
|
|
725
|
+
console.log(
|
|
726
|
+
`\u2705 Locale "${locale}" added and copied from default locale "${defaultLocale}".`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/lib/addLib.ts
|
|
731
|
+
import { join as join5 } from "path";
|
|
732
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
733
|
+
import prompts5 from "prompts";
|
|
734
|
+
import { existsSync as existsSync6 } from "fs";
|
|
735
|
+
async function addLib(args, cwd = process.cwd()) {
|
|
736
|
+
let libArg = args[1];
|
|
737
|
+
if (!libArg || libArg.startsWith("-")) {
|
|
738
|
+
const response = await prompts5.prompt({
|
|
739
|
+
type: "text",
|
|
740
|
+
name: "libArg",
|
|
741
|
+
message: "\u{1F4E6} Lib name to add:",
|
|
742
|
+
validate: (name) => name ? true : "Lib name is required"
|
|
743
|
+
});
|
|
744
|
+
libArg = response.libArg;
|
|
745
|
+
}
|
|
746
|
+
const libSegments = parseLogicalName(libArg, "library name");
|
|
747
|
+
let libName = libArg;
|
|
748
|
+
let fileName = null;
|
|
749
|
+
if (libSegments.length === 2) {
|
|
750
|
+
[libName, fileName] = libSegments;
|
|
751
|
+
} else if (libSegments.length > 2) {
|
|
752
|
+
throw new Error("Libraries currently support exactly library.module.");
|
|
753
|
+
}
|
|
754
|
+
const config = await loadConfig(cwd);
|
|
755
|
+
if (!config) {
|
|
756
|
+
console.error(
|
|
757
|
+
"\u274C Configuration file cnp.config.json not found. Run this command from the project root."
|
|
758
|
+
);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const libDir = join5(cwd, "src", "lib", libName);
|
|
762
|
+
await assertSafeTarget(cwd, libDir);
|
|
763
|
+
if (!existsSync6(libDir)) {
|
|
764
|
+
await mkdir4(libDir, { recursive: true });
|
|
765
|
+
}
|
|
766
|
+
const templateDir = join5(
|
|
767
|
+
resolvePackageRoot(import.meta.url),
|
|
768
|
+
"templates",
|
|
769
|
+
"Lib"
|
|
770
|
+
);
|
|
771
|
+
const indexTemplate = join5(templateDir, "index.ts");
|
|
772
|
+
const fileTemplate = join5(templateDir, "item.ts");
|
|
773
|
+
const indexPath = join5(libDir, "index.ts");
|
|
774
|
+
if (!existsSync6(indexPath)) {
|
|
775
|
+
if (existsSync6(indexTemplate)) {
|
|
776
|
+
const content = await readFile6(indexTemplate, "utf-8");
|
|
777
|
+
await writeFile5(indexPath, content);
|
|
778
|
+
} else {
|
|
779
|
+
await writeFile5(indexPath, "export {}\n");
|
|
780
|
+
}
|
|
781
|
+
console.log(`\u{1F4C4} File created: ${indexPath}`);
|
|
782
|
+
}
|
|
783
|
+
if (fileName) {
|
|
784
|
+
const filePath = join5(libDir, `${fileName}.ts`);
|
|
785
|
+
if (!existsSync6(filePath)) {
|
|
786
|
+
if (existsSync6(fileTemplate)) {
|
|
787
|
+
let content = await readFile6(fileTemplate, "utf-8");
|
|
788
|
+
content = content.replace(/template/g, fileName).replace(/Template/g, capitalize(fileName));
|
|
789
|
+
await writeFile5(filePath, content);
|
|
790
|
+
} else {
|
|
791
|
+
await writeFile5(
|
|
792
|
+
filePath,
|
|
793
|
+
`export function ${fileName}() {
|
|
794
|
+
// TODO: implement
|
|
795
|
+
}
|
|
796
|
+
`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
console.log(`\u{1F4C4} File created: ${filePath}`);
|
|
800
|
+
}
|
|
801
|
+
let indexContent = await readFile6(indexPath, "utf-8");
|
|
802
|
+
const importLine = `import { ${fileName} } from "./${fileName}";`;
|
|
803
|
+
const importRegex = new RegExp(
|
|
804
|
+
`import\\s*{\\s*${fileName}\\s*}\\s*from\\s*"\\./${fileName}";`
|
|
805
|
+
);
|
|
806
|
+
const exportRegex = /export\s*{([^}]*)}/m;
|
|
807
|
+
const imports = [];
|
|
808
|
+
const exportsSet = [];
|
|
809
|
+
for (const line of indexContent.split("\n")) {
|
|
810
|
+
if (line.startsWith("import")) {
|
|
811
|
+
imports.push(line);
|
|
812
|
+
} else if (line.startsWith("export")) {
|
|
813
|
+
const match = line.match(exportRegex);
|
|
814
|
+
if (match && match[1]) {
|
|
815
|
+
exportsSet.push(
|
|
816
|
+
...match[1].split(",").map((s) => s.trim()).filter(Boolean)
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (!imports.some((l) => importRegex.test(l))) {
|
|
822
|
+
imports.push(importLine);
|
|
823
|
+
}
|
|
824
|
+
if (!exportsSet.includes(fileName)) {
|
|
825
|
+
exportsSet.push(fileName);
|
|
826
|
+
}
|
|
827
|
+
indexContent = imports.join("\n") + "\n\nexport { " + exportsSet.join(", ") + " };\n";
|
|
828
|
+
await writeFile5(indexPath, indexContent);
|
|
829
|
+
console.log(`\u270F\uFE0F Updated index: ${indexPath}`);
|
|
830
|
+
}
|
|
831
|
+
console.log(
|
|
832
|
+
`\u2705 Lib "${libName}"${fileName ? ` with module ${fileName}` : ""} added.`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
171
836
|
// src/lib/addPage.ts
|
|
172
|
-
import { join as
|
|
173
|
-
import { mkdir as
|
|
174
|
-
import
|
|
175
|
-
import { existsSync as
|
|
176
|
-
|
|
837
|
+
import { join as join6 } from "path";
|
|
838
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile6, readdir as readdir4 } from "fs/promises";
|
|
839
|
+
import prompts6 from "prompts";
|
|
840
|
+
import { existsSync as existsSync7, statSync as statSync2 } from "fs";
|
|
841
|
+
function registerMessagesFile(content, locale, fileName) {
|
|
842
|
+
const importPath = `./${locale}/${fileName}.json`;
|
|
843
|
+
if (content.includes(importPath)) return content;
|
|
844
|
+
const declarationIndex = content.indexOf("const messages =");
|
|
845
|
+
const registryMatch = content.match(/const messages = \{([\s\S]*?)\n\};/);
|
|
846
|
+
if (declarationIndex === -1 || !registryMatch) {
|
|
847
|
+
throw new Error(
|
|
848
|
+
`Unable to register ${fileName}.json in messages/${locale}.ts`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const importStatement = `import ${fileName} from "${importPath}";
|
|
852
|
+
`;
|
|
853
|
+
const nextRegistry = registryMatch[0].replace(
|
|
854
|
+
/\n\};$/,
|
|
855
|
+
`
|
|
856
|
+
${fileName},
|
|
857
|
+
};`
|
|
858
|
+
);
|
|
859
|
+
return content.slice(0, declarationIndex) + importStatement + content.slice(declarationIndex).replace(registryMatch[0], nextRegistry);
|
|
860
|
+
}
|
|
861
|
+
async function addPage(args, cwd = process.cwd()) {
|
|
177
862
|
let pageName = args[1];
|
|
178
863
|
if (!pageName || pageName.startsWith("-")) {
|
|
179
|
-
const response = await
|
|
864
|
+
const response = await prompts6.prompt({
|
|
180
865
|
type: "text",
|
|
181
866
|
name: "pageName",
|
|
182
867
|
message: "\u{1F4DD} Page name to add:",
|
|
@@ -184,13 +869,16 @@ async function addPage(args) {
|
|
|
184
869
|
});
|
|
185
870
|
pageName = response.pageName;
|
|
186
871
|
}
|
|
872
|
+
const pageSegments = parseLogicalName(pageName, "page name");
|
|
187
873
|
let parentName = null;
|
|
188
874
|
let childName = null;
|
|
189
|
-
if (
|
|
190
|
-
[parentName, childName] =
|
|
875
|
+
if (pageSegments.length === 2) {
|
|
876
|
+
[parentName, childName] = pageSegments;
|
|
877
|
+
} else if (pageSegments.length > 2) {
|
|
878
|
+
throw new Error("Nested pages currently support exactly Parent.Child.");
|
|
191
879
|
}
|
|
192
880
|
let shortFlags = args.find((arg) => /^-[A-Za-z]+$/.test(arg));
|
|
193
|
-
|
|
881
|
+
const longFlags = new Set(args.filter((a) => a.startsWith("--")));
|
|
194
882
|
const flags = /* @__PURE__ */ new Set();
|
|
195
883
|
if (!shortFlags && Array.from(longFlags).length === 0) {
|
|
196
884
|
shortFlags = "-LPl";
|
|
@@ -241,7 +929,7 @@ async function addPage(args) {
|
|
|
241
929
|
]) {
|
|
242
930
|
if (longFlags.has("--" + flag)) flags.add(flag);
|
|
243
931
|
}
|
|
244
|
-
const config = await loadConfig();
|
|
932
|
+
const config = await loadConfig(cwd);
|
|
245
933
|
if (!config) {
|
|
246
934
|
console.error(
|
|
247
935
|
"\u274C Configuration file cnp.config.json not found. Run this command from the project root."
|
|
@@ -249,50 +937,58 @@ async function addPage(args) {
|
|
|
249
937
|
return;
|
|
250
938
|
}
|
|
251
939
|
const useI18n = !!config.useI18n;
|
|
940
|
+
const aliasPrefix = configuredAliasPrefix(config);
|
|
252
941
|
const srcSegments = ["src", "app"];
|
|
253
942
|
if (useI18n) srcSegments.push("[locale]");
|
|
254
|
-
const srcPath =
|
|
255
|
-
if (!
|
|
943
|
+
const srcPath = join6(cwd, ...srcSegments);
|
|
944
|
+
if (!existsSync7(srcPath)) {
|
|
256
945
|
console.error(`\u274C Expected directory not found: ${srcPath}`);
|
|
257
946
|
return;
|
|
258
947
|
}
|
|
259
948
|
let messagesPath = null;
|
|
260
949
|
let locales = [];
|
|
261
950
|
if (useI18n) {
|
|
262
|
-
messagesPath =
|
|
263
|
-
if (!
|
|
951
|
+
messagesPath = join6(cwd, "messages");
|
|
952
|
+
if (!existsSync7(messagesPath)) {
|
|
264
953
|
console.error(
|
|
265
954
|
"\u274C Messages directory missing. Ensure i18n was configured."
|
|
266
955
|
);
|
|
267
956
|
return;
|
|
268
957
|
}
|
|
269
|
-
const entries = await
|
|
958
|
+
const entries = await readdir4(messagesPath, { withFileTypes: true });
|
|
270
959
|
locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
271
960
|
}
|
|
272
|
-
const templatePath =
|
|
273
|
-
|
|
961
|
+
const templatePath = join6(
|
|
962
|
+
resolvePackageRoot(import.meta.url),
|
|
274
963
|
"templates",
|
|
275
964
|
"Page"
|
|
276
965
|
);
|
|
277
966
|
let uiPageDir, localePagePath, jsonFileName;
|
|
278
967
|
if (parentName && childName) {
|
|
279
|
-
uiPageDir =
|
|
280
|
-
localePagePath =
|
|
968
|
+
uiPageDir = join6(cwd, "src", "ui", parentName, childName);
|
|
969
|
+
localePagePath = join6(srcPath, parentName, childName);
|
|
281
970
|
jsonFileName = parentName;
|
|
282
971
|
} else {
|
|
283
|
-
uiPageDir =
|
|
284
|
-
localePagePath =
|
|
972
|
+
uiPageDir = join6(cwd, "src", "ui", pageName);
|
|
973
|
+
localePagePath = join6(srcPath, pageName);
|
|
285
974
|
jsonFileName = pageName;
|
|
286
975
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
await
|
|
976
|
+
await assertSafeTarget(cwd, uiPageDir);
|
|
977
|
+
await assertSafeTarget(cwd, localePagePath);
|
|
978
|
+
if (!existsSync7(uiPageDir)) {
|
|
979
|
+
await mkdir5(uiPageDir, { recursive: true });
|
|
980
|
+
}
|
|
981
|
+
const uiPageFile = join6(uiPageDir, "page-ui.tsx");
|
|
982
|
+
const uiPageTemplate = join6(templatePath, "page-ui.tsx");
|
|
983
|
+
if (existsSync7(uiPageTemplate)) {
|
|
984
|
+
let uiContent = await readFile7(uiPageTemplate, "utf-8");
|
|
985
|
+
const translationNamespace = parentName && childName ? `${parentName}.${childName}` : pageName;
|
|
986
|
+
uiContent = uiContent.replace(
|
|
987
|
+
'useTranslations("template")',
|
|
988
|
+
`useTranslations("${translationNamespace}")`
|
|
989
|
+
);
|
|
990
|
+
uiContent = uiContent.replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
|
|
991
|
+
await writeFile6(uiPageFile, uiContent);
|
|
296
992
|
console.log(`\u{1F4C4} File created: ${uiPageFile}`);
|
|
297
993
|
} else {
|
|
298
994
|
console.warn(
|
|
@@ -300,37 +996,38 @@ async function addPage(args) {
|
|
|
300
996
|
uiPageTemplate
|
|
301
997
|
);
|
|
302
998
|
}
|
|
303
|
-
if (!
|
|
304
|
-
await
|
|
999
|
+
if (!existsSync7(localePagePath)) {
|
|
1000
|
+
await mkdir5(localePagePath, { recursive: true });
|
|
305
1001
|
}
|
|
306
1002
|
for (const flag of flags) {
|
|
307
1003
|
const filename = toFileName(flag);
|
|
308
|
-
const src =
|
|
309
|
-
const dst =
|
|
310
|
-
if (!
|
|
1004
|
+
const src = join6(templatePath, filename);
|
|
1005
|
+
const dst = join6(localePagePath, filename);
|
|
1006
|
+
if (!existsSync7(src)) {
|
|
311
1007
|
console.warn(`\u26A0\uFE0F Missing template file: ${filename} at path: ${src}`);
|
|
312
1008
|
continue;
|
|
313
1009
|
}
|
|
314
|
-
const content = await
|
|
315
|
-
const
|
|
316
|
-
|
|
1010
|
+
const content = await readFile7(src, "utf-8");
|
|
1011
|
+
const uiImportPath = parentName && childName ? `${parentName}/${childName}` : pageName;
|
|
1012
|
+
const replaced = content.replace("@/ui/template/", `@/ui/${uiImportPath}/`).replaceAll('from "@/', `from "${aliasPrefix}/`).replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
|
|
1013
|
+
await writeFile6(dst, replaced);
|
|
317
1014
|
console.log(`\u{1F4C4} File created: ${dst}`);
|
|
318
1015
|
}
|
|
319
1016
|
if (useI18n && messagesPath) {
|
|
320
|
-
const jsonTemplate =
|
|
321
|
-
if (!
|
|
1017
|
+
const jsonTemplate = join6(templatePath, "page.json");
|
|
1018
|
+
if (!existsSync7(jsonTemplate)) {
|
|
322
1019
|
console.warn("\u26A0\uFE0F Missing template: page.json at path:", jsonTemplate);
|
|
323
1020
|
} else {
|
|
324
|
-
const content = await
|
|
1021
|
+
const content = await readFile7(jsonTemplate, "utf-8");
|
|
325
1022
|
const replaced = content.replace(/template/g, childName || pageName).replace(/Template/g, capitalize(childName || pageName));
|
|
326
1023
|
for (const locale of locales) {
|
|
327
|
-
const localeDir =
|
|
328
|
-
if (!
|
|
1024
|
+
const localeDir = join6(messagesPath, locale);
|
|
1025
|
+
if (!existsSync7(localeDir) || !statSync2(localeDir).isDirectory())
|
|
329
1026
|
continue;
|
|
330
|
-
const jsonTarget =
|
|
1027
|
+
const jsonTarget = join6(messagesPath, locale, `${jsonFileName}.json`);
|
|
331
1028
|
let current = {};
|
|
332
|
-
if (
|
|
333
|
-
const jsonFile = await
|
|
1029
|
+
if (existsSync7(jsonTarget)) {
|
|
1030
|
+
const jsonFile = await readFile7(jsonTarget, "utf-8");
|
|
334
1031
|
try {
|
|
335
1032
|
current = JSON.parse(jsonFile);
|
|
336
1033
|
} catch {
|
|
@@ -342,8 +1039,23 @@ async function addPage(args) {
|
|
|
342
1039
|
} else {
|
|
343
1040
|
current = JSON.parse(replaced);
|
|
344
1041
|
}
|
|
345
|
-
await
|
|
1042
|
+
await writeFile6(jsonTarget, `${JSON.stringify(current, null, 2)}
|
|
1043
|
+
`);
|
|
346
1044
|
console.log(`\u{1F4C4} File created: ${jsonTarget}`);
|
|
1045
|
+
const localeAggregator = join6(messagesPath, `${locale}.ts`);
|
|
1046
|
+
if (!existsSync7(localeAggregator)) {
|
|
1047
|
+
throw new Error(`Locale aggregator not found: messages/${locale}.ts`);
|
|
1048
|
+
}
|
|
1049
|
+
const aggregatorContent = await readFile7(localeAggregator, "utf-8");
|
|
1050
|
+
const nextAggregatorContent = registerMessagesFile(
|
|
1051
|
+
aggregatorContent,
|
|
1052
|
+
locale,
|
|
1053
|
+
jsonFileName
|
|
1054
|
+
);
|
|
1055
|
+
if (nextAggregatorContent !== aggregatorContent) {
|
|
1056
|
+
await writeFile6(localeAggregator, nextAggregatorContent);
|
|
1057
|
+
console.log(`\u{1F4C4} File updated: ${localeAggregator}`);
|
|
1058
|
+
}
|
|
347
1059
|
}
|
|
348
1060
|
}
|
|
349
1061
|
} else {
|
|
@@ -354,216 +1066,170 @@ async function addPage(args) {
|
|
|
354
1066
|
);
|
|
355
1067
|
}
|
|
356
1068
|
|
|
357
|
-
// src/lib/
|
|
358
|
-
import { join as
|
|
359
|
-
import {
|
|
360
|
-
import
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
let pageName = args[1];
|
|
364
|
-
if (!pageName || pageName.startsWith("-")) {
|
|
365
|
-
const response = await prompts3.prompt({
|
|
366
|
-
type: "text",
|
|
367
|
-
name: "pageName",
|
|
368
|
-
message: "\u{1F5D1}\uFE0F Page name to remove:",
|
|
369
|
-
validate: (name) => name ? true : "Page name is required"
|
|
370
|
-
});
|
|
371
|
-
pageName = response.pageName;
|
|
372
|
-
}
|
|
373
|
-
const messagesPath = join4(process.cwd(), "messages");
|
|
374
|
-
const entries = await readdir3(messagesPath, { withFileTypes: true });
|
|
375
|
-
const langDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
376
|
-
for (const locale of langDirs) {
|
|
377
|
-
const jsonTarget = join4(messagesPath, locale, `${pageName}.json`);
|
|
378
|
-
if (existsSync4(jsonTarget)) {
|
|
379
|
-
await writeFile3(jsonTarget, "");
|
|
380
|
-
await import("child_process").then(
|
|
381
|
-
(cp3) => cp3.execSync(`rm -f '${jsonTarget}'`)
|
|
382
|
-
);
|
|
383
|
-
console.log(`\u{1F5D1}\uFE0F Deleted: ${jsonTarget}`);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
const uiPageDir = join4(process.cwd(), "src", "ui", pageName);
|
|
387
|
-
if (existsSync4(uiPageDir)) {
|
|
388
|
-
await import("child_process").then(
|
|
389
|
-
(cp3) => cp3.execSync(`rm -rf '${uiPageDir}'`)
|
|
390
|
-
);
|
|
391
|
-
console.log(`\u{1F5D1}\uFE0F Deleted: ${uiPageDir}`);
|
|
392
|
-
}
|
|
393
|
-
const appLocaleDir = join4(process.cwd(), "src", "app", "[locale]", pageName);
|
|
394
|
-
if (existsSync4(appLocaleDir)) {
|
|
395
|
-
await import("child_process").then(
|
|
396
|
-
(cp3) => cp3.execSync(`rm -rf '${appLocaleDir}'`)
|
|
397
|
-
);
|
|
398
|
-
console.log(`\u{1F5D1}\uFE0F Deleted: ${appLocaleDir}`);
|
|
399
|
-
}
|
|
400
|
-
console.log(`\u2705 Page "${pageName}" deleted.`);
|
|
1069
|
+
// src/lib/addText.ts
|
|
1070
|
+
import { join as join7 } from "path";
|
|
1071
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1072
|
+
import { readFile as readFile8, writeFile as writeFile7, readdir as readdir5 } from "fs/promises";
|
|
1073
|
+
function defaultText(key) {
|
|
1074
|
+
return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
401
1075
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
import { existsSync as existsSync5 } from "fs";
|
|
408
|
-
async function addLib(args) {
|
|
409
|
-
let libArg = args[1];
|
|
410
|
-
if (!libArg || libArg.startsWith("-")) {
|
|
411
|
-
const response = await prompts4.prompt({
|
|
412
|
-
type: "text",
|
|
413
|
-
name: "libArg",
|
|
414
|
-
message: "\u{1F4E6} Lib name to add:",
|
|
415
|
-
validate: (name) => name ? true : "Lib name is required"
|
|
416
|
-
});
|
|
417
|
-
libArg = response.libArg;
|
|
418
|
-
}
|
|
419
|
-
let libName = libArg;
|
|
420
|
-
let fileName = null;
|
|
421
|
-
if (libArg.includes(".")) {
|
|
422
|
-
[libName, fileName] = libArg.split(".");
|
|
1076
|
+
async function addText(args, cwd = process.cwd()) {
|
|
1077
|
+
const pathArg = args[1];
|
|
1078
|
+
if (!pathArg) {
|
|
1079
|
+
console.error("\u274C Dot path parameter is required.");
|
|
1080
|
+
return;
|
|
423
1081
|
}
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
);
|
|
1082
|
+
const providedText = args.slice(2).join(" ");
|
|
1083
|
+
parseLogicalName(pathArg, "translation path");
|
|
1084
|
+
const config = await loadConfig(cwd);
|
|
1085
|
+
if (!config?.useI18n) {
|
|
1086
|
+
console.error("\u274C i18n is not enabled in this project.");
|
|
429
1087
|
return;
|
|
430
1088
|
}
|
|
431
|
-
const
|
|
432
|
-
if (!
|
|
433
|
-
|
|
1089
|
+
const messagesPath = join7(cwd, "messages");
|
|
1090
|
+
if (!existsSync8(messagesPath)) {
|
|
1091
|
+
console.error("\u274C Messages directory missing. Ensure i18n was configured.");
|
|
1092
|
+
return;
|
|
434
1093
|
}
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const fileTemplate = join5(templateDir, "item.ts");
|
|
442
|
-
const indexPath = join5(libDir, "index.ts");
|
|
443
|
-
if (!existsSync5(indexPath)) {
|
|
444
|
-
if (existsSync5(indexTemplate)) {
|
|
445
|
-
const content = await readFile4(indexTemplate, "utf-8");
|
|
446
|
-
await writeFile4(indexPath, content);
|
|
447
|
-
} else {
|
|
448
|
-
await writeFile4(indexPath, "export {}\n");
|
|
449
|
-
}
|
|
450
|
-
console.log(`\u{1F4C4} File created: ${indexPath}`);
|
|
1094
|
+
const entries = await readdir5(messagesPath, { withFileTypes: true });
|
|
1095
|
+
const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1096
|
+
const [fileName, ...segments] = pathArg.split(".");
|
|
1097
|
+
if (!fileName || segments.length === 0) {
|
|
1098
|
+
console.error("\u274C Invalid dot path provided.");
|
|
1099
|
+
return;
|
|
451
1100
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
// TODO: implement
|
|
464
|
-
}
|
|
465
|
-
`
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
console.log(`\u{1F4C4} File created: ${filePath}`);
|
|
469
|
-
}
|
|
470
|
-
let indexContent = await readFile4(indexPath, "utf-8");
|
|
471
|
-
const importLine = `import { ${fileName} } from "./${fileName}";`;
|
|
472
|
-
const importRegex = new RegExp(
|
|
473
|
-
`import\\s*{\\s*${fileName}\\s*}\\s*from\\s*"\\./${fileName}";`
|
|
474
|
-
);
|
|
475
|
-
const exportRegex = /export\s*{([^}]*)}/m;
|
|
476
|
-
const imports = [];
|
|
477
|
-
const exportsSet = [];
|
|
478
|
-
for (const line of indexContent.split("\n")) {
|
|
479
|
-
if (line.startsWith("import")) {
|
|
480
|
-
imports.push(line);
|
|
481
|
-
} else if (line.startsWith("export")) {
|
|
482
|
-
const match = line.match(exportRegex);
|
|
483
|
-
if (match && match[1]) {
|
|
484
|
-
exportsSet.push(
|
|
485
|
-
...match[1].split(",").map((s) => s.trim()).filter(Boolean)
|
|
486
|
-
);
|
|
487
|
-
}
|
|
1101
|
+
const finalKey = segments[segments.length - 1];
|
|
1102
|
+
const text = providedText || defaultText(finalKey);
|
|
1103
|
+
for (const locale of locales) {
|
|
1104
|
+
const filePath = join7(messagesPath, locale, `${fileName}.json`);
|
|
1105
|
+
await assertSafeTarget(cwd, filePath);
|
|
1106
|
+
let data = {};
|
|
1107
|
+
if (existsSync8(filePath)) {
|
|
1108
|
+
const raw = await readFile8(filePath, "utf-8");
|
|
1109
|
+
try {
|
|
1110
|
+
data = JSON.parse(raw);
|
|
1111
|
+
} catch {
|
|
488
1112
|
}
|
|
489
1113
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1114
|
+
let cursor = data;
|
|
1115
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
1116
|
+
const seg = segments[i];
|
|
1117
|
+
if (!cursor[seg]) cursor[seg] = {};
|
|
1118
|
+
cursor = cursor[seg];
|
|
495
1119
|
}
|
|
496
|
-
|
|
497
|
-
await
|
|
498
|
-
console.log(`\
|
|
499
|
-
}
|
|
500
|
-
console.log(
|
|
501
|
-
`\u2705 Lib "${libName}"${fileName ? ` with module ${fileName}` : ""} added.`
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// src/lib/addApi.ts
|
|
506
|
-
import { join as join6 } from "path";
|
|
507
|
-
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
508
|
-
import prompts5 from "prompts";
|
|
509
|
-
import { existsSync as existsSync6 } from "fs";
|
|
510
|
-
async function addApi(args) {
|
|
511
|
-
let apiName = args[1];
|
|
512
|
-
if (!apiName || apiName.startsWith("-")) {
|
|
513
|
-
const response = await prompts5.prompt({
|
|
514
|
-
type: "text",
|
|
515
|
-
name: "apiName",
|
|
516
|
-
message: "\u{1F50C} API route name to add:",
|
|
517
|
-
validate: (name) => name ? true : "API route name is required"
|
|
518
|
-
});
|
|
519
|
-
apiName = response.apiName;
|
|
1120
|
+
cursor[finalKey] = text;
|
|
1121
|
+
await writeFile7(filePath, JSON.stringify(data, null, 2));
|
|
1122
|
+
console.log(`\u{1F4C4} File updated: ${filePath}`);
|
|
520
1123
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
1124
|
+
console.log(`\u2705 Text added at path "${pathArg}" with value "${text}".`);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/lib/rmPage.ts
|
|
1128
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1129
|
+
import { readFile as readFile9, readdir as readdir6, rm, writeFile as writeFile8 } from "fs/promises";
|
|
1130
|
+
import path6 from "path";
|
|
1131
|
+
async function removeMessages(projectRoot, candidate, terminal) {
|
|
1132
|
+
const messagesRoot = resolveInside(projectRoot, "messages");
|
|
1133
|
+
let locales;
|
|
1134
|
+
try {
|
|
1135
|
+
locales = (await readdir6(messagesRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
1136
|
+
} catch {
|
|
526
1137
|
return;
|
|
527
1138
|
}
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
let content = await readFile5(routeTemplate, "utf-8");
|
|
542
|
-
content = content.replace(/template/g, apiName);
|
|
543
|
-
await writeFile5(routePath, content);
|
|
1139
|
+
for (const locale of locales) {
|
|
1140
|
+
const target = resolveInside(
|
|
1141
|
+
projectRoot,
|
|
1142
|
+
"messages",
|
|
1143
|
+
locale,
|
|
1144
|
+
`${candidate.routeSegments[0]}.json`
|
|
1145
|
+
);
|
|
1146
|
+
if (!existsSync9(target)) continue;
|
|
1147
|
+
if (candidate.messageKey) {
|
|
1148
|
+
const data = JSON.parse(await readFile9(target, "utf8"));
|
|
1149
|
+
delete data[candidate.messageKey];
|
|
1150
|
+
await writeFile8(target, `${JSON.stringify(data, null, 2)}
|
|
1151
|
+
`);
|
|
544
1152
|
} else {
|
|
545
|
-
await
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
export async function GET() {
|
|
550
|
-
return NextResponse.json({ message: "Hello from ${apiName}" });
|
|
1153
|
+
await rm(target);
|
|
1154
|
+
}
|
|
1155
|
+
terminal.log(`\u{1F5D1}\uFE0F Deleted messages for ${candidate.logicalName}: ${target}`);
|
|
1156
|
+
}
|
|
551
1157
|
}
|
|
552
|
-
|
|
553
|
-
|
|
1158
|
+
async function rmPage(args, cwd = process.cwd(), prompt, terminal = console) {
|
|
1159
|
+
const candidates = await discoverPages(cwd);
|
|
1160
|
+
let logicalName = args[1];
|
|
1161
|
+
if (!logicalName || logicalName.startsWith("-")) {
|
|
1162
|
+
if (!prompt)
|
|
1163
|
+
throw new CliError("A page name is required in non-interactive mode.");
|
|
1164
|
+
const selected = await prompt([
|
|
1165
|
+
{
|
|
1166
|
+
type: "autocomplete",
|
|
1167
|
+
name: "page",
|
|
1168
|
+
message: "\u{1F5D1}\uFE0F Page to remove:",
|
|
1169
|
+
choices: candidates.map((candidate2) => ({
|
|
1170
|
+
title: candidate2.logicalName.replaceAll(".", " \u203A "),
|
|
1171
|
+
value: candidate2.logicalName
|
|
1172
|
+
}))
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
type: (value) => value ? "confirm" : null,
|
|
1176
|
+
name: "confirm",
|
|
1177
|
+
message: "Confirm page deletion?",
|
|
1178
|
+
initial: false
|
|
1179
|
+
}
|
|
1180
|
+
]);
|
|
1181
|
+
if (!selected.confirm) {
|
|
1182
|
+
terminal.log("Page deletion cancelled.");
|
|
1183
|
+
return;
|
|
554
1184
|
}
|
|
555
|
-
|
|
556
|
-
} else {
|
|
557
|
-
console.log(`\u2139\uFE0F File already exists: ${routePath}`);
|
|
1185
|
+
logicalName = String(selected.page ?? "");
|
|
558
1186
|
}
|
|
559
|
-
|
|
1187
|
+
parseLogicalName(logicalName, "page name");
|
|
1188
|
+
const candidate = candidates.find(
|
|
1189
|
+
(entry) => entry.logicalName === logicalName
|
|
1190
|
+
);
|
|
1191
|
+
if (!candidate) throw new CliError(`Page not found: ${logicalName}`);
|
|
1192
|
+
await removeMessages(cwd, candidate, terminal);
|
|
1193
|
+
for (const target of [candidate.uiDirectory, candidate.routeDirectory]) {
|
|
1194
|
+
const safeTarget = resolveInside(cwd, path6.relative(cwd, target));
|
|
1195
|
+
if (existsSync9(safeTarget)) {
|
|
1196
|
+
await rm(safeTarget, { recursive: true, force: false });
|
|
1197
|
+
terminal.log(`\u{1F5D1}\uFE0F Deleted: ${safeTarget}`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
terminal.log(`\u2705 Page "${logicalName}" deleted.`);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/cli/registry.ts
|
|
1204
|
+
function legacyHandler(command) {
|
|
1205
|
+
return async (args, context) => {
|
|
1206
|
+
await command(args, context.cwd);
|
|
1207
|
+
return success();
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
function createCommandRegistry() {
|
|
1211
|
+
return /* @__PURE__ */ new Map([
|
|
1212
|
+
["addcomponent", legacyHandler(addComponent)],
|
|
1213
|
+
["addpage", legacyHandler(addPage)],
|
|
1214
|
+
["addlib", legacyHandler(addLib)],
|
|
1215
|
+
["addapi", legacyHandler(addApi)],
|
|
1216
|
+
["addlanguage", legacyHandler(addLanguage)],
|
|
1217
|
+
["addtext", legacyHandler(addText)],
|
|
1218
|
+
[
|
|
1219
|
+
"rmpage",
|
|
1220
|
+
async (args, context) => {
|
|
1221
|
+
await rmPage(args, context.cwd, context.prompt, context.terminal);
|
|
1222
|
+
return success();
|
|
1223
|
+
}
|
|
1224
|
+
]
|
|
1225
|
+
]);
|
|
560
1226
|
}
|
|
561
1227
|
|
|
562
1228
|
// src/scaffold.ts
|
|
563
|
-
import {
|
|
564
|
-
import { join as
|
|
565
|
-
import { existsSync as
|
|
566
|
-
import { fileURLToPath } from "url";
|
|
1229
|
+
import { mkdir as mkdir7, rm as rm2, writeFile as writeFile10 } from "fs/promises";
|
|
1230
|
+
import { join as join8, resolve } from "path";
|
|
1231
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1232
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
567
1233
|
|
|
568
1234
|
// src/lib/helper/consoleColor.ts
|
|
569
1235
|
var RED = "\x1B[31m";
|
|
@@ -580,73 +1246,186 @@ function cyan(text) {
|
|
|
580
1246
|
return CYAN + text + RESET;
|
|
581
1247
|
}
|
|
582
1248
|
|
|
1249
|
+
// src/core/template-manifest.ts
|
|
1250
|
+
import {
|
|
1251
|
+
lstat as lstat2,
|
|
1252
|
+
mkdir as mkdir6,
|
|
1253
|
+
readdir as readdir7,
|
|
1254
|
+
readFile as readFile10,
|
|
1255
|
+
writeFile as writeFile9,
|
|
1256
|
+
copyFile as copyFile2
|
|
1257
|
+
} from "fs/promises";
|
|
1258
|
+
import path7 from "path";
|
|
1259
|
+
var TEMPLATE_DENY_NAMES = /* @__PURE__ */ new Set([
|
|
1260
|
+
".env",
|
|
1261
|
+
".git",
|
|
1262
|
+
".agent",
|
|
1263
|
+
".cursor",
|
|
1264
|
+
".next",
|
|
1265
|
+
"node_modules",
|
|
1266
|
+
"artifacts",
|
|
1267
|
+
"coverage",
|
|
1268
|
+
"playwright-report",
|
|
1269
|
+
"test-results"
|
|
1270
|
+
]);
|
|
1271
|
+
function isDistributableTemplatePath(relativePath) {
|
|
1272
|
+
const segments = relativePath.split(path7.sep);
|
|
1273
|
+
return !segments.some(
|
|
1274
|
+
(segment) => TEMPLATE_DENY_NAMES.has(segment) || segment.endsWith(".tsbuildinfo") || segment === ".DS_Store"
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
async function templateManifest(root) {
|
|
1278
|
+
const files = [];
|
|
1279
|
+
async function visit(directory, relativeDirectory = "") {
|
|
1280
|
+
const entries = await readdir7(directory, { withFileTypes: true });
|
|
1281
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
1282
|
+
for (const entry of entries) {
|
|
1283
|
+
const relative = path7.join(relativeDirectory, entry.name);
|
|
1284
|
+
if (!isDistributableTemplatePath(relative)) continue;
|
|
1285
|
+
const source = path7.join(directory, entry.name);
|
|
1286
|
+
const stats = await lstat2(source);
|
|
1287
|
+
if (stats.isSymbolicLink()) {
|
|
1288
|
+
throw new CliError(
|
|
1289
|
+
`Symbolic links are forbidden in templates: ${relative}`
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
if (stats.isDirectory()) await visit(source, relative);
|
|
1293
|
+
else if (stats.isFile()) files.push(relative);
|
|
1294
|
+
else throw new CliError(`Unsupported template entry: ${relative}`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
await visit(root);
|
|
1298
|
+
return files;
|
|
1299
|
+
}
|
|
1300
|
+
async function copyTemplate(root, target) {
|
|
1301
|
+
const manifest = await templateManifest(root);
|
|
1302
|
+
await mkdir6(target, { recursive: true });
|
|
1303
|
+
for (const relative of manifest) {
|
|
1304
|
+
const destination = path7.join(
|
|
1305
|
+
target,
|
|
1306
|
+
relative === ".gitignore.template" ? ".gitignore" : relative
|
|
1307
|
+
);
|
|
1308
|
+
await mkdir6(path7.dirname(destination), { recursive: true });
|
|
1309
|
+
await copyFile2(path7.join(root, relative), destination);
|
|
1310
|
+
}
|
|
1311
|
+
return manifest;
|
|
1312
|
+
}
|
|
1313
|
+
async function customizeGeneratedProject(target, projectName, importAlias) {
|
|
1314
|
+
const packagePath = path7.join(target, "package.json");
|
|
1315
|
+
const packageJson = JSON.parse(await readFile10(packagePath, "utf8"));
|
|
1316
|
+
packageJson.name = projectName.toLowerCase();
|
|
1317
|
+
delete packageJson.packageManager;
|
|
1318
|
+
await writeFile9(packagePath, `${JSON.stringify(packageJson, null, 2)}
|
|
1319
|
+
`);
|
|
1320
|
+
const tsconfigPath = path7.join(target, "tsconfig.json");
|
|
1321
|
+
const tsconfigContent = await readFile10(tsconfigPath, "utf8");
|
|
1322
|
+
const tsconfig = JSON.parse(tsconfigContent);
|
|
1323
|
+
if (!tsconfig.compilerOptions?.paths?.["@/*"]) {
|
|
1324
|
+
throw new CliError(
|
|
1325
|
+
'The template tsconfig must define the default "@/*" alias.'
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
await writeFile9(
|
|
1329
|
+
tsconfigPath,
|
|
1330
|
+
tsconfigContent.replace('"@/*"', `"${importAlias}"`)
|
|
1331
|
+
);
|
|
1332
|
+
if (importAlias !== "@/*") {
|
|
1333
|
+
const prefix = importAlias.slice(0, -2);
|
|
1334
|
+
for (const relative of await templateManifest(target)) {
|
|
1335
|
+
if (!/\.[cm]?[jt]sx?$/.test(relative)) continue;
|
|
1336
|
+
const file = path7.join(target, relative);
|
|
1337
|
+
const content = await readFile10(file, "utf8");
|
|
1338
|
+
const next = content.replaceAll('from "@/', `from "${prefix}/`).replaceAll("from '@/", `from '${prefix}/`);
|
|
1339
|
+
if (next !== content) await writeFile9(file, next);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
583
1344
|
// src/scaffold.ts
|
|
584
|
-
async function scaffoldProject(options) {
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
"
|
|
590
|
-
"
|
|
591
|
-
"
|
|
592
|
-
|
|
1345
|
+
async function scaffoldProject(options, runtime = {}) {
|
|
1346
|
+
const requiredFeatures = [
|
|
1347
|
+
"useTypescript",
|
|
1348
|
+
"useEslint",
|
|
1349
|
+
"useTailwind",
|
|
1350
|
+
"useSrcDir",
|
|
1351
|
+
"useTurbopack",
|
|
1352
|
+
"useI18n"
|
|
1353
|
+
];
|
|
1354
|
+
const unsupported = requiredFeatures.filter(
|
|
1355
|
+
(feature) => options[feature] !== true
|
|
593
1356
|
);
|
|
594
|
-
if (
|
|
1357
|
+
if (unsupported.length > 0) {
|
|
1358
|
+
throw new CliError(
|
|
1359
|
+
`The default Next.js 16 template requires: ${unsupported.join(", ")}.`
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const cwd = runtime.cwd ?? process.cwd();
|
|
1363
|
+
const terminal = runtime.terminal ?? console;
|
|
1364
|
+
const projectName = validateProjectName(options.projectName);
|
|
1365
|
+
const importAlias = normalizeImportAlias(
|
|
1366
|
+
options.customAlias === false ? "@/*" : options.importAlias || "@/*"
|
|
1367
|
+
);
|
|
1368
|
+
const targetPath = join8(cwd, projectName);
|
|
1369
|
+
const __dirname = new URL(".", import.meta.url);
|
|
1370
|
+
const templatePath = runtime.templatePath ?? join8(fileURLToPath2(__dirname), "..", "templates", "Projects", "default");
|
|
1371
|
+
const resolvedCwd = resolve(cwd);
|
|
1372
|
+
const resolvedTarget = resolve(targetPath);
|
|
1373
|
+
if (!resolvedTarget.startsWith(`${resolvedCwd}/`)) {
|
|
1374
|
+
throw new CliError(
|
|
1375
|
+
"The project destination must be a child of the current directory."
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
if (existsSync10(targetPath)) {
|
|
595
1379
|
if (options.force) {
|
|
596
|
-
|
|
597
|
-
await
|
|
1380
|
+
terminal.warn("\u26A0\uFE0F Target directory already exists, removing...");
|
|
1381
|
+
await rm2(targetPath, { recursive: true, force: true });
|
|
598
1382
|
} else {
|
|
599
|
-
|
|
1383
|
+
terminal.error(
|
|
600
1384
|
red("[X] Target directory already exists. Use --force to overwrite.")
|
|
601
1385
|
);
|
|
602
|
-
|
|
1386
|
+
throw new CliError(
|
|
1387
|
+
"[X] Target directory already exists. Use --force to overwrite."
|
|
1388
|
+
);
|
|
603
1389
|
}
|
|
604
1390
|
}
|
|
605
1391
|
try {
|
|
606
|
-
|
|
607
|
-
await
|
|
608
|
-
|
|
609
|
-
await
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
pkg.dependencies["next-intl"] = pkg.dependencies["next-intl"] || "^4.3.5";
|
|
616
|
-
}
|
|
617
|
-
await writeFile6(pkgPath, JSON.stringify(pkg, null, 2));
|
|
618
|
-
}
|
|
619
|
-
await writeFile6(
|
|
620
|
-
join7(targetPath, "cnp.config.json"),
|
|
621
|
-
JSON.stringify(options, null, 2)
|
|
1392
|
+
terminal.log("Creating project directory...");
|
|
1393
|
+
await mkdir7(targetPath, { recursive: true });
|
|
1394
|
+
terminal.log("Copying files from template...");
|
|
1395
|
+
await copyTemplate(templatePath, targetPath);
|
|
1396
|
+
await customizeGeneratedProject(targetPath, projectName, importAlias);
|
|
1397
|
+
await writeFile10(
|
|
1398
|
+
join8(targetPath, "cnp.config.json"),
|
|
1399
|
+
`${JSON.stringify({ ...options, projectName, importAlias }, null, 2)}
|
|
1400
|
+
`
|
|
622
1401
|
);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1402
|
+
terminal.log("Project setup complete!");
|
|
1403
|
+
terminal.log("");
|
|
1404
|
+
terminal.log("To get started:");
|
|
1405
|
+
terminal.log(" " + green(`cd ${options.projectName}`));
|
|
1406
|
+
terminal.log("");
|
|
1407
|
+
terminal.log(
|
|
629
1408
|
"Then install dependencies and launch the dev server with your preferred tool:"
|
|
630
1409
|
);
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1410
|
+
terminal.log(" " + green(`bun install && bun dev`));
|
|
1411
|
+
terminal.log(" " + green(`npm install && npm run dev`));
|
|
1412
|
+
terminal.log(" " + green(`pnpm install && pnpm run dev`));
|
|
1413
|
+
terminal.log("");
|
|
1414
|
+
terminal.log("Documentation and examples can be found at:");
|
|
1415
|
+
terminal.log(
|
|
637
1416
|
" " + cyan("https://github.com/Rising-Corporation/create-next-pro-cli")
|
|
638
1417
|
);
|
|
639
|
-
|
|
1418
|
+
terminal.log(
|
|
640
1419
|
"_-`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`_`'-_-'`-_"
|
|
641
1420
|
);
|
|
642
1421
|
} catch (err) {
|
|
643
|
-
|
|
644
|
-
|
|
1422
|
+
terminal.error(red("[X] Error during project creation:"), err);
|
|
1423
|
+
throw new CliError("[X] Error during project creation:");
|
|
645
1424
|
}
|
|
646
1425
|
}
|
|
647
1426
|
|
|
648
1427
|
// src/lib/createProject.ts
|
|
649
|
-
async function createProject(nameArg, force) {
|
|
1428
|
+
async function createProject(nameArg, force, context) {
|
|
650
1429
|
const response = {
|
|
651
1430
|
projectName: nameArg,
|
|
652
1431
|
useTypescript: true,
|
|
@@ -659,14 +1438,17 @@ async function createProject(nameArg, force) {
|
|
|
659
1438
|
importAlias: "@/*",
|
|
660
1439
|
force
|
|
661
1440
|
};
|
|
662
|
-
|
|
663
|
-
|
|
1441
|
+
const terminal = context?.terminal ?? console;
|
|
1442
|
+
terminal.log(`Creating project "${response.projectName}"...`);
|
|
1443
|
+
await scaffoldProject(response, {
|
|
1444
|
+
cwd: context?.cwd ?? process.cwd(),
|
|
1445
|
+
terminal
|
|
1446
|
+
});
|
|
664
1447
|
}
|
|
665
1448
|
|
|
666
1449
|
// src/lib/createProjectWithPrompt.ts
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const response = await prompts6.prompt([
|
|
1450
|
+
async function createProjectWithPrompt(context) {
|
|
1451
|
+
const response = await context.prompt([
|
|
670
1452
|
{
|
|
671
1453
|
type: "text",
|
|
672
1454
|
name: "projectName",
|
|
@@ -736,239 +1518,17 @@ async function createProjectWithPrompt() {
|
|
|
736
1518
|
initial: "@core/*"
|
|
737
1519
|
}
|
|
738
1520
|
]);
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
import { join as join8 } from "path";
|
|
746
|
-
import { existsSync as existsSync8 } from "fs";
|
|
747
|
-
import { cp as cp2, readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
|
|
748
|
-
import prompts7 from "prompts";
|
|
749
|
-
function generateLocales() {
|
|
750
|
-
const dn = new Intl.DisplayNames(["en"], { type: "language" });
|
|
751
|
-
const locales = [];
|
|
752
|
-
for (let i = 0; i < 26; i++) {
|
|
753
|
-
for (let j = 0; j < 26; j++) {
|
|
754
|
-
const code = String.fromCharCode(97 + i) + String.fromCharCode(97 + j);
|
|
755
|
-
try {
|
|
756
|
-
const name = dn.of(code);
|
|
757
|
-
if (name && name.toLowerCase() !== code) {
|
|
758
|
-
locales.push(code);
|
|
759
|
-
}
|
|
760
|
-
} catch {
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return locales.sort();
|
|
765
|
-
}
|
|
766
|
-
async function addLanguage(args) {
|
|
767
|
-
const config = await loadConfig();
|
|
768
|
-
if (!config?.useI18n) {
|
|
769
|
-
console.error("\u274C i18n is not enabled in this project.");
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
const messagesPath = join8(process.cwd(), "messages");
|
|
773
|
-
if (!existsSync8(messagesPath)) {
|
|
774
|
-
console.error("\u274C Messages directory missing. Ensure i18n was configured.");
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
const available = generateLocales();
|
|
778
|
-
let locale = args[1];
|
|
779
|
-
if (!locale || !available.includes(locale)) {
|
|
780
|
-
const response = await prompts7({
|
|
781
|
-
type: "autocomplete",
|
|
782
|
-
name: "locale",
|
|
783
|
-
message: "\u{1F310} Locale to add:",
|
|
784
|
-
choices: available.map((l) => ({ title: l, value: l }))
|
|
785
|
-
});
|
|
786
|
-
locale = response.locale;
|
|
787
|
-
}
|
|
788
|
-
if (!locale) return;
|
|
789
|
-
if (existsSync8(join8(messagesPath, locale))) {
|
|
790
|
-
console.error(`\u274C Locale ${locale} already exists.`);
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
const routingFile = join8(process.cwd(), "src", "lib", "i18n", "routing.ts");
|
|
794
|
-
if (!existsSync8(routingFile)) {
|
|
795
|
-
console.error("\u274C routing.ts not found. Are you in project root?");
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
const routingContent = await readFile7(routingFile, "utf-8");
|
|
799
|
-
const defaultMatch = routingContent.match(/defaultLocale:\s*"([^"]+)"/);
|
|
800
|
-
const defaultLocale = defaultMatch ? defaultMatch[1] : null;
|
|
801
|
-
if (!defaultLocale || !existsSync8(join8(messagesPath, defaultLocale))) {
|
|
802
|
-
console.error("\u274C Default locale not found.");
|
|
803
|
-
return;
|
|
804
|
-
}
|
|
805
|
-
await cp2(join8(messagesPath, defaultLocale), join8(messagesPath, locale), {
|
|
806
|
-
recursive: true
|
|
1521
|
+
const options = response;
|
|
1522
|
+
context.terminal.log("\nYour choices:");
|
|
1523
|
+
context.terminal.log(options);
|
|
1524
|
+
await scaffoldProject(options, {
|
|
1525
|
+
cwd: context.cwd,
|
|
1526
|
+
terminal: context.terminal
|
|
807
1527
|
});
|
|
808
|
-
console.log(`\u{1F4C4} Directory created: ${join8(messagesPath, locale)}`);
|
|
809
|
-
const localesMatch = routingContent.match(/locales:\s*\[([^\]]*)\]/);
|
|
810
|
-
if (localesMatch) {
|
|
811
|
-
const localesArr = localesMatch[1].split(",").map((s) => s.trim().replace(/["']/g, "")).filter(Boolean);
|
|
812
|
-
if (!localesArr.includes(locale)) {
|
|
813
|
-
localesArr.push(locale);
|
|
814
|
-
const newLocales = `locales: [${localesArr.map((l) => `"${l}"`).join(", ")}]`;
|
|
815
|
-
const newContent = routingContent.replace(/locales:\s*\[[^\]]*\]/, newLocales);
|
|
816
|
-
await writeFile7(routingFile, newContent);
|
|
817
|
-
console.log(`\u{1F4C4} File updated: ${routingFile}`);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
console.log(
|
|
821
|
-
`\u2705 Locale "${locale}" added and copied from default locale "${defaultLocale}".`
|
|
822
|
-
);
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// src/lib/addText.ts
|
|
826
|
-
import { join as join9 } from "path";
|
|
827
|
-
import { existsSync as existsSync9 } from "fs";
|
|
828
|
-
import { readFile as readFile8, writeFile as writeFile8, readdir as readdir4 } from "fs/promises";
|
|
829
|
-
function defaultText(key) {
|
|
830
|
-
return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
831
|
-
}
|
|
832
|
-
async function addText(args) {
|
|
833
|
-
const pathArg = args[1];
|
|
834
|
-
if (!pathArg) {
|
|
835
|
-
console.error("\u274C Dot path parameter is required.");
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
const providedText = args.slice(2).join(" ");
|
|
839
|
-
const config = await loadConfig();
|
|
840
|
-
if (!config?.useI18n) {
|
|
841
|
-
console.error("\u274C i18n is not enabled in this project.");
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
const messagesPath = join9(process.cwd(), "messages");
|
|
845
|
-
if (!existsSync9(messagesPath)) {
|
|
846
|
-
console.error("\u274C Messages directory missing. Ensure i18n was configured.");
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
const entries = await readdir4(messagesPath, { withFileTypes: true });
|
|
850
|
-
const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
851
|
-
const [fileName, ...segments] = pathArg.split(".");
|
|
852
|
-
if (!fileName || segments.length === 0) {
|
|
853
|
-
console.error("\u274C Invalid dot path provided.");
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
const finalKey = segments[segments.length - 1];
|
|
857
|
-
const text = providedText || defaultText(finalKey);
|
|
858
|
-
for (const locale of locales) {
|
|
859
|
-
const filePath = join9(messagesPath, locale, `${fileName}.json`);
|
|
860
|
-
let data = {};
|
|
861
|
-
if (existsSync9(filePath)) {
|
|
862
|
-
const raw = await readFile8(filePath, "utf-8");
|
|
863
|
-
try {
|
|
864
|
-
data = JSON.parse(raw);
|
|
865
|
-
} catch {
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
let cursor = data;
|
|
869
|
-
for (let i = 0; i < segments.length - 1; i++) {
|
|
870
|
-
const seg = segments[i];
|
|
871
|
-
if (!cursor[seg]) cursor[seg] = {};
|
|
872
|
-
cursor = cursor[seg];
|
|
873
|
-
}
|
|
874
|
-
cursor[finalKey] = text;
|
|
875
|
-
await writeFile8(filePath, JSON.stringify(data, null, 2));
|
|
876
|
-
console.log(`\u{1F4C4} File updated: ${filePath}`);
|
|
877
|
-
}
|
|
878
|
-
console.log(`\u2705 Text added at path "${pathArg}" with value "${text}".`);
|
|
879
1528
|
}
|
|
880
1529
|
|
|
881
1530
|
// src/index.ts
|
|
882
|
-
var
|
|
883
|
-
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
884
|
-
function readCfg() {
|
|
885
|
-
try {
|
|
886
|
-
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
887
|
-
} catch {
|
|
888
|
-
return null;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
function writeCfg(cfg) {
|
|
892
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
893
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
|
894
|
-
}
|
|
895
|
-
function rcFile(shell) {
|
|
896
|
-
return path.join(os.homedir(), shell === "zsh" ? ".zshrc" : ".bashrc");
|
|
897
|
-
}
|
|
898
|
-
function ensureLineInRc(file, line) {
|
|
899
|
-
try {
|
|
900
|
-
const cur = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
901
|
-
if (!cur.includes(line)) fs.appendFileSync(file, `
|
|
902
|
-
${line}
|
|
903
|
-
`);
|
|
904
|
-
} catch {
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
async function installCompletion(shell) {
|
|
908
|
-
const __dirname2 = path.dirname(fileURLToPath2(import.meta.url));
|
|
909
|
-
const completionSrc = path.resolve(
|
|
910
|
-
__dirname2,
|
|
911
|
-
"../create-next-pro-completion.sh"
|
|
912
|
-
);
|
|
913
|
-
const completionDst = path.join(CONFIG_DIR, "completion.sh");
|
|
914
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
915
|
-
fs.copyFileSync(completionSrc, completionDst);
|
|
916
|
-
ensureLineInRc(rcFile(shell), `source "${completionDst}"`);
|
|
917
|
-
}
|
|
918
|
-
var __filename = fileURLToPath2(import.meta.url);
|
|
919
|
-
var __dirname = dirname(__filename);
|
|
920
|
-
var packageJsonPath = resolve(__dirname, "../package.json");
|
|
921
|
-
var packageJson = fs.readFileSync(packageJsonPath, "utf8");
|
|
922
|
-
async function onboarding() {
|
|
923
|
-
const pkg = JSON.parse(packageJson);
|
|
924
|
-
console.log(`\u{1F680} Welcome to create-next-pro v${pkg.version}
|
|
925
|
-
`);
|
|
926
|
-
const res = await prompts8(
|
|
927
|
-
[
|
|
928
|
-
{
|
|
929
|
-
type: "select",
|
|
930
|
-
name: "shell",
|
|
931
|
-
message: "Which shell do you use?",
|
|
932
|
-
choices: [
|
|
933
|
-
{ title: "zsh", value: "zsh" },
|
|
934
|
-
{ title: "bash", value: "bash" }
|
|
935
|
-
],
|
|
936
|
-
initial: (os.userInfo().shell || "").includes("zsh") ? 0 : 1
|
|
937
|
-
},
|
|
938
|
-
{
|
|
939
|
-
type: "toggle",
|
|
940
|
-
name: "completion",
|
|
941
|
-
message: "Install autocompletion?",
|
|
942
|
-
initial: true,
|
|
943
|
-
active: "Yes",
|
|
944
|
-
inactive: "No"
|
|
945
|
-
}
|
|
946
|
-
],
|
|
947
|
-
{ onCancel: () => process.exit(1) }
|
|
948
|
-
);
|
|
949
|
-
const cfg = {
|
|
950
|
-
version: 1,
|
|
951
|
-
shell: res.shell,
|
|
952
|
-
completionInstalled: !!res.completion,
|
|
953
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
954
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
955
|
-
};
|
|
956
|
-
if (cfg.completionInstalled) await installCompletion(cfg.shell);
|
|
957
|
-
writeCfg(cfg);
|
|
958
|
-
console.log("\n\u2705 Configuration saved.");
|
|
959
|
-
console.log("you can now use the CLI ! ex : ");
|
|
960
|
-
console.log(" Without prompt (will change in future) :");
|
|
961
|
-
console.log(" create-next-pro my-next-project");
|
|
962
|
-
console.log(" With prompt :");
|
|
963
|
-
console.log(" create-next-pro");
|
|
964
|
-
console.log(
|
|
965
|
-
"For more information, visit: https://github.com/Rising-Corporation/create-next-pro-cli"
|
|
966
|
-
);
|
|
967
|
-
console.log("Happy coding! \u{1F389}");
|
|
968
|
-
return cfg;
|
|
969
|
-
}
|
|
970
|
-
function showHelp() {
|
|
971
|
-
console.log(`create-next-pro
|
|
1531
|
+
var HELP_TEXT = `create-next-pro
|
|
972
1532
|
|
|
973
1533
|
Usage:
|
|
974
1534
|
create-next-pro <project-name> [--force]
|
|
@@ -982,68 +1542,61 @@ Usage:
|
|
|
982
1542
|
|
|
983
1543
|
Options:
|
|
984
1544
|
--help Show this help message
|
|
1545
|
+
--version Show the CLI version
|
|
985
1546
|
--reconfigure Run the configuration assistant again
|
|
986
|
-
|
|
1547
|
+
`;
|
|
1548
|
+
function parseOptions(args) {
|
|
1549
|
+
return {
|
|
1550
|
+
force: args.includes("--force"),
|
|
1551
|
+
help: args.includes("--help"),
|
|
1552
|
+
version: args.includes("--version") || args.includes("-v"),
|
|
1553
|
+
reconfigure: args.includes("--reconfigure")
|
|
1554
|
+
};
|
|
987
1555
|
}
|
|
988
|
-
function
|
|
989
|
-
const
|
|
990
|
-
|
|
1556
|
+
async function packageVersion(context) {
|
|
1557
|
+
const packageJson = JSON.parse(
|
|
1558
|
+
await context.fs.readText(path8.join(context.packageRoot, "package.json"))
|
|
1559
|
+
);
|
|
1560
|
+
return packageJson.version;
|
|
991
1561
|
}
|
|
992
|
-
async function main() {
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
args
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
if (args[0] === "addlanguage") {
|
|
1028
|
-
await addLanguage(args);
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
if (args[0] === "addtext") {
|
|
1032
|
-
await addText(args);
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
if (args[0] === "rmpage") {
|
|
1036
|
-
rmPage(args);
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
const nameArg = args.find((arg) => !arg.startsWith("--"));
|
|
1040
|
-
if (nameArg) {
|
|
1041
|
-
createProject(nameArg, force);
|
|
1042
|
-
return;
|
|
1562
|
+
async function main(context = createNodeContext()) {
|
|
1563
|
+
try {
|
|
1564
|
+
const args = [...context.argv];
|
|
1565
|
+
const options = parseOptions(args);
|
|
1566
|
+
const version = await packageVersion(context);
|
|
1567
|
+
if (args[0] === "__complete") {
|
|
1568
|
+
await printCompletions(args, context);
|
|
1569
|
+
return 0;
|
|
1570
|
+
}
|
|
1571
|
+
if (options.help) {
|
|
1572
|
+
context.terminal.log(HELP_TEXT);
|
|
1573
|
+
return 0;
|
|
1574
|
+
}
|
|
1575
|
+
if (options.version) {
|
|
1576
|
+
context.terminal.log(`v${version}`);
|
|
1577
|
+
return 0;
|
|
1578
|
+
}
|
|
1579
|
+
if (options.reconfigure || !await readConfig(context)) {
|
|
1580
|
+
await onboarding(context, version);
|
|
1581
|
+
return 0;
|
|
1582
|
+
}
|
|
1583
|
+
if (args[0] === "addpage" && args.length === 1) args.push("-LPl");
|
|
1584
|
+
const handler = args[0] ? createCommandRegistry().get(args[0]) : void 0;
|
|
1585
|
+
if (handler) return (await handler(args, context)).exitCode;
|
|
1586
|
+
const nameArg = args.find((arg) => !arg.startsWith("--"));
|
|
1587
|
+
if (nameArg) {
|
|
1588
|
+
await createProject(nameArg, options.force, context);
|
|
1589
|
+
return 0;
|
|
1590
|
+
}
|
|
1591
|
+
await createProjectWithPrompt(context);
|
|
1592
|
+
return 0;
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1595
|
+
context.terminal.error(message);
|
|
1596
|
+
return error instanceof CliError ? error.exitCode : 1;
|
|
1043
1597
|
}
|
|
1044
|
-
await createProjectWithPrompt();
|
|
1045
1598
|
}
|
|
1046
1599
|
|
|
1047
1600
|
// bin.node.ts
|
|
1048
|
-
main();
|
|
1601
|
+
process.exitCode = await main();
|
|
1049
1602
|
//# sourceMappingURL=bin.node.js.map
|