@skalfa/skalfa-cli 1.0.11 → 1.0.12

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.
@@ -89,10 +89,21 @@ program
89
89
  });
90
90
  program
91
91
  .command("pick")
92
- .description("Eject/copy a core utility from @skalfa/skalfa-api-core into your local utils folder for customization.")
93
- .argument("<utility>", `utility name: ${pick_1.UTILITIES.join(", ")}`)
94
- .action(async (utility) => {
95
- await runCommand(() => (0, pick_1.pickUtility)(utility));
92
+ .description("Eject/copy a core utility or component into your local project for customization.")
93
+ .argument("<name>", "utility or component name")
94
+ .argument("[newName]", "new name for the component (optional, component only)")
95
+ .action(async (name, newName) => {
96
+ await runCommand(() => {
97
+ if (pick_1.UTILITIES.includes(name)) {
98
+ if (newName) {
99
+ throw new Error("Renaming is only supported for components, not utilities.");
100
+ }
101
+ (0, pick_1.pickUtility)(name);
102
+ }
103
+ else {
104
+ (0, pick_1.pickComponent)(name, newName);
105
+ }
106
+ });
96
107
  });
97
108
  program
98
109
  .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...");
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skalfa/skalfa-cli",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Command Line Interface tool for scaffolding Skalfa projects, managing extensions, and ejecting core utilities.",
5
5
  "main": "dist/bin/skalfa.js",
6
6
  "bin": {