@timeax/scaffold 0.0.3 → 0.0.5
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/.vscode/settings.json +12 -0
- package/dist/ast.cjs +438 -0
- package/dist/ast.cjs.map +1 -0
- package/dist/ast.d.cts +152 -0
- package/dist/ast.d.ts +152 -0
- package/dist/ast.mjs +433 -0
- package/dist/ast.mjs.map +1 -0
- package/package.json +9 -3
- package/readme.md +215 -110
- package/src/ast/format.ts +262 -0
- package/src/ast/index.ts +2 -0
- package/src/ast/parser.ts +593 -0
- package/src/core/format.ts +87 -0
- package/src/core/init-scaffold.ts +126 -88
- package/src/core/runner.ts +74 -66
- package/src/core/structure-txt.ts +196 -222
- package/src/core/watcher.ts +87 -79
- package/src/schema/config.ts +198 -154
- package/test/format-roundtrip.spec.ts +20 -0
- package/test/format.spec.ts +104 -0
- package/test/parser-diagnostics.spec.ts +86 -0
- package/test/parser-tree.spec.ts +102 -0
- package/tsup.config.ts +61 -43
- package/vitest.config.ts +9 -0
- package/dist/cli.cjs +0 -1182
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.mjs +0 -1171
- package/dist/cli.mjs.map +0 -1
- package/dist/index.cjs +0 -864
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs +0 -848
- package/dist/index.mjs.map +0 -1
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// src/core/structure-txt.ts
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
3
|
+
import type {StructureEntry, DirEntry, FileEntry} from '../schema';
|
|
4
|
+
import {toPosixPath} from '../util/fs-utils';
|
|
5
|
+
import {mapThrough} from "../ast";
|
|
5
6
|
|
|
6
7
|
interface ParsedLine {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
lineNo: number;
|
|
9
|
+
indentSpaces: number;
|
|
10
|
+
rawPath: string;
|
|
11
|
+
stub?: string;
|
|
12
|
+
include?: string[];
|
|
13
|
+
exclude?: string[];
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -26,39 +27,12 @@ interface ParsedLine {
|
|
|
26
27
|
* preceded by whitespace (space or tab).
|
|
27
28
|
*/
|
|
28
29
|
function stripInlineComment(content: string): string {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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();
|
|
30
|
+
const cutIndex = mapThrough(content);
|
|
31
|
+
if (cutIndex === -1) {
|
|
32
|
+
return content.trimEnd();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return content.slice(0, cutIndex).trimEnd();
|
|
62
36
|
}
|
|
63
37
|
|
|
64
38
|
/**
|
|
@@ -69,77 +43,77 @@ function stripInlineComment(content: string): string {
|
|
|
69
43
|
* - @exclude:pattern,pattern2
|
|
70
44
|
*/
|
|
71
45
|
function parseLine(line: string, lineNo: number): ParsedLine | null {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
46
|
+
const match = line.match(/^(\s*)(.*)$/);
|
|
47
|
+
if (!match) return null;
|
|
48
|
+
|
|
49
|
+
const indentSpaces = match[1].length;
|
|
50
|
+
let rest = match[2];
|
|
51
|
+
|
|
52
|
+
// If line (after indent) is empty, skip
|
|
53
|
+
if (!rest.trim()) return null;
|
|
54
|
+
|
|
55
|
+
// Full-line comments after indent
|
|
56
|
+
const trimmedRest = rest.trimStart();
|
|
57
|
+
if (trimmedRest.startsWith('#') || trimmedRest.startsWith('//')) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Strip inline comments (# or //) before parsing tokens
|
|
62
|
+
const stripped = stripInlineComment(rest);
|
|
63
|
+
const trimmed = stripped.trim();
|
|
64
|
+
if (!trimmed) return null;
|
|
65
|
+
|
|
66
|
+
const parts = trimmed.split(/\s+/);
|
|
67
|
+
if (!parts.length) return null;
|
|
68
|
+
|
|
69
|
+
const pathToken = parts[0];
|
|
70
|
+
|
|
71
|
+
// 🚫 Reserve ":" for annotations only – paths may not contain it.
|
|
72
|
+
if (pathToken.includes(':')) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`structure.txt: ":" is reserved for annotations (@stub:, @include:, etc). ` +
|
|
75
|
+
`Invalid path "${pathToken}" on line ${lineNo}.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let stub: string | undefined;
|
|
80
|
+
const include: string[] = [];
|
|
81
|
+
const exclude: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const token of parts.slice(1)) {
|
|
84
|
+
if (token.startsWith('@stub:')) {
|
|
85
|
+
stub = token.slice('@stub:'.length);
|
|
86
|
+
} else if (token.startsWith('@include:')) {
|
|
87
|
+
const val = token.slice('@include:'.length);
|
|
88
|
+
if (val) {
|
|
89
|
+
include.push(
|
|
90
|
+
...val
|
|
91
|
+
.split(',')
|
|
92
|
+
.map((s) => s.trim())
|
|
93
|
+
.filter(Boolean),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
} else if (token.startsWith('@exclude:')) {
|
|
97
|
+
const val = token.slice('@exclude:'.length);
|
|
98
|
+
if (val) {
|
|
99
|
+
exclude.push(
|
|
100
|
+
...val
|
|
101
|
+
.split(',')
|
|
102
|
+
.map((s) => s.trim())
|
|
103
|
+
.filter(Boolean),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
lineNo,
|
|
111
|
+
indentSpaces,
|
|
112
|
+
rawPath: pathToken,
|
|
113
|
+
stub,
|
|
114
|
+
include: include.length ? include : undefined,
|
|
115
|
+
exclude: exclude.length ? exclude : undefined,
|
|
116
|
+
};
|
|
143
117
|
}
|
|
144
118
|
|
|
145
119
|
/**
|
|
@@ -154,123 +128,123 @@ function parseLine(line: string, lineNo: number): ParsedLine | null {
|
|
|
154
128
|
* - Folders must end with "/" in the txt; paths are normalized to POSIX.
|
|
155
129
|
*/
|
|
156
130
|
export function parseStructureText(
|
|
157
|
-
|
|
158
|
-
|
|
131
|
+
text: string,
|
|
132
|
+
indentStep = 2,
|
|
159
133
|
): StructureEntry[] {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
for (let i = 0; i < lines.length; i++) {
|
|
164
|
-
const lineNo = i + 1;
|
|
165
|
-
const p = parseLine(lines[i], lineNo);
|
|
166
|
-
if (p) parsed.push(p);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const rootEntries: StructureEntry[] = [];
|
|
134
|
+
const lines = text.split(/\r?\n/);
|
|
135
|
+
const parsed: ParsedLine[] = [];
|
|
170
136
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
137
|
+
for (let i = 0; i < lines.length; i++) {
|
|
138
|
+
const lineNo = i + 1;
|
|
139
|
+
const p = parseLine(lines[i], lineNo);
|
|
140
|
+
if (p) parsed.push(p);
|
|
141
|
+
}
|
|
176
142
|
|
|
177
|
-
|
|
143
|
+
const rootEntries: StructureEntry[] = [];
|
|
178
144
|
|
|
179
|
-
|
|
180
|
-
|
|
145
|
+
type StackItem = {
|
|
146
|
+
level: number;
|
|
147
|
+
entry: DirEntry | FileEntry;
|
|
148
|
+
isDir: boolean;
|
|
149
|
+
};
|
|
181
150
|
|
|
182
|
-
|
|
183
|
-
throw new Error(
|
|
184
|
-
`structure.txt: Invalid indent on line ${lineNo}. ` +
|
|
185
|
-
`Indent must be multiples of ${indentStep} spaces.`,
|
|
186
|
-
);
|
|
187
|
-
}
|
|
151
|
+
const stack: StackItem[] = [];
|
|
188
152
|
|
|
189
|
-
|
|
153
|
+
for (const p of parsed) {
|
|
154
|
+
const {indentSpaces, lineNo} = p;
|
|
190
155
|
|
|
191
|
-
|
|
192
|
-
if (level > stack.length) {
|
|
193
|
-
// e.g. current stack depth 1, but line level=3 is invalid
|
|
194
|
-
if (level !== stack.length + 1) {
|
|
195
|
-
throw new Error(
|
|
196
|
-
`structure.txt: Invalid indentation on line ${lineNo}. ` +
|
|
197
|
-
`You cannot jump more than one level at a time. ` +
|
|
198
|
-
`Previous depth: ${stack.length}, this line depth: ${level}.`,
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// If this line is indented (level > 0), parent must exist and must be dir
|
|
204
|
-
if (level > 0) {
|
|
205
|
-
const parent = stack[level - 1]; // parent level is (level - 1)
|
|
206
|
-
if (!parent) {
|
|
207
|
-
throw new Error(
|
|
208
|
-
`structure.txt: Indented entry without a parent on line ${lineNo}.`,
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
if (!parent.isDir) {
|
|
156
|
+
if (indentSpaces % indentStep !== 0) {
|
|
212
157
|
throw new Error(
|
|
213
|
-
|
|
214
|
-
|
|
158
|
+
`structure.txt: Invalid indent on line ${lineNo}. ` +
|
|
159
|
+
`Indent must be multiples of ${indentStep} spaces.`,
|
|
215
160
|
);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const level = indentSpaces / indentStep;
|
|
164
|
+
|
|
165
|
+
// Determine parent level and enforce no skipping
|
|
166
|
+
if (level > stack.length) {
|
|
167
|
+
// e.g. current stack depth 1, but line level=3 is invalid
|
|
168
|
+
if (level !== stack.length + 1) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`structure.txt: Invalid indentation on line ${lineNo}. ` +
|
|
171
|
+
`You cannot jump more than one level at a time. ` +
|
|
172
|
+
`Previous depth: ${stack.length}, this line depth: ${level}.`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// If this line is indented (level > 0), parent must exist and must be dir
|
|
178
|
+
if (level > 0) {
|
|
179
|
+
const parent = stack[level - 1]; // parent level is (level - 1)
|
|
180
|
+
if (!parent) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`structure.txt: Indented entry without a parent on line ${lineNo}.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (!parent.isDir) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`structure.txt: Cannot indent under a file on line ${lineNo}. ` +
|
|
188
|
+
`Files cannot have children. Parent: "${parent.entry.path}".`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const isDir = p.rawPath.endsWith('/');
|
|
194
|
+
const clean = p.rawPath.replace(/\/$/, '');
|
|
195
|
+
const basePath = toPosixPath(clean);
|
|
196
|
+
|
|
197
|
+
// Pop stack until we are at the correct depth
|
|
198
|
+
while (stack.length > level) {
|
|
199
|
+
stack.pop();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const parent = stack[stack.length - 1]?.entry as DirEntry | undefined;
|
|
203
|
+
const parentPath = parent ? parent.path.replace(/\/$/, '') : '';
|
|
204
|
+
|
|
205
|
+
const fullPath = parentPath
|
|
206
|
+
? `${parentPath}/${basePath}${isDir ? '/' : ''}`
|
|
207
|
+
: `${basePath}${isDir ? '/' : ''}`;
|
|
208
|
+
|
|
209
|
+
if (isDir) {
|
|
210
|
+
const dirEntry: DirEntry = {
|
|
211
|
+
type: 'dir',
|
|
212
|
+
path: fullPath,
|
|
213
|
+
children: [],
|
|
214
|
+
...(p.stub ? {stub: p.stub} : {}),
|
|
215
|
+
...(p.include ? {include: p.include} : {}),
|
|
216
|
+
...(p.exclude ? {exclude: p.exclude} : {}),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (parent && parent.type === 'dir') {
|
|
220
|
+
parent.children = parent.children ?? [];
|
|
221
|
+
parent.children.push(dirEntry);
|
|
222
|
+
} else if (!parent) {
|
|
223
|
+
rootEntries.push(dirEntry);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
stack.push({level, entry: dirEntry, isDir: true});
|
|
227
|
+
} else {
|
|
228
|
+
const fileEntry: FileEntry = {
|
|
229
|
+
type: 'file',
|
|
230
|
+
path: fullPath,
|
|
231
|
+
...(p.stub ? {stub: p.stub} : {}),
|
|
232
|
+
...(p.include ? {include: p.include} : {}),
|
|
233
|
+
...(p.exclude ? {exclude: p.exclude} : {}),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
if (parent && parent.type === 'dir') {
|
|
237
|
+
parent.children = parent.children ?? [];
|
|
238
|
+
parent.children.push(fileEntry);
|
|
239
|
+
} else if (!parent) {
|
|
240
|
+
rootEntries.push(fileEntry);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// We still push files into the stack at this level so that
|
|
244
|
+
// bad indentation under them can be detected and rejected.
|
|
245
|
+
stack.push({level, entry: fileEntry, isDir: false});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return rootEntries;
|
|
276
250
|
}
|
package/src/core/watcher.ts
CHANGED
|
@@ -2,105 +2,113 @@
|
|
|
2
2
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import chokidar from 'chokidar';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import {runOnce, type RunOptions} from './runner';
|
|
6
|
+
import {defaultLogger, type Logger} from '../util/logger';
|
|
7
|
+
import {SCAFFOLD_ROOT_DIR} from '..';
|
|
7
8
|
|
|
8
9
|
export interface WatchOptions extends RunOptions {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Debounce delay in milliseconds between detected changes
|
|
12
|
+
* and a scaffold re-run.
|
|
13
|
+
*
|
|
14
|
+
* Default: 150 ms
|
|
15
|
+
*/
|
|
16
|
+
debounceMs?: number;
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Optional logger; falls back to defaultLogger.child('[watch]').
|
|
20
|
+
*/
|
|
21
|
+
logger?: Logger;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Watch the scaffold directory and re-run scaffold on changes.
|
|
25
26
|
*
|
|
26
27
|
* This watches:
|
|
27
|
-
* - scaffold/config.* files
|
|
28
|
-
* - scaffold/*.txt files (structures)
|
|
28
|
+
* - .scaffold/config.* files
|
|
29
|
+
* - .scaffold/*.txt / *.tss / *.stx files (structures)
|
|
29
30
|
*
|
|
30
31
|
* CLI can call this when `--watch` is enabled.
|
|
32
|
+
* Any `format` options in RunOptions are passed straight through to `runOnce`,
|
|
33
|
+
* so formatting from config / CLI is applied on each re-run.
|
|
31
34
|
*/
|
|
32
35
|
export function watchScaffold(cwd: string, options: WatchOptions = {}): void {
|
|
33
|
-
|
|
36
|
+
const logger = options.logger ?? defaultLogger.child('[watch]');
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
const scaffoldDir = options.scaffoldDir
|
|
39
|
+
? path.resolve(cwd, options.scaffoldDir)
|
|
40
|
+
: path.resolve(cwd, SCAFFOLD_ROOT_DIR);
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
const debounceMs = options.debounceMs ?? 150;
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
logger.info(`Watching scaffold directory: ${scaffoldDir}`);
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
let timer: NodeJS.Timeout | undefined;
|
|
47
|
+
let running = false;
|
|
48
|
+
let pending = false;
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
50
|
+
async function run() {
|
|
51
|
+
if (running) {
|
|
52
|
+
pending = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
running = true;
|
|
56
|
+
try {
|
|
57
|
+
logger.info('Change detected → running scaffold...');
|
|
58
|
+
await runOnce(cwd, {
|
|
59
|
+
...options,
|
|
60
|
+
// we already resolved scaffoldDir for watcher; pass it down
|
|
61
|
+
scaffoldDir,
|
|
62
|
+
});
|
|
63
|
+
logger.info('Scaffold run completed.');
|
|
64
|
+
} catch (err) {
|
|
65
|
+
logger.error('Scaffold run failed:', err);
|
|
66
|
+
} finally {
|
|
67
|
+
running = false;
|
|
68
|
+
if (pending) {
|
|
69
|
+
pending = false;
|
|
70
|
+
timer = setTimeout(run, debounceMs);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
function scheduleRun() {
|
|
76
|
+
if (timer) clearTimeout(timer);
|
|
77
|
+
timer = setTimeout(run, debounceMs);
|
|
78
|
+
}
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
],
|
|
82
|
-
{
|
|
83
|
-
ignoreInitial: false,
|
|
84
|
-
},
|
|
85
|
-
);
|
|
80
|
+
const watcher = chokidar.watch(
|
|
81
|
+
[
|
|
82
|
+
// config files (ts/js/etc.)
|
|
83
|
+
path.join(scaffoldDir, 'config.*'),
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.on('unlink', (filePath) => {
|
|
97
|
-
logger.debug(`File removed: ${filePath}`);
|
|
98
|
-
scheduleRun();
|
|
99
|
-
})
|
|
100
|
-
.on('error', (error) => {
|
|
101
|
-
logger.error('Watcher error:', error);
|
|
102
|
-
});
|
|
85
|
+
// structure files: plain txt + our custom extensions
|
|
86
|
+
path.join(scaffoldDir, '*.txt'),
|
|
87
|
+
path.join(scaffoldDir, '*.tss'),
|
|
88
|
+
path.join(scaffoldDir, '*.stx'),
|
|
89
|
+
],
|
|
90
|
+
{
|
|
91
|
+
ignoreInitial: false,
|
|
92
|
+
},
|
|
93
|
+
);
|
|
103
94
|
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
watcher
|
|
96
|
+
.on('add', (filePath) => {
|
|
97
|
+
logger.debug(`File added: ${filePath}`);
|
|
98
|
+
scheduleRun();
|
|
99
|
+
})
|
|
100
|
+
.on('change', (filePath) => {
|
|
101
|
+
logger.debug(`File changed: ${filePath}`);
|
|
102
|
+
scheduleRun();
|
|
103
|
+
})
|
|
104
|
+
.on('unlink', (filePath) => {
|
|
105
|
+
logger.debug(`File removed: ${filePath}`);
|
|
106
|
+
scheduleRun();
|
|
107
|
+
})
|
|
108
|
+
.on('error', (error) => {
|
|
109
|
+
logger.error('Watcher error:', error);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Initial run
|
|
113
|
+
scheduleRun();
|
|
106
114
|
}
|