scaffoldrite 1.0.0

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/parser.js ADDED
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseStructure = parseStructure;
4
+ const constraints_1 = require("./constraints");
5
+ const INVALID_NAME_REGEX = /[^a-zA-Z0-9._-]/;
6
+ function validateName(name, line) {
7
+ if (name === "__root__") {
8
+ throw new Error(`[Line ${line}] Reserved name "__root__" is not allowed`);
9
+ }
10
+ if (INVALID_NAME_REGEX.test(name)) {
11
+ throw new Error(`[Line ${line}] Invalid characters in name: "${name}"`);
12
+ }
13
+ }
14
+ function parseStructure(input) {
15
+ const lines = input.split("\n");
16
+ const root = { type: "folder", name: "__root__", children: [] };
17
+ const stack = [root];
18
+ let constraints = [];
19
+ let inConstraints = false;
20
+ let braceDepth = 0;
21
+ for (let i = 0; i < lines.length; i++) {
22
+ const lineNumber = i + 1;
23
+ const raw = lines[i];
24
+ const line = raw.trim();
25
+ const codeLine = line.split("//")[0].trim();
26
+ if (codeLine.length === 0)
27
+ continue;
28
+ if (line.length === 0)
29
+ continue;
30
+ // Start constraints block
31
+ if (line === "constraints {") {
32
+ inConstraints = true;
33
+ braceDepth = 1;
34
+ continue;
35
+ }
36
+ // If we are inside constraints block
37
+ if (inConstraints) {
38
+ if (line.includes("{"))
39
+ braceDepth++;
40
+ if (line.includes("}"))
41
+ braceDepth--;
42
+ // If braceDepth becomes 0, constraints ended
43
+ if (braceDepth === 0) {
44
+ inConstraints = false;
45
+ continue;
46
+ }
47
+ constraints.push(line);
48
+ continue;
49
+ }
50
+ // Folder logic
51
+ if (line.startsWith("folder ")) {
52
+ const match = line.match(/^folder\s+(.+)\s+\{$/);
53
+ if (!match) {
54
+ throw new Error(`[Line ${lineNumber}] Invalid folder syntax: "${line}"`);
55
+ }
56
+ const name = match[1];
57
+ validateName(name, lineNumber);
58
+ const parent = stack[stack.length - 1];
59
+ if (parent.children.some(c => c.type === "folder" && c.name === name)) {
60
+ throw new Error(`[Line ${lineNumber}] Duplicate folder name "${name}" in the same scope`);
61
+ }
62
+ const folder = { type: "folder", name, children: [] };
63
+ parent.children.push(folder);
64
+ stack.push(folder);
65
+ continue;
66
+ }
67
+ // File logic
68
+ if (line.startsWith("file ")) {
69
+ const match = line.match(/^file\s+(.+)$/);
70
+ if (!match) {
71
+ throw new Error(`[Line ${lineNumber}] Invalid file syntax: "${line}"`);
72
+ }
73
+ const name = match[1];
74
+ validateName(name, lineNumber);
75
+ const parent = stack[stack.length - 1];
76
+ if (parent.children.some(c => c.type === "file" && c.name === name)) {
77
+ throw new Error(`[Line ${lineNumber}] Duplicate file name "${name}" in the same scope`);
78
+ }
79
+ const file = { type: "file", name };
80
+ parent.children.push(file);
81
+ continue;
82
+ }
83
+ // Close folder
84
+ if (line === "}") {
85
+ if (stack.length === 1) {
86
+ throw new Error(`[Line ${lineNumber}] Unexpected "}"`);
87
+ }
88
+ stack.pop();
89
+ continue;
90
+ }
91
+ throw new Error(`[Line ${lineNumber}] Unknown statement: "${line}"`);
92
+ }
93
+ if (stack.length !== 1) {
94
+ throw new Error(`Unclosed folder block`);
95
+ }
96
+ return {
97
+ root,
98
+ constraints: (0, constraints_1.parseConstraints)(constraints.join("\n")),
99
+ rawConstraints: constraints
100
+ };
101
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findNode = findNode;
4
+ exports.addNode = addNode;
5
+ exports.deleteNode = deleteNode;
6
+ exports.renameNode = renameNode;
7
+ function findNode(root, pathStr) {
8
+ const parts = pathStr.split("/").filter(Boolean);
9
+ let current = root;
10
+ for (let i = 0; i < parts.length; i++) {
11
+ const part = parts[i];
12
+ const child = current.children.find(c => c.name === part);
13
+ if (!child)
14
+ return null;
15
+ if (i === parts.length - 1)
16
+ return child;
17
+ if (child.type !== "folder")
18
+ return null;
19
+ current = child;
20
+ }
21
+ return null;
22
+ }
23
+ function addNode(root, pathStr, type, options = {}) {
24
+ const parts = pathStr.split("/").filter(Boolean);
25
+ const name = parts.pop();
26
+ if (!name) {
27
+ throw new Error("Invalid path: path is empty. Example: src/components/Button.ts");
28
+ }
29
+ let current = root;
30
+ for (const part of parts) {
31
+ let child = current.children.find(c => c.name === part && c.type === "folder");
32
+ if (!child) {
33
+ child = { type: "folder", name: part, children: [] };
34
+ current.children.push(child);
35
+ }
36
+ current = child;
37
+ }
38
+ const exists = current.children.some(c => c.name === name);
39
+ if (exists) {
40
+ if (options.ifNotExists)
41
+ return; // do nothing
42
+ if (!options.force) {
43
+ throw new Error(`Cannot create ${type}: '${pathStr}' already exists in the structure.`);
44
+ }
45
+ // force = true -> replace existing node
46
+ const idx = current.children.findIndex(c => c.name === name);
47
+ current.children.splice(idx, 1);
48
+ }
49
+ const newNode = type === "folder"
50
+ ? { type: "folder", name, children: [] }
51
+ : { type: "file", name };
52
+ current.children.push(newNode);
53
+ return current;
54
+ }
55
+ function deleteNode(root, pathStr) {
56
+ const parts = pathStr.split("/").filter(Boolean);
57
+ const name = parts.pop();
58
+ if (!name) {
59
+ throw new Error("Invalid path: path is empty. Example: src/components/Button.ts");
60
+ }
61
+ let current = root;
62
+ for (const part of parts) {
63
+ const child = current.children.find(c => c.name === part && c.type === "folder");
64
+ if (!child) {
65
+ throw new Error(`Path not found: '${parts.join("/")}' does not exist in the structure.`);
66
+ }
67
+ current = child;
68
+ }
69
+ const index = current.children.findIndex(c => c.name === name);
70
+ if (index === -1) {
71
+ throw new Error(`Node not found: '${pathStr}' does not exist in the structure.`);
72
+ }
73
+ current.children.splice(index, 1);
74
+ }
75
+ function renameNode(root, pathStr, newName) {
76
+ const node = findNode(root, pathStr);
77
+ if (!node) {
78
+ throw new Error(`Node not found: '${pathStr}' does not exist.`);
79
+ }
80
+ if (!newName || newName.trim().length === 0) {
81
+ throw new Error("Invalid new name: name cannot be empty.");
82
+ }
83
+ node.name = newName;
84
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_IGNORES = void 0;
7
+ exports.loadIgnoreList = loadIgnoreList;
8
+ exports.validateFS = validateFS;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const ignoreFilePath = "./.scaffoldignore";
12
+ exports.DEFAULT_IGNORES = [
13
+ "node_modules",
14
+ ".git",
15
+ ".next",
16
+ "dist",
17
+ "build",
18
+ "coverage",
19
+ ".turbo",
20
+ ];
21
+ function loadIgnoreList(filePath) {
22
+ if (!fs_1.default.existsSync(filePath))
23
+ return [];
24
+ const content = fs_1.default.readFileSync(filePath, "utf-8");
25
+ return content
26
+ .split("\n")
27
+ .map((x) => x.trim())
28
+ .map((x) => x.split("#")[0].trim())
29
+ .filter(Boolean);
30
+ }
31
+ function getIgnoreList() {
32
+ return fs_1.default.existsSync(ignoreFilePath)
33
+ ? loadIgnoreList(ignoreFilePath)
34
+ : exports.DEFAULT_IGNORES;
35
+ }
36
+ function isIgnored(itemName) {
37
+ return getIgnoreList().includes(itemName);
38
+ }
39
+ function validateFS(root, dir, allowExtra = false, allowExtraPaths = []) {
40
+ if (!fs_1.default.existsSync(dir)) {
41
+ throw new Error(`Folder does not exist: ${dir}`);
42
+ }
43
+ const actualItems = fs_1.default.readdirSync(dir);
44
+ // Check missing items in filesystem
45
+ for (const child of root.children) {
46
+ // skip ignored files/folders
47
+ if (isIgnored(child.name))
48
+ continue;
49
+ const expectedPath = path_1.default.join(dir, child.name);
50
+ if (!fs_1.default.existsSync(expectedPath)) {
51
+ throw new Error(`Missing in filesystem: ${expectedPath}`);
52
+ }
53
+ if (child.type === "folder") {
54
+ if (!fs_1.default.statSync(expectedPath).isDirectory()) {
55
+ throw new Error(`Expected folder but found file: ${expectedPath}`);
56
+ }
57
+ validateFS(child, expectedPath, allowExtra, allowExtraPaths);
58
+ }
59
+ else {
60
+ if (!fs_1.default.statSync(expectedPath).isFile()) {
61
+ throw new Error(`Expected file but found folder: ${expectedPath}`);
62
+ }
63
+ }
64
+ }
65
+ // Check extra items in filesystem not in .sr
66
+ for (const item of actualItems) {
67
+ // skip ignored files/folders
68
+ if (isIgnored(item))
69
+ continue;
70
+ const existsInSr = root.children.some((c) => c.name === item);
71
+ if (!existsInSr) {
72
+ const extraPath = path_1.default.join(dir, item);
73
+ const allowedExplicitly = allowExtraPaths.some((p) => {
74
+ const normalized = path_1.default.normalize(p);
75
+ const extraRel = path_1.default.relative(dir, extraPath);
76
+ const extraBasename = path_1.default.basename(extraPath);
77
+ return (extraBasename === normalized ||
78
+ extraRel === normalized ||
79
+ extraRel.endsWith(normalized));
80
+ });
81
+ if (allowExtra || allowedExplicitly)
82
+ continue;
83
+ throw new Error(`Extra file/folder found in filesystem: ${extraPath}`);
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateConstraints = validateConstraints;
4
+ function normalizePath(path) {
5
+ if (!path || path.trim() === "")
6
+ return "__root__";
7
+ return path.trim().replace(/^\/+/, "").replace(/\/+$/, "");
8
+ }
9
+ function findNodeByPath(root, path) {
10
+ const normalizedPath = normalizePath(path);
11
+ const parts = normalizedPath.split("/").filter(Boolean);
12
+ let current = root;
13
+ if (parts[0] === root.name) {
14
+ parts.shift();
15
+ }
16
+ for (const part of parts) {
17
+ if (current.type !== "folder")
18
+ return null;
19
+ const next = current.children.find((c) => c.name === part);
20
+ if (!next)
21
+ return null;
22
+ current = next;
23
+ }
24
+ return current;
25
+ }
26
+ function countFiles(folder) {
27
+ return folder.children.filter((c) => c.type === "file").length;
28
+ }
29
+ function countFolders(folder) {
30
+ return folder.children.filter((c) => c.type === "folder").length;
31
+ }
32
+ function countFilesRecursive(folder, ext) {
33
+ let count = 0;
34
+ for (const child of folder.children) {
35
+ if (child.type === "file") {
36
+ if (!ext || child.name.endsWith(ext))
37
+ count++;
38
+ }
39
+ if (child.type === "folder")
40
+ count += countFilesRecursive(child, ext);
41
+ }
42
+ return count;
43
+ }
44
+ function countFoldersRecursive(folder) {
45
+ let count = 0;
46
+ for (const child of folder.children) {
47
+ if (child.type === "folder")
48
+ count++;
49
+ if (child.type === "folder")
50
+ count += countFoldersRecursive(child);
51
+ }
52
+ return count;
53
+ }
54
+ function maxDepth(folder, current = 0) {
55
+ let depth = current;
56
+ for (const child of folder.children) {
57
+ if (child.type === "folder") {
58
+ depth = Math.max(depth, maxDepth(child, current + 1));
59
+ }
60
+ }
61
+ return depth;
62
+ }
63
+ function getFoldersByScope(root, path, scope) {
64
+ const folder = findNodeByPath(root, path);
65
+ if (!folder || folder.type !== "folder")
66
+ return [];
67
+ const folders = [];
68
+ if (scope === "*") {
69
+ // only direct child folders
70
+ for (const child of folder.children) {
71
+ if (child.type === "folder")
72
+ folders.push(child);
73
+ }
74
+ }
75
+ if (scope === "**") {
76
+ // recursively collect all folders
77
+ function traverse(f) {
78
+ for (const child of f.children) {
79
+ if (child.type === "folder") {
80
+ folders.push(child);
81
+ traverse(child);
82
+ }
83
+ }
84
+ }
85
+ traverse(folder);
86
+ }
87
+ return folders;
88
+ }
89
+ function validateConstraints(root, constraints) {
90
+ for (const c of constraints) {
91
+ if ((c.type !== "eachFolderMustContain" &&
92
+ c.type !== "eachFolderMustContainFile" &&
93
+ c.type !== "eachFolderMustContainFolder" &&
94
+ c.type !== "eachFolderMustHaveExt") &&
95
+ (!c.path || c.path.trim() === "")) {
96
+ throw new Error(`Constraint failed: path missing for ${c.type}`);
97
+ }
98
+ if (c.type === "require") {
99
+ const node = findNodeByPath(root, c.path);
100
+ if (!node)
101
+ throw new Error(`Constraint failed: required path not found: ${c.path}`);
102
+ }
103
+ if (c.type === "forbid") {
104
+ const node = findNodeByPath(root, c.path);
105
+ if (node)
106
+ throw new Error(`Constraint failed: forbidden path exists: ${c.path}`);
107
+ }
108
+ if (c.type === "maxFiles") {
109
+ const folder = findNodeByPath(root, c.path);
110
+ if (!folder || folder.type !== "folder")
111
+ continue;
112
+ const fileCount = countFiles(folder);
113
+ if (fileCount > c.value)
114
+ throw new Error(`Constraint failed: ${c.path} has more than ${c.value} files`);
115
+ }
116
+ if (c.type === "maxFilesRecursive") {
117
+ const folder = findNodeByPath(root, c.path);
118
+ if (!folder || folder.type !== "folder")
119
+ continue;
120
+ const fileCount = countFilesRecursive(folder);
121
+ if (fileCount > c.value)
122
+ throw new Error(`Constraint failed: ${c.path} has more than ${c.value} files recursively`);
123
+ }
124
+ if (c.type === "maxFilesByExt") {
125
+ const folder = findNodeByPath(root, c.path);
126
+ if (!folder || folder.type !== "folder")
127
+ continue;
128
+ const fileCount = folder.children
129
+ .filter((x) => x.type === "file")
130
+ .filter((f) => f.name.endsWith(c.ext)).length;
131
+ if (fileCount > c.value)
132
+ throw new Error(`Constraint failed: ${c.path} has more than ${c.value} files of ${c.ext}`);
133
+ }
134
+ if (c.type === "maxFilesByExtRecursive") {
135
+ const folder = findNodeByPath(root, c.path);
136
+ if (!folder || folder.type !== "folder")
137
+ continue;
138
+ const extCount = countFilesRecursive(folder, c.ext);
139
+ if (extCount > c.value)
140
+ throw new Error(`Constraint failed: ${c.path} has more than ${c.value} files of ${c.ext} recursively`);
141
+ }
142
+ if (c.type === "maxFolders") {
143
+ const folder = findNodeByPath(root, c.path);
144
+ if (!folder || folder.type !== "folder")
145
+ continue;
146
+ const folderCount = countFolders(folder);
147
+ if (folderCount > c.value)
148
+ throw new Error(`Constraint failed: ${c.path} has more than ${c.value} folders`);
149
+ }
150
+ if (c.type === "maxFoldersRecursive") {
151
+ const folder = findNodeByPath(root, c.path);
152
+ if (!folder || folder.type !== "folder")
153
+ continue;
154
+ const folderCount = countFoldersRecursive(folder);
155
+ if (folderCount > c.value)
156
+ throw new Error(`Constraint failed: ${c.path} has more than ${c.value} folders recursively`);
157
+ }
158
+ if (c.type === "minFiles") {
159
+ const folder = findNodeByPath(root, c.path);
160
+ if (!folder || folder.type !== "folder")
161
+ continue;
162
+ const fileCount = countFiles(folder);
163
+ if (fileCount < c.value)
164
+ throw new Error(`Constraint failed: ${c.path} has less than ${c.value} files`);
165
+ }
166
+ if (c.type === "minFolders") {
167
+ const folder = findNodeByPath(root, c.path);
168
+ if (!folder || folder.type !== "folder")
169
+ continue;
170
+ const folderCount = countFolders(folder);
171
+ if (folderCount < c.value)
172
+ throw new Error(`Constraint failed: ${c.path} has less than ${c.value} folders`);
173
+ }
174
+ if (c.type === "mustContain") {
175
+ const folder = findNodeByPath(root, c.path);
176
+ if (!folder || folder.type !== "folder")
177
+ continue;
178
+ const exists = folder.children.some((x) => x.name === c.value);
179
+ if (!exists)
180
+ throw new Error(`Constraint failed: ${c.path} must contain ${c.value}`);
181
+ }
182
+ if (c.type === "fileNameRegex") {
183
+ const folder = findNodeByPath(root, c.path);
184
+ if (!folder || folder.type !== "folder")
185
+ continue;
186
+ const regex = new RegExp(c.regex);
187
+ for (const child of folder.children) {
188
+ if (child.type === "file" && !regex.test(child.name)) {
189
+ throw new Error(`Constraint failed: ${child.name} in ${c.path} does not match regex`);
190
+ }
191
+ }
192
+ }
193
+ if (c.type === "maxDepth") {
194
+ const folder = findNodeByPath(root, c.path);
195
+ if (!folder || folder.type !== "folder")
196
+ continue;
197
+ const depth = maxDepth(folder);
198
+ if (depth > c.value)
199
+ throw new Error(`Constraint failed: ${c.path} exceeds max depth of ${c.value}`);
200
+ }
201
+ if (c.type === "mustHaveFile") {
202
+ const folder = findNodeByPath(root, c.path);
203
+ if (!folder || folder.type !== "folder")
204
+ continue;
205
+ const exists = folder.children.some((x) => x.type === "file" && x.name === c.value);
206
+ if (!exists)
207
+ throw new Error(`Constraint failed: ${c.path} must have file ${c.value}`);
208
+ }
209
+ // ⭐ NEW RULES ⭐
210
+ if (c.type === "eachFolderMustContain") {
211
+ const folders = getFoldersByScope(root, c.path, c.scope);
212
+ for (const folder of folders) {
213
+ const exists = folder.children.some((x) => x.name === c.value);
214
+ if (!exists) {
215
+ throw new Error(`Constraint failed: ${folder.name} must contain ${c.value}`);
216
+ }
217
+ }
218
+ }
219
+ if (c.type === "eachFolderMustContainFile") {
220
+ const folders = getFoldersByScope(root, c.path, c.scope);
221
+ for (const folder of folders) {
222
+ const exists = folder.children.some((x) => x.type === "file" && x.name === c.value);
223
+ if (!exists) {
224
+ throw new Error(`Constraint failed: ${folder.name} must contain file ${c.value}`);
225
+ }
226
+ }
227
+ }
228
+ if (c.type === "eachFolderMustContainFolder") {
229
+ const folders = getFoldersByScope(root, c.path, c.scope);
230
+ for (const folder of folders) {
231
+ const exists = folder.children.some((x) => x.type === "folder" && x.name === c.value);
232
+ if (!exists) {
233
+ throw new Error(`Constraint failed: ${folder.name} must contain folder ${c.value}`);
234
+ }
235
+ }
236
+ }
237
+ if (c.type === "eachFolderMustHaveExt") {
238
+ const folders = getFoldersByScope(root, c.path, c.scope);
239
+ for (const folder of folders) {
240
+ const exists = folder.children.some((x) => x.type === "file" && x.name.endsWith(c.ext));
241
+ if (!exists) {
242
+ throw new Error(`Constraint failed: ${folder.name} must contain a file with extension ${c.ext}`);
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.visit = visit;
4
+ function visit(node, visitor, currentPath = "") {
5
+ // 👇 ignore virtual root in path
6
+ const isVirtualRoot = node.name === "__root__";
7
+ const nodePath = isVirtualRoot
8
+ ? currentPath
9
+ : currentPath
10
+ ? `${currentPath}/${node.name}`
11
+ : node.name;
12
+ if (!isVirtualRoot) {
13
+ visitor.folder?.(node, nodePath);
14
+ }
15
+ for (const child of node.children) {
16
+ if (child.type === "folder") {
17
+ visit(child, visitor, nodePath);
18
+ }
19
+ else {
20
+ visitor.file?.(child, `${nodePath}/${child.name}`);
21
+ }
22
+ }
23
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "scaffoldrite",
3
+ "version": "1.0.0",
4
+ "description": "A project structure validator and generator CLI tool.",
5
+ "author": "Isaac Anasonye",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/Isaacprogi/scaffoldrite#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/Isaacprogi/scaffoldrite.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/Isaacprogi/scaffoldrite/issues"
14
+ },
15
+ "keywords": [
16
+ "cli",
17
+ "scaffold",
18
+ "project-structure",
19
+ "validator",
20
+ "generator",
21
+ "developer-tools",
22
+ "devtools",
23
+ "node"
24
+ ],
25
+ "type": "commonjs",
26
+ "bin": {
27
+ "scaffoldrite": "./dist/cli.js"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "scripts": {
33
+ "dev": "tsx src/cli.ts",
34
+ "build": "tsc",
35
+ "start": "node dist/cli.js",
36
+ "type-check": "tsc --noEmit",
37
+ "validate": "npm run type-check && npm run build",
38
+ "test": "jest"
39
+ },
40
+ "devDependencies": {
41
+ "@types/jest": "^30.0.0",
42
+ "@types/node": "^25.0.9",
43
+ "jest": "^30.2.0",
44
+ "ts-jest": "^29.4.6",
45
+ "tsx": "^4.21.0",
46
+ "typescript": "^5.9.3"
47
+ },
48
+ "engines": {
49
+ "node": ">=17"
50
+ }
51
+ }