@timeax/scaffold 0.0.4 → 0.0.6

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.
@@ -2,105 +2,113 @@
2
2
 
3
3
  import path from 'path';
4
4
  import chokidar from 'chokidar';
5
- import { runOnce, type RunOptions } from './runner';
6
- import { defaultLogger, type Logger } from '../util/logger';
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
- * Debounce delay in milliseconds between detected changes
11
- * and a scaffold re-run.
12
- *
13
- * Default: 150 ms
14
- */
15
- debounceMs?: number;
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
- * Optional logger; falls back to defaultLogger.child('[watch]').
19
- */
20
- logger?: Logger;
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
- const logger = options.logger ?? defaultLogger.child('[watch]');
36
+ const logger = options.logger ?? defaultLogger.child('[watch]');
34
37
 
35
- const scaffoldDir = options.scaffoldDir
36
- ? path.resolve(cwd, options.scaffoldDir)
37
- : path.resolve(cwd, 'scaffold');
38
+ const scaffoldDir = options.scaffoldDir
39
+ ? path.resolve(cwd, options.scaffoldDir)
40
+ : path.resolve(cwd, SCAFFOLD_ROOT_DIR);
38
41
 
39
- const debounceMs = options.debounceMs ?? 150;
42
+ const debounceMs = options.debounceMs ?? 150;
40
43
 
41
- logger.info(`Watching scaffold directory: ${scaffoldDir}`);
44
+ logger.info(`Watching scaffold directory: ${scaffoldDir}`);
42
45
 
43
- let timer: NodeJS.Timeout | undefined;
44
- let running = false;
45
- let pending = false;
46
+ let timer: NodeJS.Timeout | undefined;
47
+ let running = false;
48
+ let pending = false;
46
49
 
47
- async function run() {
48
- if (running) {
49
- pending = true;
50
- return;
51
- }
52
- running = true;
53
- try {
54
- logger.info('Change detected → running scaffold...');
55
- await runOnce(cwd, {
56
- ...options,
57
- // we already resolved scaffoldDir for watcher; pass it down
58
- scaffoldDir,
59
- });
60
- logger.info('Scaffold run completed.');
61
- } catch (err) {
62
- logger.error('Scaffold run failed:', err);
63
- } finally {
64
- running = false;
65
- if (pending) {
66
- pending = false;
67
- timer = setTimeout(run, debounceMs);
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
- function scheduleRun() {
73
- if (timer) clearTimeout(timer);
74
- timer = setTimeout(run, debounceMs);
75
- }
75
+ function scheduleRun() {
76
+ if (timer) clearTimeout(timer);
77
+ timer = setTimeout(run, debounceMs);
78
+ }
76
79
 
77
- const watcher = chokidar.watch(
78
- [
79
- path.join(scaffoldDir, 'config.*'),
80
- path.join(scaffoldDir, '*.txt'),
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
- watcher
88
- .on('add', (filePath) => {
89
- logger.debug(`File added: ${filePath}`);
90
- scheduleRun();
91
- })
92
- .on('change', (filePath) => {
93
- logger.debug(`File changed: ${filePath}`);
94
- scheduleRun();
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
- // Initial run
105
- scheduleRun();
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
  }
@@ -1,12 +1,50 @@
1
1
  // src/schema/config.ts
2
2
 
3
3
  import type {
4
- RegularHookConfig,
5
- RegularHookKind,
6
- StubConfig,
4
+ RegularHookConfig,
5
+ RegularHookKind,
6
+ StubConfig,
7
7
  } from './hooks';
8
- import type { StructureEntry } from './structure';
9
-
8
+ import type {StructureEntry} from './structure';
9
+
10
+ // src/schema/index.ts (or wherever ScaffoldConfig lives)
11
+
12
+ export type FormatMode = 'strict' | 'loose';
13
+
14
+ export interface FormatConfig {
15
+ /**
16
+ * Enable CLI-driven formatting.
17
+ *
18
+ * If false or omitted, formatting is only run when the user explicitly
19
+ * passes `--format` on the CLI.
20
+ */
21
+ enabled?: boolean;
22
+
23
+ /**
24
+ * Default indent step for formatting.
25
+ * Falls back to top-level `indentStep` or 2 if undefined.
26
+ */
27
+ indentStep?: number;
28
+
29
+ /**
30
+ * AST mode used by the formatter.
31
+ * - 'loose' (default): try to repair mild indentation issues.
32
+ * - 'strict': keep lines as-is and only normalize cosmetic whitespace.
33
+ */
34
+ mode?: FormatMode;
35
+
36
+ /**
37
+ * Sort non-comment entries lexicographically within their parent block.
38
+ * Matches the simple "sortEntries" behaviour you already had.
39
+ */
40
+ sortEntries?: boolean;
41
+
42
+ /**
43
+ * When running `scaffold --watch`, if true then any changed structure file
44
+ * is formatted after save and before the scaffold run.
45
+ */
46
+ formatOnWatch?: boolean;
47
+ }
10
48
 
11
49
 
12
50
  /**
@@ -17,36 +55,36 @@ import type { StructureEntry } from './structure';
17
55
  * structure definition.
18
56
  */
19
57
  export interface StructureGroupConfig {
20
- /**
21
- * Human-readable identifier for the group (e.g. "app", "routes", "frontend").
22
- * Used mainly for logging and, optionally, cache metadata.
23
- */
24
- name: string;
25
-
26
- /**
27
- * Root directory for this group, relative to the overall project root.
28
- *
29
- * Example: "app", "routes", "resources/js".
30
- *
31
- * All paths produced from this group's structure are resolved
32
- * relative to this directory.
33
- */
34
- root: string;
35
-
36
- /**
37
- * Optional inline structure entries for this group.
38
- * If present and non-empty, these take precedence over `structureFile`.
39
- */
40
- structure?: StructureEntry[];
41
-
42
- /**
43
- * Name of the structure file inside the scaffold directory for this group.
44
- *
45
- * Example: "app.txt", "routes.txt".
46
- *
47
- * If omitted, the default is `<name>.txt` within the scaffold directory.
48
- */
49
- structureFile?: string;
58
+ /**
59
+ * Human-readable identifier for the group (e.g. "app", "routes", "frontend").
60
+ * Used mainly for logging and, optionally, cache metadata.
61
+ */
62
+ name: string;
63
+
64
+ /**
65
+ * Root directory for this group, relative to the overall project root.
66
+ *
67
+ * Example: "app", "routes", "resources/js".
68
+ *
69
+ * All paths produced from this group's structure are resolved
70
+ * relative to this directory.
71
+ */
72
+ root: string;
73
+
74
+ /**
75
+ * Optional inline structure entries for this group.
76
+ * If present and non-empty, these take precedence over `structureFile`.
77
+ */
78
+ structure?: StructureEntry[];
79
+
80
+ /**
81
+ * Name of the structure file inside the scaffold directory for this group.
82
+ *
83
+ * Example: "app.txt", "routes.txt".
84
+ *
85
+ * If omitted, the default is `<name>.txt` within the scaffold directory.
86
+ */
87
+ structureFile?: string;
50
88
  }
51
89
 
52
90
  /**
@@ -56,134 +94,140 @@ export interface StructureGroupConfig {
56
94
  * project, or from any programmatic usage of the library.
57
95
  */
58
96
  export interface ScaffoldConfig {
59
- /**
60
- * Absolute or relative project root (where files are created).
61
- *
62
- * If omitted, the engine will treat `process.cwd()` as the root.
63
- */
64
- root?: string;
65
-
66
- /**
67
- * Base directory where structures are applied and files/folders
68
- * are actually created.
69
- *
70
- * This is resolved relative to `root` (not CWD).
71
- *
72
- * Default: same as `root`.
73
- *
74
- * Examples:
75
- * - base: '.' with root: '.' → apply to <cwd>
76
- * - base: 'src' with root: '.' → apply to <cwd>/src
77
- * - base: '..' with root: 'tools' → apply to <cwd>/tools/..
78
- */
79
- base?: string;
80
-
81
- /**
82
- * Path to the scaffold cache file, relative to `root`.
83
- *
84
- * Default: ".scaffold-cache.json"
85
- */
86
- cacheFile?: string;
87
-
88
- /**
89
- * File size threshold (in bytes) above which deletions become
90
- * interactive (e.g. ask "are you sure?").
91
- *
92
- * Default is determined by the core engine (e.g. 128 KB).
93
- */
94
- sizePromptThreshold?: number;
95
-
96
- /**
97
- * Optional single-root structure (legacy or simple mode).
98
- *
99
- * If `groups` is defined and non-empty, this is ignored.
100
- * Paths are relative to `root` in this mode.
101
- */
102
- structure?: StructureEntry[];
103
-
104
- /**
105
- * Name of the single structure file in the scaffold directory
106
- * for legacy mode.
107
- *
108
- * If `groups` is empty and `structure` is not provided, this
109
- * file name is used (default: "structure.txt").
110
- */
111
- structureFile?: string;
112
-
113
- /**
114
- * Multiple structure groups (recommended).
115
- *
116
- * When provided and non-empty, the engine will iterate over each
117
- * group and apply its structure relative to each group's `root`.
118
- */
119
- groups?: StructureGroupConfig[];
120
-
121
- /**
122
- * Hook configuration for file lifecycle events.
123
- *
124
- * Each category (e.g. "preCreateFile") is an array of hook configs,
125
- * each with its own `include` / `exclude` / `files` filters.
126
- */
127
- hooks?: {
128
- [K in RegularHookKind]?: RegularHookConfig[];
129
- };
130
-
131
- /**
132
- * Stub definitions keyed by stub name.
133
- *
134
- * These are referenced from structure entries by `stub: name`.
135
- */
136
- stubs?: Record<string, StubConfig>;
137
-
138
- /**
139
- * When true, the CLI or consuming code may choose to start scaffold
140
- * in watch mode by default (implementation-specific).
141
- *
142
- * This flag itself does not start watch mode; it is a hint to the
143
- * runner / CLI.
144
- */
145
- watch?: boolean;
146
-
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;
97
+ /**
98
+ * Absolute or relative project root (where files are created).
99
+ *
100
+ * If omitted, the engine will treat `process.cwd()` as the root.
101
+ */
102
+ root?: string;
103
+
104
+ /**
105
+ * Base directory where structures are applied and files/folders
106
+ * are actually created.
107
+ *
108
+ * This is resolved relative to `root` (not CWD).
109
+ *
110
+ * Default: same as `root`.
111
+ *
112
+ * Examples:
113
+ * - base: '.' with root: '.' → apply to <cwd>
114
+ * - base: 'src' with root: '.' → apply to <cwd>/src
115
+ * - base: '..' with root: 'tools' → apply to <cwd>/tools/..
116
+ */
117
+ base?: string;
118
+
119
+ /**
120
+ * Path to the scaffold cache file, relative to `root`.
121
+ *
122
+ * Default: ".scaffold-cache.json"
123
+ */
124
+ cacheFile?: string;
125
+
126
+ /**
127
+ * File size threshold (in bytes) above which deletions become
128
+ * interactive (e.g. ask "are you sure?").
129
+ *
130
+ * Default is determined by the core engine (e.g. 128 KB).
131
+ */
132
+ sizePromptThreshold?: number;
133
+
134
+ /**
135
+ * Optional single-root structure (legacy or simple mode).
136
+ *
137
+ * If `groups` is defined and non-empty, this is ignored.
138
+ * Paths are relative to `root` in this mode.
139
+ */
140
+ structure?: StructureEntry[];
141
+
142
+ /**
143
+ * Name of the single structure file in the scaffold directory
144
+ * for legacy mode.
145
+ *
146
+ * If `groups` is empty and `structure` is not provided, this
147
+ * file name is used (default: "structure.txt").
148
+ */
149
+ structureFile?: string;
150
+
151
+ /**
152
+ * Multiple structure groups (recommended).
153
+ *
154
+ * When provided and non-empty, the engine will iterate over each
155
+ * group and apply its structure relative to each group's `root`.
156
+ */
157
+ groups?: StructureGroupConfig[];
158
+
159
+ /**
160
+ * Hook configuration for file lifecycle events.
161
+ *
162
+ * Each category (e.g. "preCreateFile") is an array of hook configs,
163
+ * each with its own `include` / `exclude` / `files` filters.
164
+ */
165
+ hooks?: {
166
+ [K in RegularHookKind]?: RegularHookConfig[];
167
+ };
168
+
169
+ /**
170
+ * Stub definitions keyed by stub name.
171
+ *
172
+ * These are referenced from structure entries by `stub: name`.
173
+ */
174
+ stubs?: Record<string, StubConfig>;
175
+
176
+ /**
177
+ * When true, the CLI or consuming code may choose to start scaffold
178
+ * in watch mode by default (implementation-specific).
179
+ *
180
+ * This flag itself does not start watch mode; it is a hint to the
181
+ * runner / CLI.
182
+ */
183
+ watch?: boolean;
184
+
185
+
186
+ /**
187
+ * Number of spaces per indent level in structure files.
188
+ * Default: 2.
189
+ *
190
+ * Examples:
191
+ * - 2 → "··entry"
192
+ * - 4 → "····entry"
193
+ */
194
+ indentStep?: number;
195
+
196
+ /**
197
+ * Formatting configuration for structure files.
198
+ */
199
+ format?: FormatConfig;
157
200
  }
201
+
158
202
  /**
159
203
  * Options when scanning an existing directory into a structure.txt tree.
160
204
  */
161
205
  export interface ScanStructureOptions {
162
- /**
163
- * Glob patterns (relative to the scanned root) to ignore.
164
- */
165
- ignore?: string[];
166
-
167
- /**
168
- * Maximum depth to traverse (0 = only that dir).
169
- * Default: Infinity (no limit).
170
- */
171
- maxDepth?: number;
206
+ /**
207
+ * Glob patterns (relative to the scanned root) to ignore.
208
+ */
209
+ ignore?: string[];
210
+
211
+ /**
212
+ * Maximum depth to traverse (0 = only that dir).
213
+ * Default: Infinity (no limit).
214
+ */
215
+ maxDepth?: number;
172
216
  }
173
217
 
174
218
  /**
175
219
  * Options when scanning based on the scaffold config/groups.
176
220
  */
177
221
  export interface ScanFromConfigOptions extends ScanStructureOptions {
178
- /**
179
- * If provided, only scan these group names (by `StructureGroupConfig.name`).
180
- * If omitted, all groups are scanned (or single-root mode).
181
- */
182
- groups?: string[];
183
-
184
- /**
185
- * Optional override for scaffold directory; normally you can let
186
- * loadScaffoldConfig resolve this from "<cwd>/scaffold".
187
- */
188
- scaffoldDir?: string;
222
+ /**
223
+ * If provided, only scan these group names (by `StructureGroupConfig.name`).
224
+ * If omitted, all groups are scanned (or single-root mode).
225
+ */
226
+ groups?: string[];
227
+
228
+ /**
229
+ * Optional override for scaffold directory; normally you can let
230
+ * loadScaffoldConfig resolve this from "<cwd>/scaffold".
231
+ */
232
+ scaffoldDir?: string;
189
233
  }
package/tsup.config.ts CHANGED
@@ -1,66 +1,30 @@
1
1
  // tsup.config.ts
2
- import {defineConfig} from 'tsup';
2
+ import { defineConfig } from 'tsup';
3
3
 
4
- export default defineConfig([
5
- {
6
- entry: ['src/index.ts'],
7
- outDir: 'dist',
8
- format: ['esm', 'cjs'],
9
- dts: true,
10
- sourcemap: true,
11
- clean: true,
12
- target: 'node18',
13
- platform: 'node',
14
- treeshake: true,
15
- splitting: false, // small lib, keep it simple
16
- outExtension({format}) {
17
- return {
18
- // ESM → .mjs, CJS → .cjs
19
- js: format === 'esm' ? '.mjs' : '.cjs',
20
- };
21
- },
4
+ export default defineConfig({
5
+ entry: {
6
+ index: 'src/index.ts',
7
+ cli: 'src/cli/main.ts',
8
+ ast: 'src/ast/index.ts',
22
9
  },
23
-
24
- // CLI build (scaffold command)
25
- {
10
+ outDir: 'dist',
11
+ format: ['esm', 'cjs'],
12
+ // Only generate dts for index + ast (not CLI)
13
+ dts: {
26
14
  entry: {
27
- cli: 'src/cli/main.ts',
28
- },
29
- outDir: 'dist',
30
- format: ['esm', 'cjs'],
31
- dts: false,
32
- sourcemap: true,
33
- clean: false, // don't blow away the lib build
34
- target: 'node18',
35
- platform: 'node',
36
- treeshake: true,
37
- splitting: false,
38
- outExtension({format}) {
39
- return {
40
- js: format === 'esm' ? '.mjs' : '.cjs',
41
- };
42
- },
43
- banner: {
44
- js: '#!/usr/bin/env node',
15
+ index: 'src/index.ts',
16
+ ast: 'src/ast/index.ts',
45
17
  },
46
18
  },
47
- {
48
- entry: {
49
- ast: "src/ast/index.ts",
50
- },
51
- outDir: "dist",
52
- format: ['esm', 'cjs'],
53
- dts: true,
54
- sourcemap: true,
55
- clean: false,
56
- target: 'node18',
57
- platform: 'node',
58
- treeshake: true,
59
- splitting: false,
60
- outExtension({format}) {
61
- return {
62
- js: format === 'esm' ? '.mjs' : '.cjs',
63
- };
64
- },
65
- }
66
- ]);
19
+ sourcemap: true,
20
+ clean: true,
21
+ target: 'node18',
22
+ platform: 'node',
23
+ treeshake: true,
24
+ splitting: false,
25
+ outExtension({ format }) {
26
+ return {
27
+ js: format === 'esm' ? '.mjs' : '.cjs',
28
+ };
29
+ },
30
+ });