@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.
@@ -2,48 +2,50 @@
2
2
 
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
- import { ensureDirSync } from '../util/fs-utils';
6
- import { defaultLogger } from '../util/logger';
7
- import { SCAFFOLD_ROOT_DIR } from '../schema';
5
+ import {ensureDirSync} from '../util/fs-utils';
6
+ import {defaultLogger} from '../util/logger';
7
+ import {SCAFFOLD_ROOT_DIR} from '../schema';
8
8
 
9
9
  const logger = defaultLogger.child('[init]');
10
10
 
11
11
  export interface InitScaffoldOptions {
12
- /**
13
- * Path to the scaffold directory (relative to cwd).
14
- * Default: "scaffold"
15
- */
16
- scaffoldDir?: string;
17
-
18
- /**
19
- * Overwrite existing config/structure files if they already exist.
20
- */
21
- force?: boolean;
22
-
23
- /**
24
- * Name of the config file inside the scaffold directory.
25
- * Default: "config.ts"
26
- */
27
- configFileName?: string;
28
-
29
- /**
30
- * Name of the default structure file inside the scaffold directory
31
- * for single-root mode.
32
- * Default: "structure.txt"
33
- */
34
- structureFileName?: string;
12
+ /**
13
+ * Path to the scaffold directory (relative to cwd).
14
+ * Default: ".scaffold"
15
+ */
16
+ scaffoldDir?: string;
17
+
18
+ /**
19
+ * Overwrite existing config/structure files if they already exist.
20
+ */
21
+ force?: boolean;
22
+
23
+ /**
24
+ * Name of the config file inside the scaffold directory.
25
+ * Default: "config.ts"
26
+ */
27
+ configFileName?: string;
28
+
29
+ /**
30
+ * Name of the default structure file inside the scaffold directory
31
+ * for single-root mode.
32
+ * Default: "structure.txt"
33
+ */
34
+ structureFileName?: string;
35
35
  }
36
36
 
37
- // src/core/init-scaffold.ts
37
+ // ---------------------------------------------------------------------------
38
+ // Default config + structure templates
39
+ // ---------------------------------------------------------------------------
38
40
 
39
41
  const DEFAULT_CONFIG_TS = `import type { ScaffoldConfig } from '@timeax/scaffold';
40
42
 
41
43
  const config: ScaffoldConfig = {
42
- // Root for resolving the scaffold/ folder & this config file.
44
+ // Root for resolving the .scaffold folder & this config file.
43
45
  // By default, this is the directory where you run \`scaffold\`.
44
46
  // Example:
45
- // root: '.', // scaffold/ at <cwd>/scaffold
46
- // root: 'tools', // scaffold/ at <cwd>/tools/scaffold
47
+ // root: '.', // .scaffold at <cwd>/.scaffold
48
+ // root: 'tools', // .scaffold at <cwd>/tools/.scaffold
47
49
  // root: '.',
48
50
 
49
51
  // Base directory where structures are applied and files/folders are created.
@@ -53,13 +55,42 @@ const config: ScaffoldConfig = {
53
55
  // base: 'src', // apply to <root>/src
54
56
  // base: '..', // apply to parent of <root>
55
57
  // base: '.',
56
-
58
+
57
59
  // Number of spaces per indent level in structure files (default: 2).
60
+ // This also informs the formatter when indenting entries.
58
61
  // indentStep: 2,
59
-
62
+
60
63
  // Cache file path, relative to base.
61
64
  // cacheFile: '.scaffold-cache.json',
62
65
 
66
+ // Formatting options for structure files.
67
+ // These are used by:
68
+ // - \`scaffold --format\` (forces formatting before apply)
69
+ // - \`scaffold --watch\` when \`formatOnWatch\` is true
70
+ //
71
+ // format: {
72
+ // // Enable config-driven formatting in general.
73
+ // // \`scaffold --format\` always forces formatting even if this is false.
74
+ // enabled: true,
75
+ //
76
+ // // Override indent step specifically for formatting (falls back to
77
+ // // top-level \`indentStep\` if omitted).
78
+ // indentStep: 2,
79
+ //
80
+ // // AST mode:
81
+ // // - 'loose' (default): tries to repair mild indentation issues.
82
+ // // - 'strict': mostly cosmetic changes (trims trailing whitespace, etc.).
83
+ // mode: 'loose',
84
+ //
85
+ // // Sort non-comment entries lexicographically within their parent block.
86
+ // // Comments and blank lines keep their relative positions.
87
+ // sortEntries: true,
88
+ //
89
+ // // When running \`scaffold --watch\`, format structure files on each
90
+ // // detected change before applying scaffold.
91
+ // formatOnWatch: true,
92
+ // },
93
+
63
94
  // --- Single-structure mode (simple) ---
64
95
  // structureFile: 'structure.txt',
65
96
 
@@ -83,20 +114,25 @@ const config: ScaffoldConfig = {
83
114
  // getContent: (ctx) =>
84
115
  // \`export default function Page() { return <div>\${ctx.targetPath}</div>; }\`,
85
116
  // },
86
- },
117
+ }
87
118
  };
88
119
 
89
120
  export default config;
90
121
  `;
91
122
 
92
-
93
123
  const DEFAULT_STRUCTURE_TXT = `# ${SCAFFOLD_ROOT_DIR}/structure.txt
94
124
  # Example structure definition.
95
- # - Indent with 2 spaces per level
125
+ # - Indent with 2 spaces per level (or your configured indentStep)
96
126
  # - Directories must end with "/"
97
127
  # - Files do not
98
128
  # - Lines starting with "#" are comments and ignored by parser
99
-
129
+ # - Inline comments are allowed after "#" or "//" separated by whitespace
130
+ #
131
+ # The formatter (when enabled via config.format or --format) will:
132
+ # - Normalize indentation based on indentStep
133
+ # - Preserve blank lines and comments
134
+ # - Keep inline comments attached to their entries
135
+ #
100
136
  # Example:
101
137
  # src/
102
138
  # index.ts
@@ -110,58 +146,60 @@ const DEFAULT_STRUCTURE_TXT = `# ${SCAFFOLD_ROOT_DIR}/structure.txt
110
146
  * - Writes a default structure.txt if missing (or if force = true).
111
147
  */
112
148
  export async function initScaffold(
113
- cwd: string,
114
- options: InitScaffoldOptions = {},
149
+ cwd: string,
150
+ options: InitScaffoldOptions = {},
115
151
  ): Promise<{
116
- scaffoldDir: string;
117
- configPath: string;
118
- structurePath: string;
119
- created: { config: boolean; structure: boolean };
152
+ scaffoldDir: string;
153
+ configPath: string;
154
+ structurePath: string;
155
+ created: { config: boolean; structure: boolean };
120
156
  }> {
121
- const scaffoldDirRel = options.scaffoldDir ?? SCAFFOLD_ROOT_DIR;
122
- const scaffoldDirAbs = path.resolve(cwd, scaffoldDirRel);
123
- const configFileName = options.configFileName ?? 'config.ts';
124
- const structureFileName = options.structureFileName ?? 'structure.txt';
125
-
126
- ensureDirSync(scaffoldDirAbs);
127
-
128
- const configPath = path.join(scaffoldDirAbs, configFileName);
129
- const structurePath = path.join(scaffoldDirAbs, structureFileName);
130
-
131
- let createdConfig = false;
132
- let createdStructure = false;
133
-
134
- // config.ts
135
- if (fs.existsSync(configPath) && !options.force) {
136
- logger.info(`Config already exists at ${configPath} (use --force to overwrite).`);
137
- } else {
138
- fs.writeFileSync(configPath, DEFAULT_CONFIG_TS, 'utf8');
139
- createdConfig = true;
140
- logger.info(
141
- `${fs.existsSync(configPath) ? 'Overwrote' : 'Created'} config at ${configPath}`,
142
- );
143
- }
144
-
145
- // structure.txt
146
- if (fs.existsSync(structurePath) && !options.force) {
147
- logger.info(
148
- `Structure file already exists at ${structurePath} (use --force to overwrite).`,
149
- );
150
- } else {
151
- fs.writeFileSync(structurePath, DEFAULT_STRUCTURE_TXT, 'utf8');
152
- createdStructure = true;
153
- logger.info(
154
- `${fs.existsSync(structurePath) ? 'Overwrote' : 'Created'} structure file at ${structurePath}`,
155
- );
156
- }
157
-
158
- return {
159
- scaffoldDir: scaffoldDirAbs,
160
- configPath,
161
- structurePath,
162
- created: {
163
- config: createdConfig,
164
- structure: createdStructure,
165
- },
166
- };
157
+ const scaffoldDirRel = options.scaffoldDir ?? SCAFFOLD_ROOT_DIR;
158
+ const scaffoldDirAbs = path.resolve(cwd, scaffoldDirRel);
159
+ const configFileName = options.configFileName ?? 'config.ts';
160
+ const structureFileName = options.structureFileName ?? 'structure.txt';
161
+
162
+ ensureDirSync(scaffoldDirAbs);
163
+
164
+ const configPath = path.join(scaffoldDirAbs, configFileName);
165
+ const structurePath = path.join(scaffoldDirAbs, structureFileName);
166
+
167
+ let createdConfig = false;
168
+ let createdStructure = false;
169
+
170
+ // config.ts
171
+ if (fs.existsSync(configPath) && !options.force) {
172
+ logger.info(
173
+ `Config already exists at ${configPath} (use --force to overwrite).`,
174
+ );
175
+ } else {
176
+ fs.writeFileSync(configPath, DEFAULT_CONFIG_TS, 'utf8');
177
+ createdConfig = true;
178
+ logger.info(
179
+ `${fs.existsSync(configPath) ? 'Overwrote' : 'Created'} config at ${configPath}`,
180
+ );
181
+ }
182
+
183
+ // structure.txt
184
+ if (fs.existsSync(structurePath) && !options.force) {
185
+ logger.info(
186
+ `Structure file already exists at ${structurePath} (use --force to overwrite).`,
187
+ );
188
+ } else {
189
+ fs.writeFileSync(structurePath, DEFAULT_STRUCTURE_TXT, 'utf8');
190
+ createdStructure = true;
191
+ logger.info(
192
+ `${fs.existsSync(structurePath) ? 'Overwrote' : 'Created'} structure file at ${structurePath}`,
193
+ );
194
+ }
195
+
196
+ return {
197
+ scaffoldDir: scaffoldDirAbs,
198
+ configPath,
199
+ structurePath,
200
+ created: {
201
+ config: createdConfig,
202
+ structure: createdStructure,
203
+ },
204
+ };
167
205
  }
@@ -1,94 +1,102 @@
1
1
  // src/core/runner.ts
2
2
 
3
3
  import path from 'path';
4
- import { loadScaffoldConfig } from './config-loader';
4
+ import {loadScaffoldConfig} from './config-loader';
5
5
  import {
6
- resolveGroupStructure,
7
- resolveSingleStructure,
6
+ resolveGroupStructure,
7
+ resolveSingleStructure,
8
8
  } from './resolve-structure';
9
- import { CacheManager } from './cache-manager';
10
- import { HookRunner } from './hook-runner';
11
- import { applyStructure, type InteractiveDeleteParams } from './apply-structure';
12
- import type { Logger } from '../util/logger';
13
- import { defaultLogger } from '../util/logger';
9
+ import {CacheManager} from './cache-manager';
10
+ import {HookRunner} from './hook-runner';
11
+ import {applyStructure, type InteractiveDeleteParams} from './apply-structure';
12
+ import type {Logger} from '../util/logger';
13
+ import {defaultLogger} from '../util/logger';
14
+ import {formatStructureFilesFromConfig} from "./format";
14
15
 
15
16
  export interface RunOptions {
16
- /**
17
- * Optional interactive delete callback; if omitted, deletions
18
- * above the size threshold will be skipped (kept + removed from cache).
19
- */
20
- interactiveDelete?: (
21
- params: InteractiveDeleteParams,
22
- ) => Promise<'delete' | 'keep'>;
17
+ /**
18
+ * Optional interactive delete callback; if omitted, deletions
19
+ * above the size threshold will be skipped (kept + removed from cache).
20
+ */
21
+ interactiveDelete?: (
22
+ params: InteractiveDeleteParams,
23
+ ) => Promise<'delete' | 'keep'>;
23
24
 
24
- /**
25
- * Optional logger override.
26
- */
27
- logger?: Logger;
25
+ /**
26
+ * Optional logger override.
27
+ */
28
+ logger?: Logger;
28
29
 
29
- /**
30
- * Optional overrides (e.g. allow CLI to point at a different scaffold dir).
31
- */
32
- scaffoldDir?: string;
33
- configPath?: string;
30
+ /**
31
+ * Optional overrides (e.g. allow CLI to point at a different scaffold dir).
32
+ */
33
+ scaffoldDir?: string;
34
+ configPath?: string;
35
+ /**
36
+ * If true, force formatting even if config.format?.enabled === false.
37
+ * This is what `--format` will use.
38
+ */
39
+ format?: boolean;
34
40
  }
35
41
 
36
42
  /**
37
43
  * Run scaffold once for the current working directory.
38
44
  */
39
45
  export async function runOnce(cwd: string, options: RunOptions = {}): Promise<void> {
40
- const logger = options.logger ?? defaultLogger.child('[runner]');
41
- const { config, scaffoldDir, projectRoot } = await loadScaffoldConfig(cwd, {
42
- scaffoldDir: options.scaffoldDir,
43
- configPath: options.configPath,
44
- });
46
+ const logger = options.logger ?? defaultLogger.child('[runner]');
47
+ const {config, scaffoldDir, projectRoot} = await loadScaffoldConfig(cwd, {
48
+ scaffoldDir: options.scaffoldDir,
49
+ configPath: options.configPath,
50
+ });
45
51
 
46
- const cachePath = config.cacheFile ?? '.scaffold-cache.json';
47
- const cache = new CacheManager(projectRoot, cachePath);
48
- cache.load();
52
+ await formatStructureFilesFromConfig(projectRoot, scaffoldDir, config, {force: options.format})
49
53
 
50
- const hooks = new HookRunner(config);
54
+ const cachePath = config.cacheFile ?? '.scaffold-cache.json';
55
+ const cache = new CacheManager(projectRoot, cachePath);
56
+ cache.load();
51
57
 
52
- // Grouped mode
53
- if (config.groups && config.groups.length > 0) {
54
- for (const group of config.groups) {
55
- const groupRootAbs = path.resolve(projectRoot, group.root);
56
- const structure = resolveGroupStructure(scaffoldDir, group);
58
+ const hooks = new HookRunner(config);
57
59
 
58
- const groupLogger = logger.child(`[group:${group.name}]`);
60
+ // Grouped mode
61
+ if (config.groups && config.groups.length > 0) {
62
+ for (const group of config.groups) {
63
+ const groupRootAbs = path.resolve(projectRoot, group.root);
64
+ const structure = resolveGroupStructure(scaffoldDir, group);
59
65
 
60
- // eslint-disable-next-line no-await-in-loop
61
- await applyStructure({
66
+ const groupLogger = logger.child(`[group:${group.name}]`);
67
+
68
+ // eslint-disable-next-line no-await-in-loop
69
+ await applyStructure({
70
+ config,
71
+ projectRoot,
72
+ baseDir: groupRootAbs,
73
+ structure,
74
+ cache,
75
+ hooks,
76
+ groupName: group.name,
77
+ groupRoot: group.root,
78
+ interactiveDelete: options.interactiveDelete,
79
+ logger: groupLogger,
80
+ });
81
+ }
82
+ } else {
83
+ // Single-root mode
84
+ const structure = resolveSingleStructure(scaffoldDir, config);
85
+ const baseLogger = logger.child('[group:default]');
86
+
87
+ await applyStructure({
62
88
  config,
63
89
  projectRoot,
64
- baseDir: groupRootAbs,
90
+ baseDir: projectRoot,
65
91
  structure,
66
92
  cache,
67
93
  hooks,
68
- groupName: group.name,
69
- groupRoot: group.root,
94
+ groupName: 'default',
95
+ groupRoot: '.',
70
96
  interactiveDelete: options.interactiveDelete,
71
- logger: groupLogger,
72
- });
73
- }
74
- } else {
75
- // Single-root mode
76
- const structure = resolveSingleStructure(scaffoldDir, config);
77
- const baseLogger = logger.child('[group:default]');
78
-
79
- await applyStructure({
80
- config,
81
- projectRoot,
82
- baseDir: projectRoot,
83
- structure,
84
- cache,
85
- hooks,
86
- groupName: 'default',
87
- groupRoot: '.',
88
- interactiveDelete: options.interactiveDelete,
89
- logger: baseLogger,
90
- });
91
- }
97
+ logger: baseLogger,
98
+ });
99
+ }
92
100
 
93
- cache.save();
101
+ cache.save();
94
102
  }