@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
|
@@ -2,48 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
46
|
-
// root: '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
|
-
|
|
114
|
-
|
|
149
|
+
cwd: string,
|
|
150
|
+
options: InitScaffoldOptions = {},
|
|
115
151
|
): Promise<{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
152
|
+
scaffoldDir: string;
|
|
153
|
+
configPath: string;
|
|
154
|
+
structurePath: string;
|
|
155
|
+
created: { config: boolean; structure: boolean };
|
|
120
156
|
}> {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
}
|
package/src/core/runner.ts
CHANGED
|
@@ -1,94 +1,102 @@
|
|
|
1
1
|
// src/core/runner.ts
|
|
2
2
|
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import {loadScaffoldConfig} from './config-loader';
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
resolveGroupStructure,
|
|
7
|
+
resolveSingleStructure,
|
|
8
8
|
} from './resolve-structure';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import type {
|
|
13
|
-
import {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Optional logger override.
|
|
27
|
+
*/
|
|
28
|
+
logger?: Logger;
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
const cache = new CacheManager(projectRoot, cachePath);
|
|
48
|
-
cache.load();
|
|
52
|
+
await formatStructureFilesFromConfig(projectRoot, scaffoldDir, config, {force: options.format})
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
const cachePath = config.cacheFile ?? '.scaffold-cache.json';
|
|
55
|
+
const cache = new CacheManager(projectRoot, cachePath);
|
|
56
|
+
cache.load();
|
|
51
57
|
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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:
|
|
90
|
+
baseDir: projectRoot,
|
|
65
91
|
structure,
|
|
66
92
|
cache,
|
|
67
93
|
hooks,
|
|
68
|
-
groupName:
|
|
69
|
-
groupRoot:
|
|
94
|
+
groupName: 'default',
|
|
95
|
+
groupRoot: '.',
|
|
70
96
|
interactiveDelete: options.interactiveDelete,
|
|
71
|
-
logger:
|
|
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
|
-
|
|
101
|
+
cache.save();
|
|
94
102
|
}
|