@timeax/scaffold 0.0.1 → 0.0.3
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/cli.cjs +115 -14
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +115 -14
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +91 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -1
- package/dist/index.d.ts +84 -1
- package/dist/index.mjs +88 -13
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
- package/readme.md +184 -46
- package/scripts/postpublish.mjs +72 -0
- package/scripts/prepublish.mjs +95 -0
- package/src/cli/main.ts +38 -0
- package/src/core/config-loader.ts +4 -3
- package/src/core/init-scaffold.ts +8 -3
- package/src/core/scan-structure.ts +71 -0
- package/src/core/structure-txt.ts +88 -15
- package/src/index.ts +3 -2
- package/src/schema/config.ts +10 -1
- package/src/schema/index.ts +1 -0
|
@@ -211,4 +211,75 @@ export async function writeScannedStructuresFromConfig(
|
|
|
211
211
|
`Wrote structure for group "${result.groupName}" to ${result.structureFilePath}`,
|
|
212
212
|
);
|
|
213
213
|
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
export interface EnsureStructuresResult {
|
|
219
|
+
created: string[];
|
|
220
|
+
existing: string[];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Ensure all structure files declared in the config exist.
|
|
225
|
+
*
|
|
226
|
+
* - Grouped mode: one file per group (group.structureFile || `${group.name}.txt`)
|
|
227
|
+
* - Single-root mode: config.structureFile || "structure.txt"
|
|
228
|
+
*
|
|
229
|
+
* Existing files are left untouched. Only missing files are created with
|
|
230
|
+
* a small header comment.
|
|
231
|
+
*/
|
|
232
|
+
export async function ensureStructureFilesFromConfig(
|
|
233
|
+
cwd: string,
|
|
234
|
+
options: { scaffoldDirOverride?: string } = {},
|
|
235
|
+
): Promise<EnsureStructuresResult> {
|
|
236
|
+
const { config, scaffoldDir } = await loadScaffoldConfig(cwd, {
|
|
237
|
+
scaffoldDir: options.scaffoldDirOverride,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
ensureDirSync(scaffoldDir);
|
|
241
|
+
|
|
242
|
+
const created: string[] = [];
|
|
243
|
+
const existing: string[] = [];
|
|
244
|
+
|
|
245
|
+
const seen = new Set<string>();
|
|
246
|
+
|
|
247
|
+
const ensureFile = (fileName: string) => {
|
|
248
|
+
if (!fileName) return;
|
|
249
|
+
|
|
250
|
+
const filePath = path.join(scaffoldDir, fileName);
|
|
251
|
+
const key = path.resolve(filePath);
|
|
252
|
+
|
|
253
|
+
if (seen.has(key)) return;
|
|
254
|
+
seen.add(key);
|
|
255
|
+
|
|
256
|
+
if (fs.existsSync(filePath)) {
|
|
257
|
+
existing.push(filePath);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const header =
|
|
262
|
+
`# ${fileName}\n` +
|
|
263
|
+
`# Structure file for @timeax/scaffold\n` +
|
|
264
|
+
`# Define your desired folders/files here.\n`;
|
|
265
|
+
|
|
266
|
+
fs.writeFileSync(filePath, header, 'utf8');
|
|
267
|
+
created.push(filePath);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (config.groups && config.groups.length > 0) {
|
|
271
|
+
for (const group of config.groups) {
|
|
272
|
+
const fileName = group.structureFile ?? `${group.name}.txt`;
|
|
273
|
+
ensureFile(fileName);
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
const fileName = config.structureFile ?? 'structure.txt';
|
|
277
|
+
ensureFile(fileName);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
logger.debug(
|
|
281
|
+
`ensureStructureFilesFromConfig: created=${created.length}, existing=${existing.length}`,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
return { created, existing };
|
|
214
285
|
}
|
|
@@ -12,6 +12,55 @@ interface ParsedLine {
|
|
|
12
12
|
exclude?: string[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Strip inline comments from a content segment.
|
|
17
|
+
*
|
|
18
|
+
* Supports:
|
|
19
|
+
* - "index.ts # comment"
|
|
20
|
+
* - "index.ts // comment"
|
|
21
|
+
*
|
|
22
|
+
* Rules:
|
|
23
|
+
* - We assume leading indentation has already been removed.
|
|
24
|
+
* - Leading '#' or '//' (full-line comments) are handled BEFORE this function.
|
|
25
|
+
* - A comment starts at the first '#' or '//' that is
|
|
26
|
+
* preceded by whitespace (space or tab).
|
|
27
|
+
*/
|
|
28
|
+
function stripInlineComment(content: string): string {
|
|
29
|
+
let cutIndex = -1;
|
|
30
|
+
const len = content.length;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < len; i++) {
|
|
33
|
+
const ch = content[i];
|
|
34
|
+
const prev = i > 0 ? content[i - 1] : '';
|
|
35
|
+
|
|
36
|
+
// Inline "# ..."
|
|
37
|
+
if (ch === '#') {
|
|
38
|
+
if (i === 0) continue; // full-line handled earlier
|
|
39
|
+
if (prev === ' ' || prev === '\t') {
|
|
40
|
+
cutIndex = i;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Inline "// ..."
|
|
46
|
+
if (
|
|
47
|
+
ch === '/' &&
|
|
48
|
+
i + 1 < len &&
|
|
49
|
+
content[i + 1] === '/' &&
|
|
50
|
+
(prev === ' ' || prev === '\t')
|
|
51
|
+
) {
|
|
52
|
+
cutIndex = i;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (cutIndex === -1) {
|
|
58
|
+
return content.trimEnd();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return content.slice(0, cutIndex).trimEnd();
|
|
62
|
+
}
|
|
63
|
+
|
|
15
64
|
/**
|
|
16
65
|
* Parse a single non-empty, non-comment line into a ParsedLine.
|
|
17
66
|
* Supports inline annotations:
|
|
@@ -20,16 +69,39 @@ interface ParsedLine {
|
|
|
20
69
|
* - @exclude:pattern,pattern2
|
|
21
70
|
*/
|
|
22
71
|
function parseLine(line: string, lineNo: number): ParsedLine | null {
|
|
23
|
-
const match = line.match(/^(\s*)(
|
|
72
|
+
const match = line.match(/^(\s*)(.*)$/);
|
|
24
73
|
if (!match) return null;
|
|
25
74
|
|
|
26
75
|
const indentSpaces = match[1].length;
|
|
27
|
-
|
|
28
|
-
|
|
76
|
+
let rest = match[2];
|
|
77
|
+
|
|
78
|
+
// If line (after indent) is empty, skip
|
|
79
|
+
if (!rest.trim()) return null;
|
|
80
|
+
|
|
81
|
+
// Full-line comments after indent
|
|
82
|
+
const trimmedRest = rest.trimStart();
|
|
83
|
+
if (trimmedRest.startsWith('#') || trimmedRest.startsWith('//')) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Strip inline comments (# or //) before parsing tokens
|
|
88
|
+
const stripped = stripInlineComment(rest);
|
|
89
|
+
const trimmed = stripped.trim();
|
|
90
|
+
if (!trimmed) return null;
|
|
91
|
+
|
|
92
|
+
const parts = trimmed.split(/\s+/);
|
|
93
|
+
if (!parts.length) return null;
|
|
29
94
|
|
|
30
|
-
const parts = rest.split(/\s+/);
|
|
31
95
|
const pathToken = parts[0];
|
|
32
96
|
|
|
97
|
+
// 🚫 Reserve ":" for annotations only – paths may not contain it.
|
|
98
|
+
if (pathToken.includes(':')) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`structure.txt: ":" is reserved for annotations (@stub:, @include:, etc). ` +
|
|
101
|
+
`Invalid path "${pathToken}" on line ${lineNo}.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
33
105
|
let stub: string | undefined;
|
|
34
106
|
const include: string[] = [];
|
|
35
107
|
const exclude: string[] = [];
|
|
@@ -74,14 +146,17 @@ function parseLine(line: string, lineNo: number): ParsedLine | null {
|
|
|
74
146
|
* Convert a structure.txt content into a nested StructureEntry[].
|
|
75
147
|
*
|
|
76
148
|
* Rules:
|
|
77
|
-
* - Indentation is **
|
|
78
|
-
* - Indent must be a multiple of
|
|
149
|
+
* - Indentation is **indentStep** spaces per level (default: 2).
|
|
150
|
+
* - Indent must be a multiple of indentStep.
|
|
79
151
|
* - You cannot "skip" levels (no jumping from level 0 to 2 directly).
|
|
80
152
|
* - **Only directories can have children**:
|
|
81
153
|
* - If you indent under a file, an error is thrown.
|
|
82
154
|
* - Folders must end with "/" in the txt; paths are normalized to POSIX.
|
|
83
155
|
*/
|
|
84
|
-
export function parseStructureText(
|
|
156
|
+
export function parseStructureText(
|
|
157
|
+
text: string,
|
|
158
|
+
indentStep = 2,
|
|
159
|
+
): StructureEntry[] {
|
|
85
160
|
const lines = text.split(/\r?\n/);
|
|
86
161
|
const parsed: ParsedLine[] = [];
|
|
87
162
|
|
|
@@ -100,23 +175,22 @@ export function parseStructureText(text: string): StructureEntry[] {
|
|
|
100
175
|
};
|
|
101
176
|
|
|
102
177
|
const stack: StackItem[] = [];
|
|
103
|
-
const INDENT_STEP = 2;
|
|
104
178
|
|
|
105
179
|
for (const p of parsed) {
|
|
106
180
|
const { indentSpaces, lineNo } = p;
|
|
107
181
|
|
|
108
|
-
if (indentSpaces %
|
|
182
|
+
if (indentSpaces % indentStep !== 0) {
|
|
109
183
|
throw new Error(
|
|
110
184
|
`structure.txt: Invalid indent on line ${lineNo}. ` +
|
|
111
|
-
`Indent must be multiples of ${
|
|
185
|
+
`Indent must be multiples of ${indentStep} spaces.`,
|
|
112
186
|
);
|
|
113
187
|
}
|
|
114
188
|
|
|
115
|
-
const level = indentSpaces /
|
|
189
|
+
const level = indentSpaces / indentStep;
|
|
116
190
|
|
|
117
191
|
// Determine parent level and enforce no skipping
|
|
118
192
|
if (level > stack.length) {
|
|
119
|
-
// e.g. current stack depth 1, but line level=
|
|
193
|
+
// e.g. current stack depth 1, but line level=3 is invalid
|
|
120
194
|
if (level !== stack.length + 1) {
|
|
121
195
|
throw new Error(
|
|
122
196
|
`structure.txt: Invalid indentation on line ${lineNo}. ` +
|
|
@@ -146,7 +220,6 @@ export function parseStructureText(text: string): StructureEntry[] {
|
|
|
146
220
|
const clean = p.rawPath.replace(/\/$/, '');
|
|
147
221
|
const basePath = toPosixPath(clean);
|
|
148
222
|
|
|
149
|
-
// Determine parent based on level
|
|
150
223
|
// Pop stack until we are at the correct depth
|
|
151
224
|
while (stack.length > level) {
|
|
152
225
|
stack.pop();
|
|
@@ -193,9 +266,9 @@ export function parseStructureText(text: string): StructureEntry[] {
|
|
|
193
266
|
rootEntries.push(fileEntry);
|
|
194
267
|
}
|
|
195
268
|
|
|
196
|
-
//
|
|
269
|
+
// We still push files into the stack at this level so that
|
|
270
|
+
// bad indentation under them can be detected and rejected.
|
|
197
271
|
stack.push({ level, entry: fileEntry, isDir: false });
|
|
198
|
-
// but next lines at same or lower level will pop correctly
|
|
199
272
|
}
|
|
200
273
|
}
|
|
201
274
|
|
package/src/index.ts
CHANGED
package/src/schema/config.ts
CHANGED
|
@@ -143,9 +143,18 @@ export interface ScaffoldConfig {
|
|
|
143
143
|
* runner / CLI.
|
|
144
144
|
*/
|
|
145
145
|
watch?: boolean;
|
|
146
|
-
}
|
|
147
146
|
|
|
148
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Number of spaces per indent level in structure files.
|
|
150
|
+
* Default: 2.
|
|
151
|
+
*
|
|
152
|
+
* Examples:
|
|
153
|
+
* - 2 → "··entry"
|
|
154
|
+
* - 4 → "····entry"
|
|
155
|
+
*/
|
|
156
|
+
indentStep?: number;
|
|
157
|
+
}
|
|
149
158
|
/**
|
|
150
159
|
* Options when scanning an existing directory into a structure.txt tree.
|
|
151
160
|
*/
|