@timeax/scaffold 0.0.2 → 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 +55 -14
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +55 -14
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +51 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.mjs +49 -13
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/readme.md +150 -51
- package/src/core/config-loader.ts +4 -3
- package/src/core/init-scaffold.ts +8 -3
- 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
package/readme.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @timeax/scaffold
|
|
2
2
|
|
|
3
3
|
A tiny, opinionated scaffolding tool that keeps your project structure in sync with a **declarative tree** (like `structure.txt`) – Prisma‑style.
|
|
4
4
|
|
|
@@ -8,11 +8,13 @@ A tiny, opinionated scaffolding tool that keeps your project structure in sync w
|
|
|
8
8
|
* Reverse‑engineer existing projects into `*.txt` structures.
|
|
9
9
|
* Watch for changes and re‑apply automatically.
|
|
10
10
|
|
|
11
|
+
> **Supported structure files:** `.tss`, `.stx`, `structure.txt`, and any `.txt` files inside `.scaffold/`.
|
|
12
|
+
|
|
11
13
|
---
|
|
12
14
|
|
|
13
15
|
## Features
|
|
14
16
|
|
|
15
|
-
* **Prisma‑style scaffold directory**: all config and structure lives under
|
|
17
|
+
* **Prisma‑style scaffold directory**: all config and structure lives under `.scaffold/` by default.
|
|
16
18
|
* **Config‑driven groups**: declare multiple roots (e.g. `app`, `frontend`) with their own structure files.
|
|
17
19
|
* **Plain‑text structure files**: strict, easy‑to‑read tree syntax with indentation and annotations.
|
|
18
20
|
* **Safe apply**:
|
|
@@ -26,8 +28,14 @@ A tiny, opinionated scaffolding tool that keeps your project structure in sync w
|
|
|
26
28
|
* Regular hooks around file create/delete.
|
|
27
29
|
* Stub hooks around content generation.
|
|
28
30
|
* **Stubs**: programmatic content generators for files (e.g. React pages, controllers, etc.).
|
|
29
|
-
* **Watch mode**: watch
|
|
31
|
+
* **Watch mode**: watch `.scaffold/` for changes and re‑run automatically.
|
|
30
32
|
* **Scanner**: generate `structure.txt` (or per‑group `*.txt`) from an existing codebase.
|
|
33
|
+
* **VS Code integration** (via a companion extension):
|
|
34
|
+
|
|
35
|
+
* Syntax highlighting for `.tss`, `.stx`, `structure.txt`, and `.scaffold/**/*.txt`.
|
|
36
|
+
* Inline diagnostics using the same parser as the CLI.
|
|
37
|
+
* “Go to file” from a structure line.
|
|
38
|
+
* Simple formatting and sorting commands.
|
|
31
39
|
|
|
32
40
|
---
|
|
33
41
|
|
|
@@ -57,7 +65,7 @@ pnpm scaffold init
|
|
|
57
65
|
This will create:
|
|
58
66
|
|
|
59
67
|
```txt
|
|
60
|
-
scaffold/
|
|
68
|
+
.scaffold/
|
|
61
69
|
config.ts # main ScaffoldConfig
|
|
62
70
|
structure.txt # example structure (single-root mode)
|
|
63
71
|
```
|
|
@@ -74,7 +82,7 @@ scaffold init --dir tools/scaffold
|
|
|
74
82
|
|
|
75
83
|
### 2. Define your structure
|
|
76
84
|
|
|
77
|
-
By default,
|
|
85
|
+
By default, `.scaffold/structure.txt` is used in single‑root mode.
|
|
78
86
|
|
|
79
87
|
Example:
|
|
80
88
|
|
|
@@ -98,12 +106,16 @@ README.md
|
|
|
98
106
|
|
|
99
107
|
**Rules:**
|
|
100
108
|
|
|
101
|
-
* Indent with **2 spaces per level** (strict).
|
|
109
|
+
* Indent with **2 spaces per level** (strict by default; configurable).
|
|
102
110
|
* Directories **must** end with `/`.
|
|
103
111
|
* Files **must not** end with `/`.
|
|
104
112
|
* You **cannot indent under a file** (files cannot have children).
|
|
105
113
|
* You can’t “skip” levels (no jumping from depth 0 to depth 2 in one go).
|
|
106
|
-
* Lines starting with `#` are comments.
|
|
114
|
+
* Lines starting with `#` or `//` (after indentation) are comments.
|
|
115
|
+
* Inline comments are supported:
|
|
116
|
+
|
|
117
|
+
* `index.ts # comment`
|
|
118
|
+
* `index.ts // comment`
|
|
107
119
|
|
|
108
120
|
#### Annotations
|
|
109
121
|
|
|
@@ -124,20 +136,22 @@ Supported inline annotations:
|
|
|
124
136
|
|
|
125
137
|
These map onto the `StructureEntry` fields in TypeScript.
|
|
126
138
|
|
|
139
|
+
> `:` is reserved for annotations (e.g. `@stub:page`). Paths themselves must **not** contain `:`.
|
|
140
|
+
|
|
127
141
|
---
|
|
128
142
|
|
|
129
143
|
### 3. Configure groups (optional but recommended)
|
|
130
144
|
|
|
131
|
-
In
|
|
145
|
+
In `.scaffold/config.ts` you can enable grouped mode:
|
|
132
146
|
|
|
133
147
|
```ts
|
|
134
148
|
import type { ScaffoldConfig } from '@timeax/scaffold';
|
|
135
149
|
|
|
136
150
|
const config: ScaffoldConfig = {
|
|
137
|
-
|
|
151
|
+
base: '.', // project root (optional, defaults to cwd)
|
|
138
152
|
|
|
139
153
|
groups: [
|
|
140
|
-
{ name: 'app',
|
|
154
|
+
{ name: 'app', root: 'app', structureFile: 'app.txt' },
|
|
141
155
|
{ name: 'frontend', root: 'resources/js', structureFile: 'frontend.txt' },
|
|
142
156
|
],
|
|
143
157
|
|
|
@@ -148,21 +162,21 @@ const config: ScaffoldConfig = {
|
|
|
148
162
|
export default config;
|
|
149
163
|
```
|
|
150
164
|
|
|
151
|
-
Then create per‑group structure files in
|
|
165
|
+
Then create per‑group structure files in `.scaffold/`:
|
|
152
166
|
|
|
153
167
|
```txt
|
|
154
|
-
# scaffold/app.txt
|
|
168
|
+
# .scaffold/app.txt
|
|
155
169
|
App/Services/
|
|
156
170
|
UserService.php
|
|
157
171
|
|
|
158
|
-
# scaffold/frontend.txt
|
|
172
|
+
# .scaffold/frontend.txt
|
|
159
173
|
src/
|
|
160
174
|
index.tsx
|
|
161
175
|
pages/
|
|
162
176
|
home.tsx
|
|
163
177
|
```
|
|
164
178
|
|
|
165
|
-
> When `groups` is defined and non‑empty, single‑root `structure
|
|
179
|
+
> When `groups` is defined and non‑empty, single‑root `structure` / `structureFile` is ignored.
|
|
166
180
|
|
|
167
181
|
---
|
|
168
182
|
|
|
@@ -173,16 +187,16 @@ src/
|
|
|
173
187
|
scaffold
|
|
174
188
|
|
|
175
189
|
# or with explicit scaffold dir / config
|
|
176
|
-
scaffold --dir scaffold --config scaffold/config.ts
|
|
190
|
+
scaffold --dir .scaffold --config .scaffold/config.ts
|
|
177
191
|
```
|
|
178
192
|
|
|
179
193
|
What happens:
|
|
180
194
|
|
|
181
|
-
* Config is loaded from
|
|
195
|
+
* Config is loaded from `.scaffold/config.*` (Prisma‑style resolution).
|
|
182
196
|
* Structure(s) are resolved (grouped or single‑root).
|
|
183
197
|
* Files/directories missing on disk are created.
|
|
184
198
|
* New files are registered in `.scaffold-cache.json` (under project root by default).
|
|
185
|
-
* Any previously created files that are no longer in the structure are candidates for deletion
|
|
199
|
+
* Any previously created files that are no longer in the structure are candidates for deletion:
|
|
186
200
|
|
|
187
201
|
* Small files are deleted automatically.
|
|
188
202
|
* Large files (configurable threshold) trigger an interactive prompt.
|
|
@@ -195,8 +209,8 @@ scaffold --watch
|
|
|
195
209
|
|
|
196
210
|
* Watches:
|
|
197
211
|
|
|
198
|
-
*
|
|
199
|
-
*
|
|
212
|
+
* `.scaffold/config.*`
|
|
213
|
+
* `.scaffold/*.txt`
|
|
200
214
|
* Debounces rapid edits.
|
|
201
215
|
* Prevents overlapping runs.
|
|
202
216
|
|
|
@@ -213,11 +227,13 @@ scaffold [options]
|
|
|
213
227
|
Options:
|
|
214
228
|
|
|
215
229
|
* `-c, --config <path>` – override config file path.
|
|
216
|
-
* `-d, --dir <path>` – override scaffold directory (default:
|
|
230
|
+
* `-d, --dir <path>` – override scaffold directory (default: `./.scaffold`).
|
|
217
231
|
* `-w, --watch` – watch scaffold directory for changes.
|
|
218
232
|
* `--quiet` – silence logs.
|
|
219
233
|
* `--debug` – verbose debug logs.
|
|
220
234
|
|
|
235
|
+
---
|
|
236
|
+
|
|
221
237
|
### `scaffold init`
|
|
222
238
|
|
|
223
239
|
Initialize the scaffold directory + config + structure.
|
|
@@ -228,42 +244,44 @@ scaffold init [options]
|
|
|
228
244
|
|
|
229
245
|
Options:
|
|
230
246
|
|
|
231
|
-
* `-d, --dir <path>` – scaffold directory (default:
|
|
247
|
+
* `-d, --dir <path>` – scaffold directory (default: `./.scaffold`, inherited from root options).
|
|
232
248
|
* `--force` – overwrite existing `config.ts` / `structure.txt`.
|
|
233
249
|
|
|
250
|
+
---
|
|
251
|
+
|
|
234
252
|
### `scaffold scan`
|
|
235
253
|
|
|
236
254
|
Generate `structure.txt`‑style definitions from an existing project.
|
|
237
255
|
|
|
238
256
|
Two modes:
|
|
239
257
|
|
|
240
|
-
1.
|
|
258
|
+
#### 1. Config‑aware mode (default if no `--root` / `--out` given)
|
|
241
259
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
260
|
+
```bash
|
|
261
|
+
scaffold scan
|
|
262
|
+
scaffold scan --from-config
|
|
263
|
+
scaffold scan --from-config --groups app frontend
|
|
264
|
+
```
|
|
247
265
|
|
|
248
|
-
|
|
249
|
-
|
|
266
|
+
* Loads `.scaffold/config.ts`.
|
|
267
|
+
* For each `group` in config:
|
|
250
268
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
269
|
+
* Scans `group.root` on disk.
|
|
270
|
+
* Writes to `.scaffold/<group.structureFile || group.name + '.txt'>`.
|
|
271
|
+
* `--groups` filters which groups to scan.
|
|
254
272
|
|
|
255
|
-
2.
|
|
273
|
+
#### 2. Manual mode (single root)
|
|
256
274
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
275
|
+
```bash
|
|
276
|
+
scaffold scan -r src
|
|
277
|
+
scaffold scan -r src -o .scaffold/src.txt
|
|
278
|
+
```
|
|
261
279
|
|
|
262
|
-
|
|
280
|
+
Options:
|
|
263
281
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
282
|
+
* `-r, --root <path>` – directory to scan.
|
|
283
|
+
* `-o, --out <path>` – output file (otherwise prints to stdout).
|
|
284
|
+
* `--ignore <patterns...>` – extra globs to ignore (in addition to defaults like `node_modules/**`, `.git/**`, etc.).
|
|
267
285
|
|
|
268
286
|
---
|
|
269
287
|
|
|
@@ -277,10 +295,10 @@ scaffold structures
|
|
|
277
295
|
|
|
278
296
|
What it does:
|
|
279
297
|
|
|
280
|
-
* Loads
|
|
298
|
+
* Loads `.scaffold/config.*`.
|
|
281
299
|
* Determines which structure files are expected:
|
|
282
300
|
|
|
283
|
-
* **Grouped mode** (`config.groups` defined): each group gets `group.structureFile ||
|
|
301
|
+
* **Grouped mode** (`config.groups` defined): each group gets `group.structureFile || `${group.name}.txt``.
|
|
284
302
|
* **Single-root mode** (no groups): uses `config.structureFile || 'structure.txt'`.
|
|
285
303
|
* For each expected structure file:
|
|
286
304
|
|
|
@@ -296,15 +314,17 @@ Examples:
|
|
|
296
314
|
# { name: 'frontend', root: 'resources/js', structureFile: 'frontend.txt' },
|
|
297
315
|
# ]
|
|
298
316
|
scaffold structures
|
|
299
|
-
# => ensures scaffold/app.txt and scaffold/frontend.txt exist
|
|
317
|
+
# => ensures .scaffold/app.txt and .scaffold/frontend.txt exist
|
|
300
318
|
|
|
301
319
|
# With single-root config:
|
|
302
320
|
# structureFile: 'structure.txt'
|
|
303
321
|
scaffold structures
|
|
304
|
-
# => ensures scaffold/structure.txt exists
|
|
322
|
+
# => ensures .scaffold/structure.txt exists
|
|
305
323
|
```
|
|
306
324
|
|
|
307
|
-
This is useful right after setting up or editing
|
|
325
|
+
This is useful right after setting up or editing `.scaffold/config.ts` so that all declared structure files are present and ready to edit.
|
|
326
|
+
|
|
327
|
+
---
|
|
308
328
|
|
|
309
329
|
## TypeScript API
|
|
310
330
|
|
|
@@ -315,8 +335,8 @@ import { runOnce } from '@timeax/scaffold';
|
|
|
315
335
|
|
|
316
336
|
await runOnce(process.cwd(), {
|
|
317
337
|
// optional overrides
|
|
318
|
-
configPath: 'scaffold/config.ts',
|
|
319
|
-
scaffoldDir: 'scaffold',
|
|
338
|
+
configPath: '.scaffold/config.ts',
|
|
339
|
+
scaffoldDir: '.scaffold',
|
|
320
340
|
});
|
|
321
341
|
```
|
|
322
342
|
|
|
@@ -337,7 +357,7 @@ const results = await scanProjectFromConfig(process.cwd(), {
|
|
|
337
357
|
groups: ['app', 'frontend'],
|
|
338
358
|
});
|
|
339
359
|
|
|
340
|
-
// write group structure files to scaffold/
|
|
360
|
+
// write group structure files to .scaffold/
|
|
341
361
|
await writeScannedStructuresFromConfig(process.cwd(), {
|
|
342
362
|
groups: ['app'],
|
|
343
363
|
});
|
|
@@ -401,7 +421,9 @@ const config: ScaffoldConfig = {
|
|
|
401
421
|
name: 'page',
|
|
402
422
|
async getContent(ctx) {
|
|
403
423
|
const name = ctx.targetPath.split('/').pop();
|
|
404
|
-
return `export default function ${name}() {
|
|
424
|
+
return `export default function ${name}() {
|
|
425
|
+
return <div>${name}</div>;
|
|
426
|
+
}`;
|
|
405
427
|
},
|
|
406
428
|
hooks: {
|
|
407
429
|
preStub: [
|
|
@@ -418,7 +440,7 @@ const config: ScaffoldConfig = {
|
|
|
418
440
|
};
|
|
419
441
|
```
|
|
420
442
|
|
|
421
|
-
In
|
|
443
|
+
In a structure file:
|
|
422
444
|
|
|
423
445
|
```txt
|
|
424
446
|
src/
|
|
@@ -459,5 +481,82 @@ Some things this package is intentionally designed to grow into:
|
|
|
459
481
|
* Stub groups (one logical stub creating multiple files).
|
|
460
482
|
* Built‑in templates for common stacks (Laravel + Inertia, Next.js, etc.).
|
|
461
483
|
* Better diff/dry‑run UX (show what will change without touching disk).
|
|
484
|
+
* Deeper VS Code integration:
|
|
485
|
+
|
|
486
|
+
* Tree-aware sorting.
|
|
487
|
+
* Visual tree editor.
|
|
488
|
+
* Code actions / quick fixes for common mistakes.
|
|
462
489
|
|
|
463
490
|
PRs and ideas are welcome ✨
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## VS Code extension
|
|
495
|
+
|
|
496
|
+
There is an official VS Code companion extension for `@timeax/scaffold` that makes working with your structure files much nicer.
|
|
497
|
+
|
|
498
|
+
### Language support
|
|
499
|
+
|
|
500
|
+
The extension adds a custom language **Scaffold Structure** and:
|
|
501
|
+
|
|
502
|
+
* Highlights:
|
|
503
|
+
|
|
504
|
+
* Directories (lines ending with `/`)
|
|
505
|
+
* Files
|
|
506
|
+
* Inline annotations like `@stub:name`, `@include:pattern`, `@exclude:pattern`
|
|
507
|
+
* Comments using `#` or `//` (full-line and inline)
|
|
508
|
+
* Treats the following files as scaffold structures:
|
|
509
|
+
|
|
510
|
+
* `*.tss`
|
|
511
|
+
* `*.stx`
|
|
512
|
+
* `structure.txt`
|
|
513
|
+
* Any `.txt` file inside your `.scaffold/` directory
|
|
514
|
+
|
|
515
|
+
The syntax rules match the CLI parser:
|
|
516
|
+
|
|
517
|
+
* Indent is in fixed steps (configurable via `indentStep`).
|
|
518
|
+
* Only directories can have children.
|
|
519
|
+
* `:` is reserved for annotations and not allowed inside path names.
|
|
520
|
+
|
|
521
|
+
### Editor commands
|
|
522
|
+
|
|
523
|
+
The extension contributes several commands (available via the Command Palette and context menus when editing a scaffold structure file):
|
|
524
|
+
|
|
525
|
+
* **Scaffold: Go to file**
|
|
526
|
+
|
|
527
|
+
* Reads the path on the current line and opens the corresponding file in your project.
|
|
528
|
+
* Respects `.scaffold/config.*`:
|
|
529
|
+
|
|
530
|
+
* Uses `base`/`root` to resolve paths.
|
|
531
|
+
* If the current structure file belongs to a `group`, it resolves relative to that group’s `root`.
|
|
532
|
+
* If the file doesn’t exist, it can prompt to create it and open it immediately.
|
|
533
|
+
|
|
534
|
+
* **Scaffold: Format structure file**
|
|
535
|
+
|
|
536
|
+
* Normalizes line endings and trims trailing whitespace.
|
|
537
|
+
* Designed to be safe even on partially-invalid files.
|
|
538
|
+
* Future versions may use the full AST from `@timeax/scaffold` to enforce indentation and ordering.
|
|
539
|
+
|
|
540
|
+
* **Scaffold: Sort entries**
|
|
541
|
+
|
|
542
|
+
* Naive helper that sorts non-comment lines lexicographically while keeping comment/blank lines in place.
|
|
543
|
+
* Useful for quick cleanups of small structure files.
|
|
544
|
+
|
|
545
|
+
* **Scaffold: Open config**
|
|
546
|
+
|
|
547
|
+
* Opens `.scaffold/config.*` for the current workspace (searching common extensions like `config.ts`, `config.mts`, etc.).
|
|
548
|
+
|
|
549
|
+
* **Scaffold: Open .scaffold folder**
|
|
550
|
+
|
|
551
|
+
* Reveals the `.scaffold/` directory in the VS Code Explorer.
|
|
552
|
+
|
|
553
|
+
### Live validation (diagnostics)
|
|
554
|
+
|
|
555
|
+
Whenever you open or edit a scaffold structure file:
|
|
556
|
+
|
|
557
|
+
* The extension calls `parseStructureText` from `@timeax/scaffold` under the hood.
|
|
558
|
+
* If parsing fails, the error message (e.g. invalid indentation, children under a file, bad path, etc.) is shown as an editor diagnostic (squiggly underline) on the relevant line.
|
|
559
|
+
|
|
560
|
+
This means your editor and the CLI always agree on what is valid, since they share the same parser and rules.
|
|
561
|
+
|
|
562
|
+
> The extension is optional, but highly recommended if you edit `*.tss` / `*.stx` or `structure.txt` files frequently.
|
|
@@ -7,12 +7,13 @@ import crypto from 'crypto';
|
|
|
7
7
|
import { pathToFileURL } from 'url';
|
|
8
8
|
import { transform } from 'esbuild';
|
|
9
9
|
|
|
10
|
-
import type
|
|
10
|
+
import { SCAFFOLD_ROOT_DIR, type ScaffoldConfig } from '../schema';
|
|
11
11
|
import { defaultLogger } from '../util/logger';
|
|
12
12
|
import { ensureDirSync } from '../util/fs-utils';
|
|
13
13
|
|
|
14
14
|
const logger = defaultLogger.child('[config]');
|
|
15
15
|
|
|
16
|
+
|
|
16
17
|
export interface LoadScaffoldConfigOptions {
|
|
17
18
|
/**
|
|
18
19
|
* Optional explicit scaffold directory path (absolute or relative to cwd).
|
|
@@ -68,7 +69,7 @@ export async function loadScaffoldConfig(
|
|
|
68
69
|
// First pass: figure out an initial scaffold dir just to locate config.*
|
|
69
70
|
const initialScaffoldDir = options.scaffoldDir
|
|
70
71
|
? path.resolve(absCwd, options.scaffoldDir)
|
|
71
|
-
: path.join(absCwd,
|
|
72
|
+
: path.join(absCwd, SCAFFOLD_ROOT_DIR);
|
|
72
73
|
|
|
73
74
|
const configPath =
|
|
74
75
|
options.configPath ?? resolveConfigPath(initialScaffoldDir);
|
|
@@ -85,7 +86,7 @@ export async function loadScaffoldConfig(
|
|
|
85
86
|
// Final scaffoldDir (can still be overridden by CLI)
|
|
86
87
|
const scaffoldDir = options.scaffoldDir
|
|
87
88
|
? path.resolve(absCwd, options.scaffoldDir)
|
|
88
|
-
: path.join(configRoot,
|
|
89
|
+
: path.join(configRoot, SCAFFOLD_ROOT_DIR);
|
|
89
90
|
|
|
90
91
|
// projectRoot (base) is relative to configRoot
|
|
91
92
|
const baseRoot = config.base
|
|
@@ -4,6 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { ensureDirSync } from '../util/fs-utils';
|
|
6
6
|
import { defaultLogger } from '../util/logger';
|
|
7
|
+
import { SCAFFOLD_ROOT_DIR } from '../schema';
|
|
7
8
|
|
|
8
9
|
const logger = defaultLogger.child('[init]');
|
|
9
10
|
|
|
@@ -52,7 +53,10 @@ const config: ScaffoldConfig = {
|
|
|
52
53
|
// base: 'src', // apply to <root>/src
|
|
53
54
|
// base: '..', // apply to parent of <root>
|
|
54
55
|
// base: '.',
|
|
55
|
-
|
|
56
|
+
|
|
57
|
+
// Number of spaces per indent level in structure files (default: 2).
|
|
58
|
+
// indentStep: 2,
|
|
59
|
+
|
|
56
60
|
// Cache file path, relative to base.
|
|
57
61
|
// cacheFile: '.scaffold-cache.json',
|
|
58
62
|
|
|
@@ -85,7 +89,8 @@ const config: ScaffoldConfig = {
|
|
|
85
89
|
export default config;
|
|
86
90
|
`;
|
|
87
91
|
|
|
88
|
-
|
|
92
|
+
|
|
93
|
+
const DEFAULT_STRUCTURE_TXT = `# ${SCAFFOLD_ROOT_DIR}/structure.txt
|
|
89
94
|
# Example structure definition.
|
|
90
95
|
# - Indent with 2 spaces per level
|
|
91
96
|
# - Directories must end with "/"
|
|
@@ -113,7 +118,7 @@ export async function initScaffold(
|
|
|
113
118
|
structurePath: string;
|
|
114
119
|
created: { config: boolean; structure: boolean };
|
|
115
120
|
}> {
|
|
116
|
-
const scaffoldDirRel = options.scaffoldDir ??
|
|
121
|
+
const scaffoldDirRel = options.scaffoldDir ?? SCAFFOLD_ROOT_DIR;
|
|
117
122
|
const scaffoldDirAbs = path.resolve(cwd, scaffoldDirRel);
|
|
118
123
|
const configFileName = options.configFileName ?? 'config.ts';
|
|
119
124
|
const structureFileName = options.structureFileName ?? 'structure.txt';
|
|
@@ -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
|
*/
|