@skalfa/skalfa-cli 1.0.11 → 1.0.13
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/dist/bin/skalfa.js +24 -10
- package/dist/commands/add-extension.js +69 -0
- package/dist/commands/agent.js +27 -0
- package/dist/commands/create-api.js +107 -9
- package/dist/commands/create-app.js +64 -5
- package/dist/commands/init.js +10 -1
- package/dist/commands/pick.js +131 -3
- package/package.json +1 -1
package/dist/bin/skalfa.js
CHANGED
|
@@ -63,22 +63,25 @@ program
|
|
|
63
63
|
.command("init")
|
|
64
64
|
.description("Initialize a new Skalfa monorepo project containing both API and App.")
|
|
65
65
|
.argument("[name]", "project folder name")
|
|
66
|
-
.
|
|
67
|
-
|
|
66
|
+
.option("-a, --auth <type>", "Authentication type: username or email")
|
|
67
|
+
.action(async (name, options) => {
|
|
68
|
+
await runCommand(() => (0, init_1.initProject)(name, options?.auth ? { authType: options.auth } : undefined));
|
|
68
69
|
});
|
|
69
70
|
program
|
|
70
71
|
.command("create:api")
|
|
71
72
|
.description("Create a new Skalfa API project.")
|
|
72
73
|
.argument("<name>", "project folder and package name")
|
|
73
|
-
.
|
|
74
|
-
|
|
74
|
+
.option("-a, --auth <type>", "Authentication type: username or email")
|
|
75
|
+
.action(async (name, options) => {
|
|
76
|
+
await runCommand(() => (0, create_api_1.createApi)(name, options.auth ? { authType: options.auth } : undefined));
|
|
75
77
|
});
|
|
76
78
|
program
|
|
77
79
|
.command("create:app")
|
|
78
80
|
.description("Create a new Skalfa App Next.js project.")
|
|
79
81
|
.argument("<name>", "project folder and package name")
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
+
.option("-a, --auth <type>", "Authentication type: username or email")
|
|
83
|
+
.action(async (name, options) => {
|
|
84
|
+
await runCommand(() => (0, create_app_1.createApp)(name, options.auth ? { authType: options.auth } : undefined));
|
|
82
85
|
});
|
|
83
86
|
program
|
|
84
87
|
.command("add")
|
|
@@ -89,10 +92,21 @@ program
|
|
|
89
92
|
});
|
|
90
93
|
program
|
|
91
94
|
.command("pick")
|
|
92
|
-
.description("Eject/copy a core utility
|
|
93
|
-
.argument("<
|
|
94
|
-
.
|
|
95
|
-
|
|
95
|
+
.description("Eject/copy a core utility or component into your local project for customization.")
|
|
96
|
+
.argument("<name>", "utility or component name")
|
|
97
|
+
.argument("[newName]", "new name for the component (optional, component only)")
|
|
98
|
+
.action(async (name, newName) => {
|
|
99
|
+
await runCommand(() => {
|
|
100
|
+
if (pick_1.UTILITIES.includes(name)) {
|
|
101
|
+
if (newName) {
|
|
102
|
+
throw new Error("Renaming is only supported for components, not utilities.");
|
|
103
|
+
}
|
|
104
|
+
(0, pick_1.pickUtility)(name);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
(0, pick_1.pickComponent)(name, newName);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
96
110
|
});
|
|
97
111
|
program
|
|
98
112
|
.command("update")
|
|
@@ -43,6 +43,75 @@ async function addExtension(extensionName) {
|
|
|
43
43
|
(0, installer_1.installPackage)(projectRoot, isDev ? "file:../skalfa-idb" : "@skalfa/skalfa-idb");
|
|
44
44
|
addTsconfigPath(node_path_1.default.join(projectRoot, "tsconfig.json"), "@skalfa/skalfa-idb");
|
|
45
45
|
addUtilExport(node_path_1.default.join(projectRoot, "utils", "index.ts"), "@skalfa/skalfa-idb");
|
|
46
|
+
// Scaffold IDBProvider
|
|
47
|
+
console.log("Scaffolding IDBProvider...");
|
|
48
|
+
const providerDir = node_path_1.default.join(projectRoot, "components", "base.components", "wrap");
|
|
49
|
+
if (!node_fs_1.default.existsSync(providerDir)) {
|
|
50
|
+
node_fs_1.default.mkdirSync(providerDir, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
const providerPath = node_path_1.default.join(providerDir, "IDBProvider.tsx");
|
|
53
|
+
const providerContent = `"use client"
|
|
54
|
+
|
|
55
|
+
import { useEffect } from "react"
|
|
56
|
+
import { idb } from "@skalfa/skalfa-idb"
|
|
57
|
+
import { AppSchema } from "@schema"
|
|
58
|
+
import { registry } from "@utils"
|
|
59
|
+
|
|
60
|
+
export function IDBProvider({ children }: { children: React.ReactNode }) {
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
idb.setDefaultSchema(AppSchema);
|
|
63
|
+
registry.register("idb", idb);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
return <>{children}</>
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
node_fs_1.default.writeFileSync(providerPath, providerContent, "utf8");
|
|
70
|
+
console.log(`Created: ${providerPath}`);
|
|
71
|
+
// Scaffold app.schema.ts
|
|
72
|
+
console.log("Scaffolding AppSchema...");
|
|
73
|
+
const schemaDir = node_path_1.default.join(projectRoot, "schema", "idb");
|
|
74
|
+
if (!node_fs_1.default.existsSync(schemaDir)) {
|
|
75
|
+
node_fs_1.default.mkdirSync(schemaDir, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
const schemaPath = node_path_1.default.join(schemaDir, "app.schema.ts");
|
|
78
|
+
const schemaContent = `import { DBSchema } from "@skalfa/skalfa-idb"
|
|
79
|
+
|
|
80
|
+
const name = String(process.env.NEXT_PUBLIC_APP_NAME || "").toLowerCase().trim().replace(/[^\\w\\s-]/g, "").replace(/[\\s_-]+/g, "-").replace(/^-+|-+$/g, "") + ".idb-app";
|
|
81
|
+
|
|
82
|
+
export const AppSchema: DBSchema = {
|
|
83
|
+
name: name,
|
|
84
|
+
version: 1,
|
|
85
|
+
stores: {}
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
node_fs_1.default.writeFileSync(schemaPath, schemaContent, "utf8");
|
|
89
|
+
console.log(`Created: ${schemaPath}`);
|
|
90
|
+
// Ensure schema/index.ts exists so the @schema alias works
|
|
91
|
+
const schemaIndexPath = node_path_1.default.join(projectRoot, "schema", "index.ts");
|
|
92
|
+
if (!node_fs_1.default.existsSync(schemaIndexPath)) {
|
|
93
|
+
node_fs_1.default.writeFileSync(schemaIndexPath, `export * from "./idb/app.schema";\n`, "utf8");
|
|
94
|
+
console.log(`Created: ${schemaIndexPath}`);
|
|
95
|
+
}
|
|
96
|
+
// Update app/layout.tsx
|
|
97
|
+
console.log("Updating app/layout.tsx...");
|
|
98
|
+
const layoutPath = node_path_1.default.join(projectRoot, "app", "layout.tsx");
|
|
99
|
+
if (node_fs_1.default.existsSync(layoutPath)) {
|
|
100
|
+
let layoutContent = node_fs_1.default.readFileSync(layoutPath, "utf8");
|
|
101
|
+
// 1. Add IDBProvider to import
|
|
102
|
+
if (layoutContent.includes('import { ShortcutProvider } from "@components";')) {
|
|
103
|
+
layoutContent = layoutContent.replace('import { ShortcutProvider } from "@components";', 'import { IDBProvider, ShortcutProvider } from "@components";');
|
|
104
|
+
}
|
|
105
|
+
else if (layoutContent.includes('import { ShortcutProvider } from "@components"')) {
|
|
106
|
+
layoutContent = layoutContent.replace('import { ShortcutProvider } from "@components"', 'import { IDBProvider, ShortcutProvider } from "@components"');
|
|
107
|
+
}
|
|
108
|
+
// 2. Wrap {children} with <IDBProvider>
|
|
109
|
+
if (layoutContent.includes("{children}") && !layoutContent.includes("<IDBProvider>")) {
|
|
110
|
+
layoutContent = layoutContent.replace(/\{\s*children\s*\}/, `<IDBProvider>\n {children}\n </IDBProvider>`);
|
|
111
|
+
}
|
|
112
|
+
node_fs_1.default.writeFileSync(layoutPath, layoutContent, "utf8");
|
|
113
|
+
console.log(`Updated: ${layoutPath}`);
|
|
114
|
+
}
|
|
46
115
|
}
|
|
47
116
|
else if (extensionName === "socket") {
|
|
48
117
|
console.log("Installing Skalfa Socket.io client extension...");
|
package/dist/commands/agent.js
CHANGED
|
@@ -67,6 +67,33 @@ async function installAgent(overrideType, targetProjectDir = process.cwd()) {
|
|
|
67
67
|
console.log(`Created initial record: ${file}`);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
if (type === "app") {
|
|
71
|
+
// Detect auth type from page.tsx or similar
|
|
72
|
+
const loginPagePath = node_path_1.default.join(targetProjectDir, "app", "auth", "login", "page.tsx");
|
|
73
|
+
let isEmail = true;
|
|
74
|
+
if (node_fs_1.default.existsSync(loginPagePath)) {
|
|
75
|
+
const loginContent = node_fs_1.default.readFileSync(loginPagePath, "utf8");
|
|
76
|
+
isEmail = loginContent.includes("email");
|
|
77
|
+
}
|
|
78
|
+
const updateFeaturesFile = (filePath) => {
|
|
79
|
+
if (node_fs_1.default.existsSync(filePath)) {
|
|
80
|
+
let content = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
81
|
+
if (isEmail) {
|
|
82
|
+
content = content
|
|
83
|
+
.replace(/\|\s*`admin`\s*\|/g, "| `admin@mail.com` |")
|
|
84
|
+
.replace(/\|\s*`user`\s*\|/g, "| `user@mail.com` |");
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
content = content
|
|
88
|
+
.replace(/\|\s*`admin`\s*\|/g, "| `admin` |")
|
|
89
|
+
.replace(/\|\s*`user`\s*\|/g, "| `user` |");
|
|
90
|
+
}
|
|
91
|
+
node_fs_1.default.writeFileSync(filePath, content, "utf8");
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
updateFeaturesFile(node_path_1.default.join(templatesDir, "features.md"));
|
|
95
|
+
updateFeaturesFile(node_path_1.default.join(recordsDir, "features.md"));
|
|
96
|
+
}
|
|
70
97
|
}
|
|
71
98
|
// 3. Add /.agents/ to project's .gitignore
|
|
72
99
|
const gitignorePath = node_path_1.default.join(targetProjectDir, ".gitignore");
|
|
@@ -48,10 +48,13 @@ async function createApi(projectName, options) {
|
|
|
48
48
|
let hasCron = options?.cron ?? false;
|
|
49
49
|
let hasDa = options?.da ?? false;
|
|
50
50
|
let hasSocket = options?.socket ?? false;
|
|
51
|
+
let authType = options?.authType ?? "username";
|
|
51
52
|
if (!options) {
|
|
52
53
|
// Ask interactive questions sequentially using a single readline interface
|
|
53
54
|
const q = new Questioner();
|
|
54
55
|
try {
|
|
56
|
+
const authChoice = (await q.ask("Choose authentication type (username/email) [default: username]: ", "username")).toLowerCase();
|
|
57
|
+
authType = authChoice === "email" ? "email" : "username";
|
|
55
58
|
hasRedis = (await q.ask("Do you need Redis? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
56
59
|
hasQueue = (await q.ask("Do you need Queue? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
57
60
|
hasCache = (await q.ask("Do you need Cache? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
@@ -188,7 +191,8 @@ If you are an AI coding agent assisting with this project, please make sure to r
|
|
|
188
191
|
cache: hasCache,
|
|
189
192
|
cron: hasCron,
|
|
190
193
|
da: hasDa,
|
|
191
|
-
socket: hasSocket
|
|
194
|
+
socket: hasSocket,
|
|
195
|
+
authType: authType
|
|
192
196
|
});
|
|
193
197
|
spinner.update("Installing dependencies (this may take a moment)...");
|
|
194
198
|
await (0, installer_1.installDependenciesAsync)(target);
|
|
@@ -221,35 +225,37 @@ function customizeProject(target, opts) {
|
|
|
221
225
|
const pkg = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
|
|
222
226
|
pkg.dependencies = pkg.dependencies || {};
|
|
223
227
|
pkg.scripts = pkg.scripts || {};
|
|
228
|
+
const isMonorepo = node_path_1.default.basename(target) === "api" || node_path_1.default.basename(target) === "app";
|
|
229
|
+
const devPathPrefix = isMonorepo ? "file:../../" : "file:../";
|
|
224
230
|
// Base ORM integration (always included)
|
|
225
|
-
pkg.dependencies["@skalfa/skalfa-orm"] = isDev ?
|
|
231
|
+
pkg.dependencies["@skalfa/skalfa-orm"] = isDev ? `${devPathPrefix}skalfa-orm` : "^1.0.0";
|
|
226
232
|
if (isDev) {
|
|
227
|
-
pkg.dependencies["@skalfa/skalfa-api-core"] =
|
|
233
|
+
pkg.dependencies["@skalfa/skalfa-api-core"] = `${devPathPrefix}skalfa-api-core`;
|
|
228
234
|
}
|
|
229
235
|
const devCommands = ["bun run --watch app/app.ts", "bun skalfa watch:barrels"];
|
|
230
236
|
if (opts.redis) {
|
|
231
|
-
pkg.dependencies["@skalfa/skalfa-redis"] = isDev ?
|
|
237
|
+
pkg.dependencies["@skalfa/skalfa-redis"] = isDev ? `${devPathPrefix}skalfa-redis` : "^1.0.0";
|
|
232
238
|
pkg.dependencies["ioredis"] = "^5.4.1";
|
|
233
239
|
}
|
|
234
240
|
if (opts.queue) {
|
|
235
|
-
pkg.dependencies["@skalfa/skalfa-queue"] = isDev ?
|
|
241
|
+
pkg.dependencies["@skalfa/skalfa-queue"] = isDev ? `${devPathPrefix}skalfa-queue` : "^1.0.0";
|
|
236
242
|
pkg.scripts["start:queue"] = "bun run app/jobs/queues/worker.queue.ts";
|
|
237
243
|
devCommands.push("bun start:queue");
|
|
238
244
|
}
|
|
239
245
|
if (opts.cache) {
|
|
240
|
-
pkg.dependencies["@skalfa/skalfa-cache"] = isDev ?
|
|
246
|
+
pkg.dependencies["@skalfa/skalfa-cache"] = isDev ? `${devPathPrefix}skalfa-cache` : "^1.0.0";
|
|
241
247
|
}
|
|
242
248
|
if (opts.cron) {
|
|
243
|
-
pkg.dependencies["@skalfa/skalfa-cron"] = isDev ?
|
|
249
|
+
pkg.dependencies["@skalfa/skalfa-cron"] = isDev ? `${devPathPrefix}skalfa-cron` : "^1.0.0";
|
|
244
250
|
pkg.scripts["start:cron"] = "bun run app/jobs/crons/worker.cron.ts";
|
|
245
251
|
devCommands.push("bun start:cron");
|
|
246
252
|
}
|
|
247
253
|
if (opts.da) {
|
|
248
|
-
pkg.dependencies["@skalfa/skalfa-da"] = isDev ?
|
|
254
|
+
pkg.dependencies["@skalfa/skalfa-da"] = isDev ? `${devPathPrefix}skalfa-da` : "^1.0.0";
|
|
249
255
|
pkg.dependencies["@clickhouse/client"] = "^1.6.0";
|
|
250
256
|
}
|
|
251
257
|
if (opts.socket) {
|
|
252
|
-
pkg.dependencies["@skalfa/skalfa-socket"] = isDev ?
|
|
258
|
+
pkg.dependencies["@skalfa/skalfa-socket"] = isDev ? `${devPathPrefix}skalfa-socket` : "^1.0.0";
|
|
253
259
|
pkg.dependencies["socket.io"] = "^4.7.5";
|
|
254
260
|
pkg.scripts["start:socket"] = "bun run app/jobs/sockets/worker.socket.ts";
|
|
255
261
|
devCommands.push("bun start:socket");
|
|
@@ -349,6 +355,98 @@ function customizeProject(target, opts) {
|
|
|
349
355
|
}
|
|
350
356
|
node_fs_1.default.writeFileSync(appTsPath, content, "utf8");
|
|
351
357
|
}
|
|
358
|
+
// 6. Handle authentication type customization
|
|
359
|
+
if (opts.authType === "username") {
|
|
360
|
+
// A. Modify migration: database/migrations/0000_00/users.ts
|
|
361
|
+
const migrationDir = node_path_1.default.join(target, "database", "migrations", "0000_00");
|
|
362
|
+
const migrationUsersPath = node_path_1.default.join(migrationDir, "users.ts");
|
|
363
|
+
if (node_fs_1.default.existsSync(migrationUsersPath)) {
|
|
364
|
+
let content = node_fs_1.default.readFileSync(migrationUsersPath, "utf8");
|
|
365
|
+
// Replace table.string("email").unique().notNullable() with table.string("username").unique().notNullable()
|
|
366
|
+
content = content.replace(/table\.string\("email"\)\.unique\(\)\.notNullable\(\)/g, 'table.string("username").unique().notNullable()');
|
|
367
|
+
// Remove table.timestamp("email_verification_at")
|
|
368
|
+
content = content.replace(/table\.timestamp\("email_verification_at"\)\r?\n?/g, '');
|
|
369
|
+
// Delete the user_mail_tokens table schema creation
|
|
370
|
+
content = content.replace(/await\s+knex\.schema\.createTable\("user_mail_tokens",\s*\((table|t)\)\s*=>\s*\{[\s\S]*?\}\)\r?\n?/g, '');
|
|
371
|
+
node_fs_1.default.writeFileSync(migrationUsersPath, content, "utf8");
|
|
372
|
+
}
|
|
373
|
+
// B. Modify model: app/models/iam/user.model.ts
|
|
374
|
+
const userModelPath = node_path_1.default.join(target, "app", "models", "iam", "user.model.ts");
|
|
375
|
+
if (node_fs_1.default.existsSync(userModelPath)) {
|
|
376
|
+
let content = node_fs_1.default.readFileSync(userModelPath, "utf8");
|
|
377
|
+
// Replace email field with username field
|
|
378
|
+
content = content.replace(/@Field\(\s*\[\s*"fillable"\s*,\s*"selectable"\s*,\s*"searchable"\s*\]\s*\)\s*\r?\n?\s*email!:\s*string/g, '@Field(["fillable", "selectable", "searchable"])\n username!: string');
|
|
379
|
+
// Remove email_verification_at field
|
|
380
|
+
content = content.replace(/@Field\(\s*\[\s*"fillable"\s*,\s*"selectable"\s*,\s*"searchable"\s*\]\s*\)\s*\r?\n?\s*email_verification_at!:\s*Date/g, '');
|
|
381
|
+
node_fs_1.default.writeFileSync(userModelPath, content, "utf8");
|
|
382
|
+
}
|
|
383
|
+
// C. Modify controller: app/controllers/iam/auth.controller.ts
|
|
384
|
+
const authControllerPath = node_path_1.default.join(target, "app", "controllers", "iam", "auth.controller.ts");
|
|
385
|
+
if (node_fs_1.default.existsSync(authControllerPath)) {
|
|
386
|
+
let content = node_fs_1.default.readFileSync(authControllerPath, "utf8");
|
|
387
|
+
// Remove import { UserMailToken } from "app/outputs/mails";
|
|
388
|
+
content = content.replace(/import\s*\{\s*UserMailToken\s*\}\s*from\s*["']app\/outputs\/mails["'];?\r?\n?/g, '');
|
|
389
|
+
// Update validation and logic in login
|
|
390
|
+
content = content.replace(/email\s*:\s*["']required["'],/g, 'username : "required",');
|
|
391
|
+
content = content.replace(/const\s*\{\s*email\s*,\s*password\s*\}\s*=\s*c\.body/g, 'const { username, password } = c.body');
|
|
392
|
+
content = content.replace(/const user\s*=\s*await\s*User\.query\(\)\.where\("email",\s*email\)\.whereNotNull\("email_verification_at"\)\.first\(\);/g, 'const user = await User.query().where("username", username).first();');
|
|
393
|
+
content = content.replace(/if\s*\(!user\)\s*return\s*c\.responseErrorValidation\(\{\s*email\s*:\s*\[\s*["']E-mail not found!["']\s*\]\s*\}\)/g, 'if (!user) return c.responseErrorValidation({username: ["Username not found!"]})');
|
|
394
|
+
// Remove register and verify methods
|
|
395
|
+
content = content.replace(/\/\/\s*=+\s*>\s*\/\/\s*## Register new account\.[\s\S]*?(?=\/\/\s*=+\s*>\s*\/\/\s*## Get logged account)/g, '');
|
|
396
|
+
// Replace update method validation: email: "required" -> username: "required"
|
|
397
|
+
content = content.replace(/email\s*:\s*["']required["'],/g, 'username : "required",');
|
|
398
|
+
node_fs_1.default.writeFileSync(authControllerPath, content, "utf8");
|
|
399
|
+
}
|
|
400
|
+
// D. Modify router: app/routes/base.routes.ts
|
|
401
|
+
const baseRoutesPath = node_path_1.default.join(target, "app", "routes", "base.routes.ts");
|
|
402
|
+
if (node_fs_1.default.existsSync(baseRoutesPath)) {
|
|
403
|
+
let content = node_fs_1.default.readFileSync(baseRoutesPath, "utf8");
|
|
404
|
+
// Comment out register and verify routes
|
|
405
|
+
content = content.replace(/route\.post\('\/register',\s*AuthController\.register\)/g, '// route.post(\'/register\', AuthController.register) - disabled in username auth');
|
|
406
|
+
content = content.replace(/route\.post\('\/verify',\s*AuthController\.verify\)/g, '// route.post(\'/verify\', AuthController.verify) - disabled in username auth');
|
|
407
|
+
node_fs_1.default.writeFileSync(baseRoutesPath, content, "utf8");
|
|
408
|
+
}
|
|
409
|
+
// E. Modify controller: app/controllers/iam/user.controller.ts
|
|
410
|
+
const userControllerPath = node_path_1.default.join(target, "app", "controllers", "iam", "user.controller.ts");
|
|
411
|
+
if (node_fs_1.default.existsSync(userControllerPath)) {
|
|
412
|
+
let content = node_fs_1.default.readFileSync(userControllerPath, "utf8");
|
|
413
|
+
// Replace validation in store: email : ["required", "email"], -> username : ["required"],
|
|
414
|
+
content = content.replace(/email\s*:\s*\[\s*["']required["']\s*,\s*["']email["']\s*\]/g, 'username : ["required"]');
|
|
415
|
+
// Replace validation in update: email : "required", -> username : "required",
|
|
416
|
+
content = content.replace(/email\s*:\s*["']required["']/g, 'username : "required"');
|
|
417
|
+
node_fs_1.default.writeFileSync(userControllerPath, content, "utf8");
|
|
418
|
+
}
|
|
419
|
+
// F. Modify seeder: database/seeders/user.seeder.ts
|
|
420
|
+
const userSeederPath = node_path_1.default.join(target, "database", "seeders", "user.seeder.ts");
|
|
421
|
+
if (node_fs_1.default.existsSync(userSeederPath)) {
|
|
422
|
+
let content = node_fs_1.default.readFileSync(userSeederPath, "utf8");
|
|
423
|
+
// Seed roles: Admin and User
|
|
424
|
+
content = content.replace(/\{"name": "Petugas"\}/g, '{"name": "User"}');
|
|
425
|
+
// Seed users with username instead of email
|
|
426
|
+
content = content.replace(/\{"name": "Admin", "email": "admin@skalfa.id",/g, '{"name": "Admin", "username": "admin",');
|
|
427
|
+
content = content.replace(/\{"name": "Petugas", "email": "petugas@skalfa.id",/g, '{"name": "User", "username": "user",');
|
|
428
|
+
node_fs_1.default.writeFileSync(userSeederPath, content, "utf8");
|
|
429
|
+
}
|
|
430
|
+
// G. Delete app/outputs/mails folder
|
|
431
|
+
const mailsDir = node_path_1.default.join(target, "app", "outputs", "mails");
|
|
432
|
+
if (node_fs_1.default.existsSync(mailsDir)) {
|
|
433
|
+
node_fs_1.default.rmSync(mailsDir, { recursive: true, force: true });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
// If authType === "email":
|
|
438
|
+
// Change seeder from petugas@skalfa.id to user@mail.com and admin@skalfa.id to admin@mail.com, role Petugas to User
|
|
439
|
+
const userSeederPath = node_path_1.default.join(target, "database", "seeders", "user.seeder.ts");
|
|
440
|
+
if (node_fs_1.default.existsSync(userSeederPath)) {
|
|
441
|
+
let content = node_fs_1.default.readFileSync(userSeederPath, "utf8");
|
|
442
|
+
content = content.replace(/\{"name": "Petugas"\}/g, '{"name": "User"}');
|
|
443
|
+
content = content.replace(/admin@skalfa.id/g, 'admin@mail.com');
|
|
444
|
+
content = content.replace(/\{"name": "Petugas", "email": "petugas@mail.com",/g, '{"name": "User", "email": "user@mail.com",');
|
|
445
|
+
content = content.replace(/\{"name": "Petugas", "email": "petugas@skalfa.id",/g, '{"name": "User", "email": "user@mail.com",');
|
|
446
|
+
content = content.replace(/petugas@skalfa.id/g, 'user@mail.com');
|
|
447
|
+
node_fs_1.default.writeFileSync(userSeederPath, content, "utf8");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
352
450
|
}
|
|
353
451
|
function addTsconfigPath(tsconfigPath, packageName) {
|
|
354
452
|
if (!node_fs_1.default.existsSync(tsconfigPath))
|
|
@@ -47,10 +47,13 @@ async function createApp(projectName, options) {
|
|
|
47
47
|
let hasPwa = options?.pwa ?? false;
|
|
48
48
|
let hasTauriDesktop = options?.tauriDesktop ?? false;
|
|
49
49
|
let hasTauriMobile = options?.tauriMobile ?? false;
|
|
50
|
+
let authType = options?.authType ?? "username";
|
|
50
51
|
if (!options) {
|
|
51
52
|
// Ask interactive questions sequentially
|
|
52
53
|
const q = new Questioner();
|
|
53
54
|
try {
|
|
55
|
+
const authChoice = (await q.ask("Choose authentication type (username/email) [default: username]: ", "username")).toLowerCase();
|
|
56
|
+
authType = authChoice === "email" ? "email" : "username";
|
|
54
57
|
hasIdb = (await q.ask("Do you need IndexedDB (IDB)? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
55
58
|
hasSocket = (await q.ask("Do you need Socket Client? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
56
59
|
hasDocument = (await q.ask("Do you need Document Export/Viewer (PDF/Excel)? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
@@ -174,7 +177,8 @@ If you are an AI coding agent assisting with this project, please make sure to r
|
|
|
174
177
|
document: hasDocument,
|
|
175
178
|
pwa: hasPwa,
|
|
176
179
|
tauriDesktop: hasTauriDesktop,
|
|
177
|
-
tauriMobile: hasTauriMobile
|
|
180
|
+
tauriMobile: hasTauriMobile,
|
|
181
|
+
authType: authType
|
|
178
182
|
});
|
|
179
183
|
spinner.update("Installing dependencies (this may take a moment)...");
|
|
180
184
|
await (0, installer_1.installDependenciesAsync)(target);
|
|
@@ -200,6 +204,8 @@ function customizeProject(target, opts) {
|
|
|
200
204
|
const packageJsonPath = node_path_1.default.join(target, "package.json");
|
|
201
205
|
const baseComponentsIndexPath = node_path_1.default.join(target, "components", "base.components", "index.ts");
|
|
202
206
|
const isDev = !!process.env[TEMPLATE_ENV_KEY];
|
|
207
|
+
const isMonorepo = node_path_1.default.basename(target) === "api" || node_path_1.default.basename(target) === "app";
|
|
208
|
+
const devPathPrefix = isMonorepo ? "file:../../" : "file:../";
|
|
203
209
|
// 1. Update dependencies and scripts in package.json
|
|
204
210
|
if (node_fs_1.default.existsSync(packageJsonPath)) {
|
|
205
211
|
const pkg = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, "utf8"));
|
|
@@ -207,10 +213,13 @@ function customizeProject(target, opts) {
|
|
|
207
213
|
pkg.devDependencies = pkg.devDependencies || {};
|
|
208
214
|
pkg.scripts = pkg.scripts || {};
|
|
209
215
|
// Core dependency
|
|
210
|
-
pkg.dependencies["@skalfa/skalfa-app-core"] = isDev ?
|
|
216
|
+
pkg.dependencies["@skalfa/skalfa-app-core"] = isDev ? `${devPathPrefix}skalfa-app-core` : "^1.0.0";
|
|
217
|
+
if (isDev && pkg.dependencies["@skalfa/skalfa-component"]) {
|
|
218
|
+
pkg.dependencies["@skalfa/skalfa-component"] = `${devPathPrefix}skalfa-component`;
|
|
219
|
+
}
|
|
211
220
|
// A. IndexedDB Option
|
|
212
221
|
if (opts.idb) {
|
|
213
|
-
pkg.dependencies["@skalfa/skalfa-idb"] = isDev ?
|
|
222
|
+
pkg.dependencies["@skalfa/skalfa-idb"] = isDev ? `${devPathPrefix}skalfa-idb` : "^1.0.0";
|
|
214
223
|
}
|
|
215
224
|
else {
|
|
216
225
|
// Delete schema directory
|
|
@@ -242,12 +251,12 @@ function customizeProject(target, opts) {
|
|
|
242
251
|
}
|
|
243
252
|
// B. Socket Option
|
|
244
253
|
if (opts.socket) {
|
|
245
|
-
pkg.dependencies["@skalfa/skalfa-socket-client"] = isDev ?
|
|
254
|
+
pkg.dependencies["@skalfa/skalfa-socket-client"] = isDev ? `${devPathPrefix}skalfa-socket-client` : "^1.0.0";
|
|
246
255
|
pkg.dependencies["socket.io-client"] = "^4.8.1";
|
|
247
256
|
}
|
|
248
257
|
// C. Document Option
|
|
249
258
|
if (opts.document) {
|
|
250
|
-
pkg.dependencies["@skalfa/skalfa-document"] = isDev ?
|
|
259
|
+
pkg.dependencies["@skalfa/skalfa-document"] = isDev ? `${devPathPrefix}skalfa-document` : "^1.0.0";
|
|
251
260
|
pkg.dependencies["exceljs"] = "^4.4.0";
|
|
252
261
|
pkg.dependencies["pdf-lib"] = "^1.17.1";
|
|
253
262
|
pkg.dependencies["pdfjs-dist"] = "^4.4.168";
|
|
@@ -316,4 +325,54 @@ function customizeProject(target, opts) {
|
|
|
316
325
|
}
|
|
317
326
|
node_fs_1.default.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), "utf8");
|
|
318
327
|
}
|
|
328
|
+
// 6. Handle authentication type customization
|
|
329
|
+
if (opts.authType === "username") {
|
|
330
|
+
// A. Modify app/auth/login/page.tsx
|
|
331
|
+
const loginPagePath = node_path_1.default.join(target, "app", "auth", "login", "page.tsx");
|
|
332
|
+
if (node_fs_1.default.existsSync(loginPagePath)) {
|
|
333
|
+
let content = node_fs_1.default.readFileSync(loginPagePath, "utf8");
|
|
334
|
+
// Replace email field with username field
|
|
335
|
+
content = content.replace(/\{\s*construction:\s*\{\s*name:\s*["']email["'],\s*label:\s*["']E-mail["'],\s*placeholder:\s*["']Ex:\s*example@mail\.com["'],\s*validations:\s*["']required\|min:10\|max:50\|email["']\s*\}\s*\}/g, `{\n construction: {\n name: "username",\n label: "Username",\n placeholder: "Ex: joko.gunawan",\n validations: "required|min:3|max:50"\n }\n }`);
|
|
336
|
+
// Remove Create Account link in login page
|
|
337
|
+
content = content.replace(/<p className="mt-4 text-center">Don't have an account yet\? <Link href="\/auth\/register" className="text-primary underline">Create Account<\/Link><\/p>/g, '');
|
|
338
|
+
node_fs_1.default.writeFileSync(loginPagePath, content, "utf8");
|
|
339
|
+
}
|
|
340
|
+
// B. Modify app/auth/edit/page.tsx
|
|
341
|
+
const editPagePath = node_path_1.default.join(target, "app", "auth", "edit", "page.tsx");
|
|
342
|
+
if (node_fs_1.default.existsSync(editPagePath)) {
|
|
343
|
+
let content = node_fs_1.default.readFileSync(editPagePath, "utf8");
|
|
344
|
+
// Replace email field with username field
|
|
345
|
+
content = content.replace(/\{\s*construction:\s*\{\s*name:\s*["']email["'],\s*label:\s*["']E-mail["'],\s*placeholder:\s*["']Ex:\s*example@mail\.com["'],\s*\}\s*\}/g, `{\n construction: {\n name: "username",\n label: "Username",\n placeholder: "Ex: joko.gunawan",\n }\n }`);
|
|
346
|
+
node_fs_1.default.writeFileSync(editPagePath, content, "utf8");
|
|
347
|
+
}
|
|
348
|
+
// C. Modify app/auth/me/page.tsx
|
|
349
|
+
const mePagePath = node_path_1.default.join(target, "app", "auth", "me", "page.tsx");
|
|
350
|
+
if (node_fs_1.default.existsSync(mePagePath)) {
|
|
351
|
+
let content = node_fs_1.default.readFileSync(mePagePath, "utf8");
|
|
352
|
+
// Replace email display with username
|
|
353
|
+
content = content.replace(/<div>\s*<p className="text-xs font-semibold text-light-foreground">\s*Email\s*<\/p>\s*<p>\{user\?\.email\}<\/p>\s*<\/div>/g, `<div>\n <p className="text-xs font-semibold text-light-foreground">\n Username\n </p>\n <p>{user?.username}</p>\n </div>`);
|
|
354
|
+
node_fs_1.default.writeFileSync(mePagePath, content, "utf8");
|
|
355
|
+
}
|
|
356
|
+
// D. Modify app/dashboard/user/page.tsx
|
|
357
|
+
const userPagePath = node_path_1.default.join(target, "app", "dashboard", "user", "page.tsx");
|
|
358
|
+
if (node_fs_1.default.existsSync(userPagePath)) {
|
|
359
|
+
let content = node_fs_1.default.readFileSync(userPagePath, "utf8");
|
|
360
|
+
// Replace table column for email
|
|
361
|
+
content = content.replace(/\{\s*selector:\s*["']email["'],\s*label:\s*["']Email["'],\s*sortable:\s*true,\s*width:\s*["']250px["'],\s*\}/g, `{\n selector: "username",\n label: "Username",\n sortable: true,\n width: "250px",\n }`);
|
|
362
|
+
// Replace detail panel for email
|
|
363
|
+
content = content.replace(/\{\s*label:\s*["']Email["'],\s*item:\s*["']email["'],\s*\}/g, `{\n label: "Username",\n item: "username",\n }`);
|
|
364
|
+
// Replace form field for email
|
|
365
|
+
content = content.replace(/\{\s*construction:\s*\{\s*name:\s*["']email["'],\s*label:\s*["']E-mail["'],\s*placeholder:\s*["']Ex:\s*example@mail\.com["'],\s*validations:\s*\[\s*["']required["']\s*\]\s*,\s*\}\s*,?\s*\}/g, `{\n construction: {\n name: "username",\n label: "Username",\n placeholder: "Ex: joko.gunawan",\n validations: ["required"],\n },\n }`);
|
|
366
|
+
node_fs_1.default.writeFileSync(userPagePath, content, "utf8");
|
|
367
|
+
}
|
|
368
|
+
// E. Delete app/auth/register and app/auth/verify folders (self-registration and verification disabled/removed)
|
|
369
|
+
const registerDir = node_path_1.default.join(target, "app", "auth", "register");
|
|
370
|
+
if (node_fs_1.default.existsSync(registerDir)) {
|
|
371
|
+
node_fs_1.default.rmSync(registerDir, { recursive: true, force: true });
|
|
372
|
+
}
|
|
373
|
+
const verifyDir = node_path_1.default.join(target, "app", "auth", "verify");
|
|
374
|
+
if (node_fs_1.default.existsSync(verifyDir)) {
|
|
375
|
+
node_fs_1.default.rmSync(verifyDir, { recursive: true, force: true });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
319
378
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -30,7 +30,7 @@ class Questioner {
|
|
|
30
30
|
this.rl.close();
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
async function initProject(projectName) {
|
|
33
|
+
async function initProject(projectName, options) {
|
|
34
34
|
const cwd = process.cwd();
|
|
35
35
|
const isCurrentDir = !projectName || projectName === ".";
|
|
36
36
|
const target = isCurrentDir ? cwd : node_path_1.default.resolve(cwd, projectName);
|
|
@@ -62,7 +62,14 @@ async function initProject(projectName) {
|
|
|
62
62
|
let appPwa = false;
|
|
63
63
|
let appTauriDesktop = false;
|
|
64
64
|
let appTauriMobile = false;
|
|
65
|
+
let authType = options?.authType ?? "username";
|
|
66
|
+
const shouldPromptAuth = !options || !options.authType;
|
|
65
67
|
try {
|
|
68
|
+
if (shouldPromptAuth) {
|
|
69
|
+
console.log("\n--- Configure Authentication ---");
|
|
70
|
+
const authChoice = (await q.ask("Choose authentication type (username/email) [default: username]: ", "username")).toLowerCase();
|
|
71
|
+
authType = authChoice === "email" ? "email" : "username";
|
|
72
|
+
}
|
|
66
73
|
console.log("\n--- Configure API (Backend) ---");
|
|
67
74
|
apiRedis = (await q.ask("Do you need Redis? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
68
75
|
apiQueue = (await q.ask("Do you need Queue? (y/N): ", "No")).toLowerCase().startsWith("y");
|
|
@@ -93,6 +100,7 @@ async function initProject(projectName) {
|
|
|
93
100
|
cron: apiCron,
|
|
94
101
|
da: apiDa,
|
|
95
102
|
socket: apiSocket,
|
|
103
|
+
authType: authType,
|
|
96
104
|
});
|
|
97
105
|
console.log("\nCreating App...");
|
|
98
106
|
await (0, create_app_1.createApp)(appDir, {
|
|
@@ -102,6 +110,7 @@ async function initProject(projectName) {
|
|
|
102
110
|
pwa: appPwa,
|
|
103
111
|
tauriDesktop: appTauriDesktop,
|
|
104
112
|
tauriMobile: appTauriMobile,
|
|
113
|
+
authType: authType,
|
|
105
114
|
});
|
|
106
115
|
console.log("\nInstalling AI Agents...");
|
|
107
116
|
try {
|
package/dist/commands/pick.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.UTILITIES = exports.UTILITY_EXPORTS = void 0;
|
|
7
7
|
exports.pickUtility = pickUtility;
|
|
8
|
+
exports.pickComponent = pickComponent;
|
|
8
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
11
|
const fs_1 = require("../utils/fs");
|
|
@@ -38,7 +39,6 @@ function pickUtility(utilityName) {
|
|
|
38
39
|
if (!(0, fs_1.exists)(utilsDir) || !(0, fs_1.exists)(indexPath)) {
|
|
39
40
|
throw new Error("Folder utils or utils/index.ts not found. Make sure you are at the project root.");
|
|
40
41
|
}
|
|
41
|
-
// 1. Tentukan path source dari node_modules dan target di lokal proyek
|
|
42
42
|
const corePackagePath = node_path_1.default.join(projectRoot, "node_modules", "@skalfa", "skalfa-api-core");
|
|
43
43
|
const sourceDir = node_path_1.default.join(corePackagePath, "src", utilityName);
|
|
44
44
|
const targetDir = node_path_1.default.join(utilsDir, utilityName);
|
|
@@ -48,7 +48,6 @@ function pickUtility(utilityName) {
|
|
|
48
48
|
if ((0, fs_1.exists)(targetDir)) {
|
|
49
49
|
throw new Error(`Utility folder "${utilityName}" is already present in your local utils folder.`);
|
|
50
50
|
}
|
|
51
|
-
// 2. Salin folder dari node_modules ke lokal proyek secara rekursif
|
|
52
51
|
console.log(`Copying ${utilityName} folder from @skalfa/skalfa-api-core to utils/ ...`);
|
|
53
52
|
node_fs_1.default.cpSync(sourceDir, targetDir, { recursive: true });
|
|
54
53
|
const filesToDelete = ["CONTRIBUTING.md", "LICENSE"];
|
|
@@ -59,7 +58,6 @@ function pickUtility(utilityName) {
|
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
60
|
console.log(`✓ Copied ${utilityName} folder`);
|
|
62
|
-
// 3. Perbarui utils/index.ts untuk mereferensikan folder lokal
|
|
63
61
|
console.log("Updating utils/index.ts with explicit local export override ...");
|
|
64
62
|
let indexContent = node_fs_1.default.readFileSync(indexPath, "utf8").trim();
|
|
65
63
|
const localExportLine = `export { ${utilitySymbols.join(", ")} } from "./${utilityName}";`;
|
|
@@ -73,3 +71,133 @@ function pickUtility(utilityName) {
|
|
|
73
71
|
}
|
|
74
72
|
console.log(`\nSuccess! You can now customize files under: utils/${utilityName}/`);
|
|
75
73
|
}
|
|
74
|
+
function findComponentFolder(srcDir, componentName) {
|
|
75
|
+
if (!node_fs_1.default.existsSync(srcDir))
|
|
76
|
+
return null;
|
|
77
|
+
const folders = node_fs_1.default.readdirSync(srcDir);
|
|
78
|
+
for (const folder of folders) {
|
|
79
|
+
const folderPath = node_path_1.default.join(srcDir, folder);
|
|
80
|
+
if (!node_fs_1.default.statSync(folderPath).isDirectory())
|
|
81
|
+
continue;
|
|
82
|
+
const files = node_fs_1.default.readdirSync(folderPath);
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
if (!file.endsWith(".tsx") && !file.endsWith(".ts"))
|
|
85
|
+
continue;
|
|
86
|
+
const filePath = node_path_1.default.join(folderPath, file);
|
|
87
|
+
const content = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
88
|
+
// Match the component name or the component name with "Component" suffix (e.g. Button or ButtonComponent)
|
|
89
|
+
const regex = new RegExp(`export\\s+(async\\s+)?(function|const|class|type|interface)\\s+\\b(${componentName}|${componentName}Component)\\b`);
|
|
90
|
+
if (regex.test(content)) {
|
|
91
|
+
return { folderName: folder, fileName: file };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function pickComponent(componentName, newName) {
|
|
98
|
+
const projectRoot = (0, fs_1.findProjectRoot)(process.cwd());
|
|
99
|
+
if (!projectRoot) {
|
|
100
|
+
throw new Error("No package.json found. Run this command inside a Skalfa project.");
|
|
101
|
+
}
|
|
102
|
+
// 1. Locate the component source in @skalfa/skalfa-component
|
|
103
|
+
let componentSrcDir = node_path_1.default.join(projectRoot, "node_modules", "@skalfa", "skalfa-component", "src");
|
|
104
|
+
if (!node_fs_1.default.existsSync(componentSrcDir)) {
|
|
105
|
+
componentSrcDir = node_path_1.default.resolve(projectRoot, "..", "skalfa-component", "src");
|
|
106
|
+
}
|
|
107
|
+
// Clean the componentName if it has "Component" suffix for searching
|
|
108
|
+
const cleanComponentName = componentName.endsWith("Component") ? componentName.replace(/Component$/, "") : componentName;
|
|
109
|
+
const match = findComponentFolder(componentSrcDir, cleanComponentName);
|
|
110
|
+
if (!match) {
|
|
111
|
+
throw new Error(`Component "${componentName}" not found in @skalfa/skalfa-component.`);
|
|
112
|
+
}
|
|
113
|
+
const { folderName, fileName } = match;
|
|
114
|
+
const sourceFolder = node_path_1.default.join(componentSrcDir, folderName);
|
|
115
|
+
const sourceFilePath = node_path_1.default.join(sourceFolder, fileName);
|
|
116
|
+
// Extract base name of the component file (e.g. "Button" from "Button.component.tsx")
|
|
117
|
+
const fileBaseName = fileName.replace(/\.(component)?\.(tsx|ts)$/, "");
|
|
118
|
+
// Find all exported symbols in the file that start with or match the file base name
|
|
119
|
+
const sourceContent = node_fs_1.default.readFileSync(sourceFilePath, "utf8");
|
|
120
|
+
const exportRegex = new RegExp(`export\\s+(async\\s+)?(function|const|class|type|interface)\\s+\\b(${fileBaseName}\\w*)\\b`, "g");
|
|
121
|
+
const exportedTypes = [];
|
|
122
|
+
const exportedValues = [];
|
|
123
|
+
let exportMatch;
|
|
124
|
+
while ((exportMatch = exportRegex.exec(sourceContent)) !== null) {
|
|
125
|
+
const keyword = exportMatch[2];
|
|
126
|
+
const symbol = exportMatch[3];
|
|
127
|
+
if (keyword === "type" || keyword === "interface") {
|
|
128
|
+
exportedTypes.push(symbol);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
exportedValues.push(symbol);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const componentsDir = node_path_1.default.join(projectRoot, "components");
|
|
135
|
+
const indexPath = node_path_1.default.join(componentsDir, "index.ts");
|
|
136
|
+
if (!node_fs_1.default.existsSync(componentsDir)) {
|
|
137
|
+
node_fs_1.default.mkdirSync(componentsDir, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
// Ensure index.ts exists
|
|
140
|
+
if (!node_fs_1.default.existsSync(indexPath)) {
|
|
141
|
+
node_fs_1.default.writeFileSync(indexPath, `export * from "@skalfa/skalfa-component";\n`, "utf8");
|
|
142
|
+
}
|
|
143
|
+
// Determine target names
|
|
144
|
+
const targetFolderName = newName ? newName.charAt(0).toLowerCase() + newName.slice(1) : folderName;
|
|
145
|
+
const targetFolder = node_path_1.default.join(componentsDir, targetFolderName);
|
|
146
|
+
if (node_fs_1.default.existsSync(targetFolder)) {
|
|
147
|
+
throw new Error(`Component folder "${targetFolderName}" already exists in components/.`);
|
|
148
|
+
}
|
|
149
|
+
console.log(`Copying component "${cleanComponentName}" from ${sourceFolder} to components/${targetFolderName} ...`);
|
|
150
|
+
node_fs_1.default.cpSync(sourceFolder, targetFolder, { recursive: true });
|
|
151
|
+
let finalFileBaseName = fileBaseName;
|
|
152
|
+
let finalExportedTypes = [...exportedTypes];
|
|
153
|
+
let finalExportedValues = [...exportedValues];
|
|
154
|
+
// If renaming is requested, perform renaming of files and contents
|
|
155
|
+
if (newName) {
|
|
156
|
+
const cleanNewName = newName.endsWith("Component") ? newName.replace(/Component$/, "") : newName;
|
|
157
|
+
finalFileBaseName = cleanNewName;
|
|
158
|
+
finalExportedTypes = exportedTypes.map(sym => sym.replace(new RegExp(fileBaseName, "g"), cleanNewName));
|
|
159
|
+
finalExportedValues = exportedValues.map(sym => sym.replace(new RegExp(fileBaseName, "g"), cleanNewName));
|
|
160
|
+
const files = node_fs_1.default.readdirSync(targetFolder);
|
|
161
|
+
for (const file of files) {
|
|
162
|
+
const filePath = node_path_1.default.join(targetFolder, file);
|
|
163
|
+
if (node_fs_1.default.statSync(filePath).isDirectory())
|
|
164
|
+
continue;
|
|
165
|
+
// Read file content and replace component names
|
|
166
|
+
let content = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
167
|
+
// Replace fileBaseName (e.g. Button) with cleanNewName (e.g. MyButton)
|
|
168
|
+
const nameRegex = new RegExp(fileBaseName, "g");
|
|
169
|
+
content = content.replace(nameRegex, cleanNewName);
|
|
170
|
+
node_fs_1.default.writeFileSync(filePath, content, "utf8");
|
|
171
|
+
// Rename the file if it contains the original file base name
|
|
172
|
+
if (file.includes(fileBaseName)) {
|
|
173
|
+
const newFile = file.replace(fileBaseName, cleanNewName);
|
|
174
|
+
node_fs_1.default.renameSync(filePath, node_path_1.default.join(targetFolder, newFile));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Update components/index.ts to export the local component explicitly
|
|
179
|
+
let indexContent = node_fs_1.default.readFileSync(indexPath, "utf8");
|
|
180
|
+
// Find the exact filename of the main component file inside the target folder
|
|
181
|
+
const targetFiles = node_fs_1.default.readdirSync(targetFolder);
|
|
182
|
+
const componentFile = targetFiles.find(f => f.includes(finalFileBaseName) && (f.endsWith(".tsx") || f.endsWith(".ts")));
|
|
183
|
+
if (componentFile) {
|
|
184
|
+
const componentBaseFile = componentFile.replace(/\.(tsx|ts)$/, "");
|
|
185
|
+
const valueExport = finalExportedValues.length > 0
|
|
186
|
+
? `export { ${finalExportedValues.join(", ")} } from "./${targetFolderName}/${componentBaseFile}";`
|
|
187
|
+
: "";
|
|
188
|
+
const typeExport = finalExportedTypes.length > 0
|
|
189
|
+
? `export type { ${finalExportedTypes.join(", ")} } from "./${targetFolderName}/${componentBaseFile}";`
|
|
190
|
+
: "";
|
|
191
|
+
const localExportLine = [valueExport, typeExport].filter(Boolean).join("\n");
|
|
192
|
+
if (!indexContent.includes(`./${targetFolderName}/`)) {
|
|
193
|
+
indexContent = indexContent.trim() + `\n${localExportLine}\n`;
|
|
194
|
+
node_fs_1.default.writeFileSync(indexPath, indexContent, "utf8");
|
|
195
|
+
console.log(`✓ Updated components/index.ts with local export for: ${[...finalExportedValues, ...finalExportedTypes].join(", ")}`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
console.log(`⚠️ Info: Local export for "${targetFolderName}" already exists in components/index.ts`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const finalSymbols = [...finalExportedValues, ...finalExportedTypes];
|
|
202
|
+
console.log(`\nSuccess! Component "${finalSymbols.find((s) => !s.endsWith("Props")) || finalSymbols[0]}" is now available locally at components/${targetFolderName}/`);
|
|
203
|
+
}
|
package/package.json
CHANGED